From 3aab80b73df021e4b42997d15861bd73bc7f1126 Mon Sep 17 00:00:00 2001 From: MJ Date: Thu, 26 Mar 2020 22:23:14 +0100 Subject: [PATCH] Added profiling utilities. #183 --- CHANGELOG.md | 4 + app/README.md | 48 +++++++++++- app/src/main/kotlin/ktx/app/profiling.kt | 79 ++++++++++++++++++++ app/src/test/kotlin/ktx/app/profilingTest.kt | 53 +++++++++++++ graphics/README.md | 2 + 5 files changed, 183 insertions(+), 3 deletions(-) create mode 100644 app/src/main/kotlin/ktx/app/profiling.kt create mode 100644 app/src/test/kotlin/ktx/app/profilingTest.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index da620e20..810e42a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/app/README.md b/app/README.md index 459fe34d..921cd996 100644 --- a/app/README.md +++ b/app/README.md @@ -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`: @@ -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. @@ -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) diff --git a/app/src/main/kotlin/ktx/app/profiling.kt b/app/src/main/kotlin/ktx/app/profiling.kt new file mode 100644 index 00000000..ba76f5e0 --- /dev/null +++ b/app/src/main/kotlin/ktx/app/profiling.kt @@ -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, "--------------------------------------------") +} + diff --git a/app/src/test/kotlin/ktx/app/profilingTest.kt b/app/src/test/kotlin/ktx/app/profilingTest.kt new file mode 100644 index 00000000..e618407b --- /dev/null +++ b/app/src/test/kotlin/ktx/app/profilingTest.kt @@ -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 + } +} diff --git a/graphics/README.md b/graphics/README.md index f3188742..41b915b8 100644 --- a/graphics/README.md +++ b/graphics/README.md @@ -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")) @@ -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)