From 2cf56e47bacfc2335f1e5cbc0d0430c88443fdb3 Mon Sep 17 00:00:00 2001 From: Igor Demin Date: Mon, 27 Jan 2025 03:21:12 +0100 Subject: [PATCH 1/5] Exclude vsync time from frames times Currently, we include the time when we wait for vsync which gives us wrong results --- .../benchmarks/src/commonMain/kotlin/MeasureComposable.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/benchmarks/multiplatform/benchmarks/src/commonMain/kotlin/MeasureComposable.kt b/benchmarks/multiplatform/benchmarks/src/commonMain/kotlin/MeasureComposable.kt index 830c38996f..9ef1663cc7 100644 --- a/benchmarks/multiplatform/benchmarks/src/commonMain/kotlin/MeasureComposable.kt +++ b/benchmarks/multiplatform/benchmarks/src/commonMain/kotlin/MeasureComposable.kt @@ -91,7 +91,7 @@ suspend fun measureComposable( val start = markNow() repeat(frameCount) { - val frameStart = start + nextVSync + val frameStart = markNow() scene.render(canvas, nextVSync.inWholeNanoseconds) surface.flushAndSubmit(false) From a50240e590f3643574654644cadb5abda00cecd7 Mon Sep 17 00:00:00 2001 From: Igor Demin Date: Mon, 27 Jan 2025 03:22:27 +0100 Subject: [PATCH 2/5] Mimic the behavior of Skiko rendering into window - Draw into an intermediate Picture. Without it benchmarks showed an improvement by 10%, but in fact it was a regression by 10% - Clear Canvas each frame --- .../commonMain/kotlin/MeasureComposable.kt | 27 +++++++++++++++---- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/benchmarks/multiplatform/benchmarks/src/commonMain/kotlin/MeasureComposable.kt b/benchmarks/multiplatform/benchmarks/src/commonMain/kotlin/MeasureComposable.kt index 9ef1663cc7..9cb602c84e 100644 --- a/benchmarks/multiplatform/benchmarks/src/commonMain/kotlin/MeasureComposable.kt +++ b/benchmarks/multiplatform/benchmarks/src/commonMain/kotlin/MeasureComposable.kt @@ -2,12 +2,16 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.InternalComposeUiApi import androidx.compose.ui.graphics.asComposeCanvas import androidx.compose.ui.scene.CanvasLayersComposeScene +import androidx.compose.ui.scene.ComposeScene import androidx.compose.ui.unit.IntSize import org.jetbrains.skia.Surface import kotlin.time.Duration import kotlin.time.Duration.Companion.nanoseconds import kotlin.time.ExperimentalTime import kotlinx.coroutines.* +import org.jetbrains.skia.Color +import org.jetbrains.skia.PictureRecorder +import org.jetbrains.skia.Rect import kotlin.time.TimeSource.Monotonic.markNow import kotlin.time.measureTime @@ -46,16 +50,15 @@ suspend fun measureComposable( graphicsContext: GraphicsContext?, content: @Composable () -> Unit ): BenchmarkResult { + val surface = graphicsContext?.surface(width, height) ?: Surface.makeNull(width, height) val scene = CanvasLayersComposeScene(size = IntSize(width, height)) try { val nanosPerFrame = (1.0 / targetFps.toDouble() * nanosPerSecond).toLong() scene.setContent(content) - val surface = graphicsContext?.surface(width, height) ?: Surface.makeNull(width, height) - val canvas = surface.canvas.asComposeCanvas() // warmup repeat(warmupCount) { - scene.render(canvas, it * nanosPerFrame) + scene.mimicSkikoRender(surface, it * nanosPerFrame, width, height) surface.flushAndSubmit(false) graphicsContext?.awaitGPUCompletion() } @@ -67,7 +70,7 @@ suspend fun measureComposable( if (Args.isModeEnabled(Mode.CPU)) { renderTime = measureTime { repeat(frameCount) { - scene.render(canvas, it * nanosPerFrame) + scene.mimicSkikoRender(surface, it * nanosPerFrame, width, height) surface.flushAndSubmit(false) gpuTime += measureTime { graphicsContext?.awaitGPUCompletion() @@ -93,7 +96,7 @@ suspend fun measureComposable( repeat(frameCount) { val frameStart = markNow() - scene.render(canvas, nextVSync.inWholeNanoseconds) + scene.mimicSkikoRender(surface, it * nextVSync.inWholeNanoseconds, width, height) surface.flushAndSubmit(false) val cpuTime = frameStart.elapsedNow() @@ -124,6 +127,20 @@ suspend fun measureComposable( ) } finally { scene.close() + surface.close() runGC() // cleanup for next benchmarks } } + +private val pictureRecorder = PictureRecorder() + +@OptIn(InternalComposeUiApi::class) +fun ComposeScene.mimicSkikoRender(surface: Surface, time: Long, width: Int, height: Int) { + val pictureCanvas = pictureRecorder.beginRecording(Rect(0f, 0f, width.toFloat(), height.toFloat())) + render(pictureCanvas.asComposeCanvas(), time) + val picture = pictureRecorder.finishRecordingAsPicture() + + surface.canvas.clear(Color.TRANSPARENT) + surface.canvas.drawPicture(picture) + picture.close() +} From 1bb85bad7ce280ff4830a3e46716cc366eebd7c2 Mon Sep 17 00:00:00 2001 From: Igor Demin Date: Mon, 27 Jan 2025 03:35:44 +0100 Subject: [PATCH 3/5] Add MavenLocal --- benchmarks/multiplatform/build.gradle.kts | 1 + 1 file changed, 1 insertion(+) diff --git a/benchmarks/multiplatform/build.gradle.kts b/benchmarks/multiplatform/build.gradle.kts index 2f53444284..166c4c163b 100644 --- a/benchmarks/multiplatform/build.gradle.kts +++ b/benchmarks/multiplatform/build.gradle.kts @@ -11,5 +11,6 @@ allprojects { google() mavenCentral() maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") + mavenLocal() } } From 25a99b4b9a7159a48b730e8c422e1ca3b78036aa Mon Sep 17 00:00:00 2001 From: Igor Demin Date: Fri, 7 Feb 2025 20:00:05 +0100 Subject: [PATCH 4/5] val frameStart = start + nextVSync --- .../benchmarks/src/commonMain/kotlin/MeasureComposable.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/benchmarks/multiplatform/benchmarks/src/commonMain/kotlin/MeasureComposable.kt b/benchmarks/multiplatform/benchmarks/src/commonMain/kotlin/MeasureComposable.kt index 9cb602c84e..fc405093be 100644 --- a/benchmarks/multiplatform/benchmarks/src/commonMain/kotlin/MeasureComposable.kt +++ b/benchmarks/multiplatform/benchmarks/src/commonMain/kotlin/MeasureComposable.kt @@ -94,7 +94,7 @@ suspend fun measureComposable( val start = markNow() repeat(frameCount) { - val frameStart = markNow() + val frameStart = start + nextVSync scene.mimicSkikoRender(surface, it * nextVSync.inWholeNanoseconds, width, height) surface.flushAndSubmit(false) From 5dc2e3bbe7db918382e826af14338dac7c401c98 Mon Sep 17 00:00:00 2001 From: Igor Demin Date: Fri, 7 Feb 2025 20:06:04 +0100 Subject: [PATCH 5/5] Add a comment --- .../src/commonMain/kotlin/MeasureComposable.kt | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/benchmarks/multiplatform/benchmarks/src/commonMain/kotlin/MeasureComposable.kt b/benchmarks/multiplatform/benchmarks/src/commonMain/kotlin/MeasureComposable.kt index fc405093be..ed06baadb3 100644 --- a/benchmarks/multiplatform/benchmarks/src/commonMain/kotlin/MeasureComposable.kt +++ b/benchmarks/multiplatform/benchmarks/src/commonMain/kotlin/MeasureComposable.kt @@ -134,6 +134,19 @@ suspend fun measureComposable( private val pictureRecorder = PictureRecorder() +/** + * Mimic Skiko render logic from https://github.com/JetBrains/skiko/blob/eb1f04ec99d50ff0bdb2f592fdf49711a9251aa7/skiko/src/awtMain/kotlin/org/jetbrains/skiko/SkiaLayer.awt.kt#L531 + * + * This is very simplified logic, and it still can differ from the real cases. + * + * Though one main function - rendering into picture - was affecting performance. + * + * Benchmarks showed an improvement by 10%, but there was a regression by 10%. + * + * Beware that this logic can be changed in some new version of Skiko. + * + * If Skiko stops using `picture`, we need to remove it here too. + */ @OptIn(InternalComposeUiApi::class) fun ComposeScene.mimicSkikoRender(surface: Surface, time: Long, width: Int, height: Int) { val pictureCanvas = pictureRecorder.beginRecording(Rect(0f, 0f, width.toFloat(), height.toFloat()))