Skip to content

Commit

Permalink
Added profiling utilities. #183
Browse files Browse the repository at this point in the history
  • Loading branch information
czyzby committed Mar 26, 2020
1 parent 24a452c commit 3aab80b
Show file tree
Hide file tree
Showing 5 changed files with 183 additions and 3 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

- **[UPDATE]** Updated to Kotlin 1.3.71.
- **[UPDATE]** Updated to Kotlin Coroutines 1.3.5.
- **[FEATURE]** (`ktx-app`) Added profiling utilities.
- `profile` inlined function allows to profile an operation with the LibGDX `PerformanceCounter`.
- `PerformanceCounter.profile` inlined extension method eases usage of `PerformanceCounter` API.
- `PerformanceCounter.prettyPrint` allows to print basic performance data after profiling.
- **[FEATURE]** (`ktx-ashley`) Added `Entity.contains` (`in` operator) that checks if an `Entity` has a `Component`.
- **[FEATURE]** (`ktx-async`) Added `RenderingScope` factory function for custom scopes using rendering thread dispatcher.
- **[FEATURE]** (`ktx-graphics`) Added `takeScreenshot` utility function that allows to save a screenshot of the application.
Expand Down
48 changes: 45 additions & 3 deletions app/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,12 @@ similarly to `ScreenViewport`. Thanks to customizable target PPI value, it is id
different screen sizes.
- `emptyScreen` provides no-op implementations of `Screen`.

#### Profiling

- `profile` inlined function allows to measure performance of the chosen operation with LibGDX `PerformanceCounter`.
- `PerformanceCounter.profile` inlined extension method eases direct usage of the `PerformanceCounter` class.
- `PerformanceCounter.prettyPrint` extension method allows to quickly log basic performance metrics.

### Usage examples

