Browse Source

Benchmarks refactoring (#5115)

Fixes [the
issue](https://youtrack.jetbrains.com/issue/CMP-5894/Refactor-Compose-benchmarks-to-count-GC-between-frames):
count GC between frames.

## Testing

Run the benchmarks locally.
pull/5116/head
Nikita Lipsky 4 months ago committed by GitHub
parent
commit
d30da0ab11
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 13
      benchmarks/ios/jvm-vs-kotlin-native/src/commonMain/kotlin/MeasureComposable.kt
  2. 3
      benchmarks/kn-performance/src/commonMain/kotlin/Benchmarks.kt
  3. 77
      benchmarks/kn-performance/src/commonMain/kotlin/MeasureComposable.kt
  4. 3
      benchmarks/kn-performance/src/desktopMain/kotlin/runGC.jvm.kt
  5. 7
      benchmarks/kn-performance/src/macosMain/kotlin/runGC.native.kt

13
benchmarks/ios/jvm-vs-kotlin-native/src/commonMain/kotlin/MeasureComposable.kt

@ -1,6 +1,9 @@
import androidx.compose.runtime.Composable 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.Constraints
import androidx.compose.ui.unit.IntSize
import kotlin.time.Duration import kotlin.time.Duration
import kotlin.time.ExperimentalTime import kotlin.time.ExperimentalTime
@ -10,20 +13,20 @@ const val height = 480
const val nanosPerSecond = 1E9.toLong() const val nanosPerSecond = 1E9.toLong()
const val nanosPerFrame = (0.16 * nanosPerSecond).toLong() const val nanosPerFrame = (0.16 * nanosPerSecond).toLong()
@OptIn(ExperimentalTime::class) @OptIn(ExperimentalTime::class, InternalComposeUiApi::class)
fun measureComposable( fun measureComposable(
frameCount: Int = 1000, frameCount: Int = 1000,
content: @Composable () -> Unit content: @Composable () -> Unit
): Duration { ): Duration {
val scene = ComposeScene() val scene = MultiLayerComposeScene(size = IntSize(width, height))
try { try {
scene.setContent(content) scene.setContent(content)
scene.constraints = Constraints.fixed(width, height)
val surface = org.jetbrains.skia.Surface.makeNull(width, height) val surface = org.jetbrains.skia.Surface.makeNull(width, height)
val canvas = surface.canvas.asComposeCanvas()
return kotlin.time.measureTime { return kotlin.time.measureTime {
var nanoTime = 0L var nanoTime = 0L
repeat(frameCount) { repeat(frameCount) {
scene.render(surface.canvas, nanoTime) scene.render(canvas, nanoTime)
nanoTime += nanosPerFrame nanoTime += nanosPerFrame
} }
} }

3
benchmarks/kn-performance/src/commonMain/kotlin/Benchmarks.kt

@ -127,9 +127,10 @@ fun runBenchmark(
targetFps: Int, targetFps: Int,
frameCount: Int, frameCount: Int,
graphicsContext: GraphicsContext?, graphicsContext: GraphicsContext?,
warmupCount: Int = 100,
content: @Composable () -> Unit content: @Composable () -> Unit
): BenchmarkStats { ): BenchmarkStats {
val stats = measureComposable(frameCount, width, height, targetFps, graphicsContext, content).generateStats() val stats = measureComposable(warmupCount, frameCount, width, height, targetFps, graphicsContext, content).generateStats()
println(name) println(name)
stats.prettyPrint() stats.prettyPrint()

77
benchmarks/kn-performance/src/commonMain/kotlin/MeasureComposable.kt

@ -1,17 +1,17 @@
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.ComposeScene import androidx.compose.ui.InternalComposeUiApi
import androidx.compose.ui.unit.Constraints import androidx.compose.ui.graphics.asComposeCanvas
import org.jetbrains.skia.DirectContext import androidx.compose.ui.scene.MultiLayerComposeScene
import androidx.compose.ui.unit.IntSize
import org.jetbrains.skia.Surface import org.jetbrains.skia.Surface
import kotlin.time.Duration import kotlin.time.Duration
import kotlin.time.Duration.Companion.nanoseconds import kotlin.time.Duration.Companion.nanoseconds
import kotlin.time.ExperimentalTime import kotlin.time.ExperimentalTime
import kotlin.time.measureTime
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.TimeSource.Monotonic.markNow
const val nanosPerSecond = 1E9.toLong() const val nanosPerSecond = 1E9.toLong()
const val millisPerSecond = 1e3.toLong()
const val nanosPerMillisecond = nanosPerSecond / millisPerSecond
interface GraphicsContext { interface GraphicsContext {
fun surface(width: Int, height: Int): Surface fun surface(width: Int, height: Int): Surface
@ -19,51 +19,78 @@ interface GraphicsContext {
suspend fun awaitGPUCompletion() 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( fun measureComposable(
frameCount: Int = 500, warmupCount: Int,
frameCount: Int,
width: Int, width: Int,
height: Int, height: Int,
targetFps: Int, targetFps: Int,
graphicsContext: GraphicsContext?, graphicsContext: GraphicsContext?,
content: @Composable () -> Unit content: @Composable () -> Unit
): BenchmarkResult = runBlocking { ): BenchmarkResult = runBlocking {
val scene = ComposeScene() val scene = MultiLayerComposeScene(size = IntSize(width, height))
try { try {
val nanosPerFrame = (1.0 / targetFps.toDouble() * nanosPerSecond).toLong() val nanosPerFrame = (1.0 / targetFps.toDouble() * nanosPerSecond).toLong()
scene.setContent(content) scene.setContent(content)
scene.constraints = Constraints.fixed(width, height)
val surface = graphicsContext?.surface(width, height) ?: Surface.makeNull(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) { val frames = MutableList(frameCount) {
BenchmarkFrame(Duration.INFINITE, Duration.INFINITE) BenchmarkFrame(Duration.INFINITE, Duration.INFINITE)
} }
var nanoTime = 0L var nextVSync = Duration.ZERO
var missedFrames = 0;
runGC()
val start = markNow()
repeat(frameCount) { repeat(frameCount) {
val frameTime = measureTime { val frameStart = start + nextVSync
val cpuTime = measureTime {
scene.render(surface.canvas, nanoTime) scene.render(canvas, nextVSync.inWholeNanoseconds)
surface.flushAndSubmit(false) surface.flushAndSubmit(false)
}
val gpuTime = measureTime { val cpuTime = frameStart.elapsedNow()
graphicsContext?.awaitGPUCompletion() graphicsContext?.awaitGPUCompletion()
}
frames[it] = BenchmarkFrame(cpuTime, gpuTime) val frameTime = frameStart.elapsedNow()
}
val actualNanosPerFrame = frameTime.inWholeNanoseconds frames[it] = BenchmarkFrame(cpuTime, frameTime - cpuTime)
val nanosUntilDeadline = nanosPerFrame - actualNanosPerFrame
missedFrames += (frameTime.inWholeNanoseconds / nanosPerFrame).toInt()
nextVSync = ((it + 1 + missedFrames) * nanosPerFrame).nanoseconds
val timeUntilNextVSync = nextVSync - start.elapsedNow()
if (timeUntilNextVSync > Duration.ZERO) {
// Emulate waiting for next vsync // Emulate waiting for next vsync
if (nanosUntilDeadline > 0) { preciseDelay(timeUntilNextVSync)
delay(nanosUntilDeadline / nanosPerMillisecond)
} }
nanoTime += maxOf(actualNanosPerFrame, nanosPerFrame)
} }
BenchmarkResult( BenchmarkResult(

3
benchmarks/kn-performance/src/desktopMain/kotlin/runGC.jvm.kt

@ -0,0 +1,3 @@
actual fun runGC() {
System.gc()
}

7
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()
}
Loading…
Cancel
Save