|
|
@ -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) |
|
|
|
|
|
|
|
surface.flushAndSubmit(false) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
val gpuTime = measureTime { |
|
|
|
scene.render(canvas, nextVSync.inWholeNanoseconds) |
|
|
|
graphicsContext?.awaitGPUCompletion() |
|
|
|
surface.flushAndSubmit(false) |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frames[it] = BenchmarkFrame(cpuTime, gpuTime) |
|
|
|
val cpuTime = frameStart.elapsedNow() |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
val actualNanosPerFrame = frameTime.inWholeNanoseconds |
|
|
|
graphicsContext?.awaitGPUCompletion() |
|
|
|
val nanosUntilDeadline = nanosPerFrame - actualNanosPerFrame |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Emulate waiting for next vsync |
|
|
|
val frameTime = frameStart.elapsedNow() |
|
|
|
if (nanosUntilDeadline > 0) { |
|
|
|
|
|
|
|
delay(nanosUntilDeadline / nanosPerMillisecond) |
|
|
|
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( |
|
|
|
BenchmarkResult( |
|
|
|