Implementing `KtxApplicationAdapter`:
Expand Down Expand Up @@ -129,6 +135,41 @@ val viewport: Viewport = LetterboxingViewport(targetPpiX = 96f, targetPpiY = 96f
viewport.update(Gdx.graphics.width, Gdx.graphics.height, true)
```

Profiling an operation:

```Kotlin
import ktx.app.profile

fun profileThreadSleep() {
profile(name = "Thread.sleep", repeats = 10) {
// Will be repeated 10 times to measure performance:
Thread.sleep(10L)
}
}
```

Profiling an operation with an existing `PerformanceCounter`:

```Kotlin
import com.badlogic.gdx.utils.PerformanceCounter
import ktx.app.prettyPrint
import ktx.app.profile

fun profileThreadSleep() {
// Window size passed to the constructor as the second argument
// will be the default amount of repetitions during profiling:
val profiler = PerformanceCounter("Thread.sleep", 10)
profiler.profile {
// Will be repeated 10 times to measure performance:
Thread.sleep(10L)
}

// You can also print the report manually
// with a custom number format:
profiler.prettyPrint(decimalFormat = "%.4f s")
}
```

### Alternatives

There are some general purpose LibGDX utility libraries out there, but most lack first-class Kotlin support.
Expand All @@ -138,11 +179,12 @@ library with some classes similar to `ktx-app`.
- [LibGDX Markup Language](https://github.com/czyzby/gdx-lml/tree/master/lml) allows to build `Scene2D` views using
HTML-like syntax. It also features a custom `ApplicationListener` implementation, which helps with managing `Scene2D`
screens.
- [Autumn MVC](https://github.com/czyzby/gdx-lml/tree/master/mvc) is a [Spring](https://spring.io/)-inspired
- [Autumn MVC](https://github.com/czyzby/gdx-lml/tree/master/mvc) is a [Spring](https://spring.io/) inspired
model-view-controller framework built on top of LibGDX. It features its own `ApplicationListener` implementation, which
initiates and handles annotated view instances.

#### Additional documentation

- [The life cycle article.](https://github.com/libgdx/libgdx/wiki/The-life-cycle)
- [Viewports article.](https://github.com/libgdx/libgdx/wiki/Viewports)
- [Official life cycle article.](https://github.com/libgdx/libgdx/wiki/The-life-cycle)
- [Official viewports article.](https://github.com/libgdx/libgdx/wiki/Viewports)
- [Official article on profiling.](https://github.com/libgdx/libgdx/wiki/Profiling)
79 changes: 79 additions & 0 deletions app/src/main/kotlin/ktx/app/profiling.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package ktx.app

import com.badlogic.gdx.*
import com.badlogic.gdx.utils.PerformanceCounter

/**
* Profiles the given [operation] using a [PerformanceCounter].
* The operation will be repeated [repeats] times to gather the performance data.
* [PerformanceCounter.tick] will be called after each operation.
* [repeats] will be used to set the window size of the [PerformanceCounter].
* If [printResults] is set to true, a short summary will be printed by the application.
*
* [PerformanceCounter] used for the profiling will be returned, so that the profiling
* data can be analyzed and further tests can be performed. Note that to perform further
* profiling with this [PerformanceCounter] of a different operation,
* [PerformanceCounter.reset] should be called.
*/
inline fun profile(
name: String = "Profiler", repeats: Int = 10, printResults: Boolean = true,
operation: () -> Unit
): PerformanceCounter {
val performanceCounter = PerformanceCounter(name, repeats)
performanceCounter.profile(repeats, printResults, operation)
return performanceCounter
}

/**
* Profiles the given [operation] using this [PerformanceCounter].
* The operation will be repeated [repeats] times to gather the performance data.
* [PerformanceCounter.tick] will be called after each operation.
* By default, [repeats] is set to the window size passed to the [PerformanceCounter]
* constructor or 10 if the window size is set to 1.
* If [printResults] is set to true, a short summary will be printed by the application.
*
* Note that to perform further profiling with this [PerformanceCounter] of a different
* operation, [PerformanceCounter.reset] should be called.
*/
inline fun PerformanceCounter.profile(
repeats: Int = if(time.mean != null) time.mean.windowSize else 10,
printResults: Boolean = true,
operation: () -> Unit
) {
if (this.time.count == 0) tick()
repeat(repeats) {
this.start()
operation()
this.stop()
this.tick()
}
if (printResults) {
prettyPrint()
}
}

/**
* Logs profiling information of this [PerformanceCounter] as an organized block.
* Uses passed [decimalFormat] to format floating point numbers.
*/
fun PerformanceCounter.prettyPrint(decimalFormat: String = "%.6fs") {
Gdx.app.log(name, "--------------------------------------------")
Gdx.app.log(name, "Number of repeats: ${time.count}")
val mean = time.mean
val minimum: Float
val maximum: Float
if (mean != null && mean.hasEnoughData()) {
Gdx.app.log(name, "Average OP time: ${decimalFormat.format(mean.mean)} " +
"± ${decimalFormat.format(mean.standardDeviation())}")
minimum = mean.lowest
maximum = mean.highest
} else {
Gdx.app.log(name, "Average OP time: ${decimalFormat.format(time.average)}")
minimum = time.min
maximum = time.max
}
Gdx.app.log(name, "Minimum OP time: ${decimalFormat.format(minimum)}")
Gdx.app.log(name, "Maximum OP time: ${decimalFormat.format(maximum)}")
Gdx.app.log(name, "--------------------------------------------")
}

53 changes: 53 additions & 0 deletions app/src/test/kotlin/ktx/app/profilingTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package ktx.app

import com.badlogic.gdx.Gdx
import com.badlogic.gdx.utils.PerformanceCounter
import com.nhaarman.mockitokotlin2.mock
import org.junit.After
import org.junit.Test
import org.junit.Assert.*
import org.junit.Before

class ProfilingTest {
@Before
fun `initiate LibGDX`() {
Gdx.app = mock()
}

@Test
fun `should profile operation`() {
var repeats = 0

val performanceCounter = profile(name = "Thread.sleep", repeats = 10) {
repeats++
Thread.sleep(10L)
}

assertEquals("Thread.sleep", performanceCounter.name)
assertEquals(10, performanceCounter.time.mean.windowSize)
assertEquals(10, performanceCounter.time.count)
assertEquals(10, repeats)
assertEquals(0.01f, performanceCounter.time.mean.mean, 0.002f)
}

@Test
fun `should profile operation with existing PerformanceCounter`() {
val performanceCounter = PerformanceCounter("Thread.sleep", 10)
var repeats = 0

performanceCounter.profile {
repeats++
Thread.sleep(10L)
}

assertEquals("Thread.sleep", performanceCounter.name)
assertEquals(10, performanceCounter.time.count)
assertEquals(10, repeats)
assertEquals(0.01f, performanceCounter.time.mean.mean, 0.002f)
}

@After
fun `destroy LibGDX`() {
Gdx.app = null
}
}
2 changes: 2 additions & 0 deletions graphics/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@ fun drawCircle(renderer: ShapeRenderer) {
Taking a screenshot of the current game screen:

```Kotlin
import com.badlogic.gdx.Gdx
import ktx.graphics.takeScreenshot

takeScreenshot(Gdx.files.external("mygame/screenshot.png"))
Expand All @@ -188,3 +189,4 @@ library with some utilities similar to `ktx-graphics`.
- [`SpriteBatch` official article.](https://github.com/libgdx/libgdx/wiki/Spritebatch%2C-Textureregions%2C-and-Sprites)
- [Official article on shaders.](https://github.com/libgdx/libgdx/wiki/Shaders)
- [`ShapeRenderer` official article.](https://github.com/libgdx/libgdx/wiki/Rendering-shapes)
- [Official article on screenshots.](https://github.com/libgdx/libgdx/wiki/Taking-a-Screenshot)

0 comments on commit 3aab80b

Please sign in to comment.