diff --git a/benchmarks/ios/jvm-vs-kotlin-native/src/commonMain/kotlin/MeasureComposable.kt b/benchmarks/ios/jvm-vs-kotlin-native/src/commonMain/kotlin/MeasureComposable.kt index 6b11a8f842..98236a9de5 100644 --- a/benchmarks/ios/jvm-vs-kotlin-native/src/commonMain/kotlin/MeasureComposable.kt +++ b/benchmarks/ios/jvm-vs-kotlin-native/src/commonMain/kotlin/MeasureComposable.kt @@ -1,6 +1,9 @@ import androidx.compose.runtime.Composable -import androidx.compose.ui.ComposeScene +import androidx.compose.ui.InternalComposeUiApi +import androidx.compose.ui.graphics.asComposeCanvas +import androidx.compose.ui.scene.MultiLayerComposeScene import androidx.compose.ui.unit.Constraints +import androidx.compose.ui.unit.IntSize import kotlin.time.Duration import kotlin.time.ExperimentalTime @@ -10,20 +13,20 @@ const val height = 480 const val nanosPerSecond = 1E9.toLong() const val nanosPerFrame = (0.16 * nanosPerSecond).toLong() -@OptIn(ExperimentalTime::class) +@OptIn(ExperimentalTime::class, InternalComposeUiApi::class) fun measureComposable( frameCount: Int = 1000, content: @Composable () -> Unit ): Duration { - val scene = ComposeScene() + val scene = MultiLayerComposeScene(size = IntSize(width, height)) try { scene.setContent(content) - scene.constraints = Constraints.fixed(width, height) val surface = org.jetbrains.skia.Surface.makeNull(width, height) + val canvas = surface.canvas.asComposeCanvas() return kotlin.time.measureTime { var nanoTime = 0L repeat(frameCount) { - scene.render(surface.canvas, nanoTime) + scene.render(canvas, nanoTime) nanoTime += nanosPerFrame } } diff --git a/benchmarks/kn-performance/src/commonMain/kotlin/Benchmarks.kt b/benchmarks/kn-performance/src/commonMain/kotlin/Benchmarks.kt index cd2d006c3a..bf5f027254 100644 --- a/benchmarks/kn-performance/src/commonMain/kotlin/Benchmarks.kt +++ b/benchmarks/kn-performance/src/commonMain/kotlin/Benchmarks.kt @@ -127,9 +127,10 @@ fun runBenchmark( targetFps: Int, frameCount: Int, graphicsContext: GraphicsContext?, + warmupCount: Int = 100, content: @Composable () -> Unit ): BenchmarkStats { - val stats = measureComposable(frameCount, width, height, targetFps, graphicsContext, content).generateStats() + val stats = measureComposable(warmupCount, frameCount, width, height, targetFps, graphicsContext, content).generateStats() println(name) stats.prettyPrint() diff --git a/benchmarks/kn-performance/src/commonMain/kotlin/MeasureComposable.kt b/benchmarks/kn-performance/src/commonMain/kotlin/MeasureComposable.kt index f614668ea3..91d84814da 100644 --- a/benchmarks/kn-performance/src/commonMain/kotlin/MeasureComposable.kt +++ b/benchmarks/kn-performance/src/commonMain/kotlin/MeasureComposable.kt @@ -1,17 +1,17 @@ import androidx.compose.runtime.Composable -import androidx.compose.ui.ComposeScene -import androidx.compose.ui.unit.Constraints -import org.jetbrains.skia.DirectContext +import androidx.compose.ui.InternalComposeUiApi +import androidx.compose.ui.graphics.asComposeCanvas +import androidx.compose.ui.scene.MultiLayerComposeScene +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 kotlin.time.measureTime import kotlinx.coroutines.* +import kotlin.time.Duration.Companion.milliseconds +import kotlin.time.TimeSource.Monotonic.markNow const val nanosPerSecond = 1E9.toLong() -const val millisPerSecond = 1e3.toLong() -const val nanosPerMillisecond = nanosPerSecond / millisPerSecond interface GraphicsContext { fun surface(width: Int, height: Int): Surface @@ -19,51 +19,78 @@ interface GraphicsContext { suspend fun awaitGPUCompletion() } -@OptIn(ExperimentalTime::class) +expect fun runGC() + +suspend inline fun preciseDelay(duration: Duration) { + val liveDelay: Duration + if (duration.inWholeMilliseconds > 1) { + val delayMillis = duration.inWholeMilliseconds - 1 + delay(delayMillis) + liveDelay = duration - delayMillis.milliseconds + } else { + liveDelay = duration + } + val start = markNow() + while (start.elapsedNow() < liveDelay){} +} + +@OptIn(ExperimentalTime::class, InternalComposeUiApi::class) fun measureComposable( - frameCount: Int = 500, + warmupCount: Int, + frameCount: Int, width: Int, height: Int, targetFps: Int, graphicsContext: GraphicsContext?, content: @Composable () -> Unit ): BenchmarkResult = runBlocking { - val scene = ComposeScene() + val scene = MultiLayerComposeScene(size = IntSize(width, height)) try { val nanosPerFrame = (1.0 / targetFps.toDouble() * nanosPerSecond).toLong() scene.setContent(content) - scene.constraints = Constraints.fixed(width, height) val surface = graphicsContext?.surface(width, height) ?: Surface.makeNull(width, height) + val canvas = surface.canvas.asComposeCanvas() + + // warmup + repeat(warmupCount) { + scene.render(canvas, it * nanosPerFrame) + } val frames = MutableList(frameCount) { BenchmarkFrame(Duration.INFINITE, Duration.INFINITE) } - var nanoTime = 0L + var nextVSync = Duration.ZERO + var missedFrames = 0; + + runGC() + + val start = markNow() repeat(frameCount) { - val frameTime = measureTime { - val cpuTime = measureTime { - scene.render(surface.canvas, nanoTime) - surface.flushAndSubmit(false) - } + val frameStart = start + nextVSync - val gpuTime = measureTime { - graphicsContext?.awaitGPUCompletion() - } + scene.render(canvas, nextVSync.inWholeNanoseconds) + surface.flushAndSubmit(false) - frames[it] = BenchmarkFrame(cpuTime, gpuTime) - } + val cpuTime = frameStart.elapsedNow() - val actualNanosPerFrame = frameTime.inWholeNanoseconds - val nanosUntilDeadline = nanosPerFrame - actualNanosPerFrame + graphicsContext?.awaitGPUCompletion() - // Emulate waiting for next vsync - if (nanosUntilDeadline > 0) { - delay(nanosUntilDeadline / nanosPerMillisecond) - } + val frameTime = frameStart.elapsedNow() + + frames[it] = BenchmarkFrame(cpuTime, frameTime - cpuTime) + + missedFrames += (frameTime.inWholeNanoseconds / nanosPerFrame).toInt() - nanoTime += maxOf(actualNanosPerFrame, nanosPerFrame) + nextVSync = ((it + 1 + missedFrames) * nanosPerFrame).nanoseconds + + val timeUntilNextVSync = nextVSync - start.elapsedNow() + + if (timeUntilNextVSync > Duration.ZERO) { + // Emulate waiting for next vsync + preciseDelay(timeUntilNextVSync) + } } BenchmarkResult( diff --git a/benchmarks/kn-performance/src/desktopMain/kotlin/runGC.jvm.kt b/benchmarks/kn-performance/src/desktopMain/kotlin/runGC.jvm.kt new file mode 100644 index 0000000000..3095a116b1 --- /dev/null +++ b/benchmarks/kn-performance/src/desktopMain/kotlin/runGC.jvm.kt @@ -0,0 +1,3 @@ +actual fun runGC() { + System.gc() +} \ No newline at end of file diff --git a/benchmarks/kn-performance/src/macosMain/kotlin/runGC.native.kt b/benchmarks/kn-performance/src/macosMain/kotlin/runGC.native.kt new file mode 100644 index 0000000000..7b499db732 --- /dev/null +++ b/benchmarks/kn-performance/src/macosMain/kotlin/runGC.native.kt @@ -0,0 +1,7 @@ +import kotlin.native.runtime.GC +import kotlin.native.runtime.NativeRuntimeApi + +@OptIn(NativeRuntimeApi::class) +actual fun runGC() { + GC.collect() +} \ No newline at end of file