diff --git a/benchmarks/multiplatform/README.md b/benchmarks/multiplatform/README.md index 1a58d47f7c1..c4a7a422446 100644 --- a/benchmarks/multiplatform/README.md +++ b/benchmarks/multiplatform/README.md @@ -14,9 +14,17 @@ The benchmarks can be run in different modes, which determine how performance is - **`REAL`**: Runs the benchmark in a real-world scenario with actual VSync. This mode provides the most accurate results for user-perceived performance (FPS, actual missed frames) but may not catch performance regressions if a frame fits to budget. Also requires a device with a real display (may have problems with headless devices). +- **`STARTUP`**: Measures application startup performance. This mode captures detailed timing metrics from process start to the first frame and beyond, including: + - Time from process start to the application entry point (`timeToMain`) - platform-dependent, available on JVM/Desktop + - Time from entry point to the first frame (`timeFromMainToFirstFrame`) + - Duration of the first frame itself (`timeOfFirstFrame`) + - Time from the first frame to a configurable Nth frame (`timeToNthFrame`, default N=30) + - The N longest frames during startup (`longestFrames`, default N=3) To enable specific modes, use the `modes` argument: -`modes=SIMPLE,VSYNC_EMULATION,REAL` +`modes=SIMPLE,VSYNC_EMULATION,REAL,STARTUP` + +Multiple modes can be combined. For example, `modes=STARTUP,REAL` will first measure startup metrics and then run real-time performance measurements, producing a unified report. ## Configuration Arguments @@ -24,12 +32,14 @@ You can configure benchmark runs using arguments passed to the Gradle task (via | Argument | Description | Example | |----------|------------------------------------------------------------|---------| -| `modes` | Comma-separated list of execution modes (`SIMPLE`, `VSYNC_EMULATION`, `REAL`). | `modes=REAL` | +| `modes` | Comma-separated list of execution modes (`SIMPLE`, `VSYNC_EMULATION`, `REAL`, `STARTUP`). | `modes=REAL,STARTUP` | | `benchmarks` | Comma-separated list of benchmarks to run. Can optionally specify problem size in parentheses. | `benchmarks=LazyGrid(100),AnimatedVisibility` | | `disabledBenchmarks` | Comma-separated list of benchmarks to skip. | `disabledBenchmarks=HeavyShader` | | `warmupCount` | Number of warmup frames before starting measurements. | `warmupCount=50` | | `frameCount` | Number of frames to measure for each benchmark. | `frameCount=500` | | `emptyScreenDelay` | Delay in milliseconds between warmup and measurement (real mode only).| `emptyScreenDelay=1000` | +| `startupFrameCount` | Number of frames to measure after the first frame in startup mode (default: 30). | `startupFrameCount=50` | +| `startupLongestFramesCount` | Number of longest frames to report in startup mode (default: 3). | `startupLongestFramesCount=5` | | `parallel` | (iOS only) Enable parallel rendering. | `parallel=true` | | `saveStatsToCSV` | Save results to CSV files. | `saveStatsToCSV=true` | | `saveStatsToJSON` | Save results to JSON files. | `saveStatsToJSON=true` | diff --git a/benchmarks/multiplatform/benchmarks/src/appleMain/kotlin/getProcessStartTime.apple.kt b/benchmarks/multiplatform/benchmarks/src/appleMain/kotlin/getProcessStartTime.apple.kt new file mode 100644 index 00000000000..d02de1d7c33 --- /dev/null +++ b/benchmarks/multiplatform/benchmarks/src/appleMain/kotlin/getProcessStartTime.apple.kt @@ -0,0 +1,38 @@ +import kotlin.time.TimeSource +import kotlinx.cinterop.* +import platform.posix.* +import platform.darwin.* +import kotlin.time.Duration.Companion.nanoseconds + +@OptIn(ExperimentalForeignApi::class) +actual fun getProcessStartTime(): TimeSource.Monotonic.ValueTimeMark? { + memScoped { + val mib = allocArray(4) + mib[0] = CTL_KERN + mib[1] = KERN_PROC + mib[2] = KERN_PROC_PID + mib[3] = getpid() + + val size = alloc() + size.value = sizeOf().convert() + val info = alloc() + + if (sysctl(mib, 4u, info.ptr, size.ptr, null, 0uL) != 0) { + return null + } + + // extern_proc.p_un (a union) is the first field of extern_proc, which is the first field of + // kinfo_proc. The union member __p_starttime (timeval) starts at offset 0 within the union. + // Therefore timeval is at offset 0 of kinfo_proc, and we can read it directly via reinterpret. + // Kotlin/Native doesn't expose extern_proc's inner fields, so we use this layout-based access. + val startTime = info.ptr.reinterpret().pointed + + val now = alloc() + gettimeofday(now.ptr, null) + + val elapsedNs = (now.tv_sec - startTime.tv_sec) * 1_000_000_000L + + (now.tv_usec - startTime.tv_usec) * 1_000L + + return TimeSource.Monotonic.markNow() - elapsedNs.nanoseconds + } +} diff --git a/benchmarks/multiplatform/benchmarks/src/commonMain/kotlin/Benchmarks.kt b/benchmarks/multiplatform/benchmarks/src/commonMain/kotlin/Benchmarks.kt index f36dd805f7e..625e68b0ed6 100644 --- a/benchmarks/multiplatform/benchmarks/src/commonMain/kotlin/Benchmarks.kt +++ b/benchmarks/multiplatform/benchmarks/src/commonMain/kotlin/Benchmarks.kt @@ -130,6 +130,16 @@ data class MissedFrames( private val json = Json { prettyPrint = true } +@Serializable +data class StartupTimeInfo( + val timeToMain: Duration? = null, + val timeFromMainToFirstFrame: Duration? = null, + val timeOfFirstFrame: Duration, + val timeToNthFrame: Duration, + val nthFrameCount: Int, + val longestFrames: List +) + @Serializable data class BenchmarkStats( val name: String, @@ -141,6 +151,7 @@ data class BenchmarkStats( val percentileGPUAverage: List, val noBufferingMissedFrames: MissedFrames, val doubleBufferingMissedFrames: MissedFrames, + val startupTimeInfo: StartupTimeInfo? = null, ) { fun prettyPrint() { println("# $name") @@ -150,6 +161,24 @@ data class BenchmarkStats( } conditions.prettyPrint() println() + startupTimeInfo?.let { + println("StartupTimeInfo:") + var timeToMain: Duration = Duration.ZERO + it.timeToMain?.let { t -> + timeToMain = t + println(" - Time to main: ${t.inWholeMilliseconds} ms") + } + it.timeFromMainToFirstFrame?.let { t -> + println(" - Time from main to first frame: ${t.inWholeMilliseconds} ms") + } + println(" - Time of first frame: ${it.timeOfFirstFrame.inWholeMilliseconds} ms") + it.timeFromMainToFirstFrame?.let { t -> + println(" - Total startup time: ${timeToMain.inWholeMilliseconds + t.inWholeMilliseconds + it.timeOfFirstFrame.inWholeMilliseconds} ms") + } + println(" - Time from first to ${it.nthFrameCount}th frame: ${it.timeToNthFrame.inWholeMilliseconds} ms") + println(" - ${it.longestFrames.size} longest frames during startup: ${it.longestFrames.joinToString { f -> "${f.inWholeMilliseconds} ms" }}") + println() + } if (Config.isModeEnabled(Mode.SIMPLE)) { val frameInfo = requireNotNull(averageFrameInfo) { "frameInfo shouldn't be null with Mode.SIMPLE" } frameInfo.prettyPrint() @@ -177,6 +206,19 @@ data class BenchmarkStats( map.put("Version", versionInfo) } conditions.putFormattedValuesTo(map) + startupTimeInfo?.let { + it.timeToMain?.let { t -> + map.put("Startup: Time to main (ms)", t.formatAsMilliseconds()) + } + it.timeFromMainToFirstFrame?.let { t -> + map.put("Startup: Time from main to first frame (ms)", t.formatAsMilliseconds()) + } + map.put("Startup: Time of first frame (ms)", it.timeOfFirstFrame.formatAsMilliseconds()) + map.put("Startup: Time from first to ${it.nthFrameCount}th frame (ms)", it.timeToNthFrame.formatAsMilliseconds()) + it.longestFrames.forEachIndexed { index, duration -> + map.put("Startup: Longest frame ${index + 1} (ms)", duration.formatAsMilliseconds()) + } + } if (Config.isModeEnabled(Mode.SIMPLE)) { val frameInfo = requireNotNull(averageFrameInfo) { "frameInfo shouldn't be null with Mode.SIMPLE" } frameInfo.putFormattedValuesTo(map) @@ -220,6 +262,7 @@ class BenchmarkResult( private val averageFrameInfo: FrameInfo, private val averageFPSInfo: FPSInfo, private val frames: List, + private val startupTimeInfo: StartupTimeInfo? = null, ) { private fun percentileAverageFrameTime(percentile: Double, kind: BenchmarkFrameTimeKind): Duration { require(percentile in 0.0..1.0) @@ -259,7 +302,8 @@ class BenchmarkResult( BenchmarkPercentileAverage(percentile, average) }, MissedFrames(noBufferingMissedFramesCount, noBufferingMissedFramesCount / frames.size.toDouble()), - MissedFrames(doubleBufferingMissedFrames, doubleBufferingMissedFrames / frames.size.toDouble()) + MissedFrames(doubleBufferingMissedFrames, doubleBufferingMissedFrames / frames.size.toDouble()), + startupTimeInfo = startupTimeInfo, ) } @@ -326,15 +370,24 @@ suspend fun runBenchmark( graphicsContext = graphicsContext, content = benchmark.content ).generateStats() + reportBenchmarkStats(stats) + } +} + +private fun reportBenchmarkStats(stats: BenchmarkStats, results: MutableList? = null) { + if (results != null) { + results.add(stats) + } + if (results == null || !Config.reportAtTheEnd) { stats.prettyPrint() - if (Config.saveStatsToJSON && isIosTarget) { - println("JSON_START") - println(stats.toJsonString()) - println("JSON_END") - } - if (Config.saveStats()) { - saveBenchmarkStats(name = benchmark.name, stats = stats) - } + } + if (Config.saveStatsToJSON && isIosTarget) { + println("JSON_START") + println(stats.toJsonString()) + println("JSON_END") + } + if (Config.saveStats()) { + saveBenchmarkStats(name = stats.name, stats = stats) } } @@ -360,6 +413,17 @@ suspend fun runBenchmarks( } } +expect fun getProcessStartTime(): TimeSource.Monotonic.ValueTimeMark? + +expect val mainTime: TimeSource.Monotonic.ValueTimeMark + +private enum class BenchmarkPhase { + STARTUP, + EMPTY_SCREEN, + WARMUP, + MEASUREMENT +} + @Composable fun BenchmarkRunner( benchmarks: List, @@ -367,8 +431,18 @@ fun BenchmarkRunner( onExit: () -> Unit ) { var currentBenchmarkIndex by remember { mutableStateOf(0) } - var isWarmup by remember { mutableStateOf(Config.warmupCount > 0) } - var isEmptyScreen by remember { mutableStateOf(false) } + var firstBenchmarkName by remember { mutableStateOf(null) } + var phaseBeforeEmptyScreen by remember { mutableStateOf(null) } + var phase by remember { + mutableStateOf( + when { + Config.isModeEnabled(Mode.STARTUP) -> BenchmarkPhase.STARTUP + Config.warmupCount > 0 -> BenchmarkPhase.WARMUP + else -> BenchmarkPhase.MEASUREMENT + } + ) + } + var startupTimeInfo by remember { mutableStateOf(null) } val results = remember { mutableListOf() } val nanosPerFrame = 1.seconds.inWholeNanoseconds / deviceFrameRate @@ -380,67 +454,126 @@ fun BenchmarkRunner( return } val benchmark = benchmarks[currentBenchmarkIndex] + if (firstBenchmarkName == null && Config.isBenchmarkEnabled(benchmark.name)) { + firstBenchmarkName = benchmark.name + } + if (Config.isBenchmarkEnabled(benchmark.name)) { Box(modifier = Modifier.fillMaxSize()) { - if (!isEmptyScreen) { + if (phase != BenchmarkPhase.EMPTY_SCREEN) { benchmark.content() } - LaunchedEffect(benchmark.name, isWarmup, isEmptyScreen) { - if (isWarmup) { - repeat(Config.warmupCount) { + LaunchedEffect(benchmark.name, phase) { + when (phase) { + BenchmarkPhase.STARTUP -> { + val processStart = getProcessStartTime() + + // Time of the first frame + val firstFrameMeasureStart = TimeSource.Monotonic.markNow() withFrameNanos { } + val firstFrameMark = TimeSource.Monotonic.markNow() + val timeOfFirstFrame = firstFrameMark - firstFrameMeasureStart + + // Nth frame + val startupFramesCount = Config.startupFrameCount + val startupFrames = MutableList(startupFramesCount) { Duration.ZERO } + repeat(startupFramesCount) { i -> + val frameStart = TimeSource.Monotonic.markNow() + withFrameNanos { } + startupFrames[i] = frameStart.elapsedNow() + } + val nthFrameMark = TimeSource.Monotonic.markNow() + val timeToNthFrame = nthFrameMark - firstFrameMark + + val capturedStartupTimeInfo = StartupTimeInfo( + timeToMain = processStart?.let { mainTime - it }, + timeFromMainToFirstFrame = if (benchmark.name == firstBenchmarkName) firstFrameMark - mainTime else null, + timeOfFirstFrame = timeOfFirstFrame, + timeToNthFrame = timeToNthFrame, + nthFrameCount = startupFramesCount, + longestFrames = startupFrames.sortedDescending().take(Config.startupLongestFramesCount) + ) + startupTimeInfo = capturedStartupTimeInfo + + if (!Config.isModeEnabled(Mode.REAL)) { + val stats = BenchmarkResult( + name = benchmark.name, + frameBudget = nanosPerFrame.nanoseconds, + conditions = BenchmarkConditions(frameCount = startupFramesCount, warmupCount = 0), + averageFrameInfo = FrameInfo(timeOfFirstFrame, Duration.ZERO), + averageFPSInfo = FPSInfo(0.0), + frames = startupFrames.map { BenchmarkFrame(it, Duration.ZERO) }, + startupTimeInfo = capturedStartupTimeInfo + ).generateStats() + + reportBenchmarkStats(stats, results) + currentBenchmarkIndex++ + startupTimeInfo = null + } + phaseBeforeEmptyScreen = BenchmarkPhase.STARTUP + phase = BenchmarkPhase.EMPTY_SCREEN } - isWarmup = false - isEmptyScreen = true - } else if (isEmptyScreen) { - delay(Config.emptyScreenDelay.milliseconds) - withFrameNanos { } - isEmptyScreen = false - } else { - val frames = MutableList(benchmark.frameCount) { - BenchmarkFrame(Duration.ZERO, Duration.ZERO) + BenchmarkPhase.WARMUP -> { + repeat(Config.warmupCount) { + withFrameNanos { } + } + phaseBeforeEmptyScreen = BenchmarkPhase.WARMUP + phase = BenchmarkPhase.EMPTY_SCREEN } - // skip first frame waiting - withFrameNanos { } - val start = TimeSource.Monotonic.markNow() - repeat(benchmark.frameCount) { - val frameStart = TimeSource.Monotonic.markNow() + BenchmarkPhase.EMPTY_SCREEN -> { + delay(Config.emptyScreenDelay.milliseconds) withFrameNanos { } - val rawFrameTime = frameStart.elapsedNow() - // rawFrameTime isn't reliable for missed-frame checks: small timing inaccuracies can push - // it just over the frame budget even when the frame would effectively fit. - // Instead, estimate how many vsync intervals the frame took by rounding the measured time to - // the nearest multiple of nanosPerFrame. - // Note: this is only an estimate — we can’t determine here whether a frame was truly missed. - val normalizedFrameTime = ((rawFrameTime.inWholeNanoseconds + nanosPerFrame/2) / nanosPerFrame) * nanosPerFrame - frames[it] = BenchmarkFrame(normalizedFrameTime.nanoseconds, Duration.ZERO) - } - val duration = start.elapsedNow() - val stats = BenchmarkResult( - name = benchmark.name, - frameBudget = nanosPerFrame.nanoseconds, - conditions = BenchmarkConditions(benchmark.frameCount, warmupCount = Config.warmupCount), - averageFrameInfo = FrameInfo(duration / benchmark.frameCount, Duration.ZERO), - averageFPSInfo = FPSInfo(benchmark.frameCount.toDouble() / duration.toDouble(DurationUnit.SECONDS)), - frames = frames - ).generateStats() - if (!Config.reportAtTheEnd) { - stats.prettyPrint() - } else { - results.add(stats) + phase = when (phaseBeforeEmptyScreen) { + BenchmarkPhase.STARTUP -> + if (Config.isModeEnabled(Mode.REAL)) { + if (Config.warmupCount > 0) BenchmarkPhase.WARMUP + else BenchmarkPhase.MEASUREMENT + } else BenchmarkPhase.STARTUP + BenchmarkPhase.WARMUP -> + BenchmarkPhase.MEASUREMENT + BenchmarkPhase.MEASUREMENT -> + if (Config.isModeEnabled(Mode.STARTUP)) BenchmarkPhase.STARTUP + else if (Config.warmupCount > 0) BenchmarkPhase.WARMUP + else BenchmarkPhase.MEASUREMENT + else -> throw IllegalStateException("Unexpected phase: $phaseBeforeEmptyScreen") + } } - if (Config.saveStatsToJSON && isIosTarget) { - println("JSON_START") - println(stats.toJsonString()) - println("JSON_END") - } - if (Config.saveStats()) { - saveBenchmarkStats(name = benchmark.name, stats = stats) + BenchmarkPhase.MEASUREMENT -> { + val frames = MutableList(benchmark.frameCount) { + BenchmarkFrame(Duration.ZERO, Duration.ZERO) + } + // skip first frame waiting + withFrameNanos { } + val start = TimeSource.Monotonic.markNow() + repeat(benchmark.frameCount) { + val frameStart = TimeSource.Monotonic.markNow() + withFrameNanos { } + val rawFrameTime = frameStart.elapsedNow() + // rawFrameTime isn't reliable for missed-frame checks: small timing inaccuracies can push + // it just over the frame budget even when the frame would effectively fit. + // Instead, estimate how many vsync intervals the frame took by rounding the measured time to + // the nearest multiple of nanosPerFrame. + // Note: this is only an estimate — we can't determine here whether a frame was truly missed. + val normalizedFrameTime = ((rawFrameTime.inWholeNanoseconds + nanosPerFrame/2) / nanosPerFrame) * nanosPerFrame + frames[it] = BenchmarkFrame(normalizedFrameTime.nanoseconds, Duration.ZERO) + } + val duration = start.elapsedNow() + val stats = BenchmarkResult( + name = benchmark.name, + frameBudget = nanosPerFrame.nanoseconds, + conditions = BenchmarkConditions(benchmark.frameCount, warmupCount = Config.warmupCount), + averageFrameInfo = FrameInfo(duration / benchmark.frameCount, Duration.ZERO), + averageFPSInfo = FPSInfo(benchmark.frameCount.toDouble() / duration.toDouble(DurationUnit.SECONDS)), + frames = frames, + startupTimeInfo = startupTimeInfo + ).generateStats() + reportBenchmarkStats(stats, results) + currentBenchmarkIndex++ + startupTimeInfo = null + phaseBeforeEmptyScreen = BenchmarkPhase.MEASUREMENT + phase = BenchmarkPhase.EMPTY_SCREEN } - currentBenchmarkIndex++ - isWarmup = Config.warmupCount > 0 - isEmptyScreen = true } } } diff --git a/benchmarks/multiplatform/benchmarks/src/commonMain/kotlin/Config.kt b/benchmarks/multiplatform/benchmarks/src/commonMain/kotlin/Config.kt index 0c37350b8ba..ec5279e61b9 100644 --- a/benchmarks/multiplatform/benchmarks/src/commonMain/kotlin/Config.kt +++ b/benchmarks/multiplatform/benchmarks/src/commonMain/kotlin/Config.kt @@ -1,7 +1,8 @@ enum class Mode { SIMPLE, VSYNC_EMULATION, - REAL + REAL, + STARTUP } object Args { @@ -45,6 +46,8 @@ object Args { var emptyScreenDelay: Long? = null var reportAtTheEnd: Boolean = false var listBenchmarks: Boolean = false + var startupFrameCount: Int? = null + var startupLongestFramesCount: Int? = null for (arg in args) { if (arg.startsWith("modes=", ignoreCase = true)) { @@ -73,11 +76,18 @@ object Args { reportAtTheEnd = arg.substringAfter("=").toBoolean() } else if (arg.startsWith("listBenchmarks=", ignoreCase = true)) { listBenchmarks = arg.substringAfter("=").toBoolean() + } else if (arg.startsWith("startupFrameCount=", ignoreCase = true)) { + startupFrameCount = arg.substringAfter("=").toInt() + } else if (arg.startsWith("startupLongestFramesCount=", ignoreCase = true)) { + startupLongestFramesCount = arg.substringAfter("=").toInt() } else { println("WARNING: unknown argument $arg") } } + if (modes.isEmpty()) { + modes.addAll(listOf(Mode.SIMPLE, Mode.VSYNC_EMULATION)) + } val defaultWarmupCount = if (modes.contains(Mode.REAL)) 0 else 100 return Config( @@ -93,7 +103,9 @@ object Args { frameCount = frameCount ?: 1000, emptyScreenDelay = emptyScreenDelay ?: 2000L, reportAtTheEnd = reportAtTheEnd, - listBenchmarks = listBenchmarks + listBenchmarks = listBenchmarks, + startupFrameCount = startupFrameCount ?: 30, + startupLongestFramesCount = startupLongestFramesCount ?: 3 ) } } @@ -129,13 +141,15 @@ data class Config( val frameCount: Int = 1000, val emptyScreenDelay: Long = 2000L, val reportAtTheEnd: Boolean = false, - val listBenchmarks: Boolean = false + val listBenchmarks: Boolean = false, + val startupFrameCount: Int = 30, + val startupLongestFramesCount: Int = 3 ) { /** * Checks if a specific mode is enabled based on the configuration. * A mode is considered enabled if no modes were specified (default) except `real` mode or if it's explicitly listed. */ - fun isModeEnabled(mode: Mode): Boolean = (modes.isEmpty() && mode != Mode.REAL) || modes.contains(mode) + fun isModeEnabled(mode: Mode): Boolean = modes.contains(mode) /** * Checks if a specific benchmark is enabled @@ -194,6 +208,12 @@ data class Config( val listBenchmarks: Boolean get() = global.listBenchmarks + val startupFrameCount: Int + get() = global.startupFrameCount + + val startupLongestFramesCount: Int + get() = global.startupLongestFramesCount + fun setGlobal(global: Config) { this.global = global } diff --git a/benchmarks/multiplatform/benchmarks/src/desktopMain/kotlin/main.desktop.kt b/benchmarks/multiplatform/benchmarks/src/desktopMain/kotlin/main.desktop.kt index b7744b7ff8c..3e3d0564491 100644 --- a/benchmarks/multiplatform/benchmarks/src/desktopMain/kotlin/main.desktop.kt +++ b/benchmarks/multiplatform/benchmarks/src/desktopMain/kotlin/main.desktop.kt @@ -10,6 +10,17 @@ import androidx.compose.ui.window.rememberWindowState import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.runBlocking import java.awt.GraphicsEnvironment +import java.lang.management.ManagementFactory +import kotlin.time.Duration.Companion.milliseconds +import kotlin.time.TimeSource + +actual val mainTime: TimeSource.Monotonic.ValueTimeMark = TimeSource.Monotonic.markNow() + +actual fun getProcessStartTime(): TimeSource.Monotonic.ValueTimeMark? { + val startTime = ManagementFactory.getRuntimeMXBean().startTime + val uptime = System.currentTimeMillis() - startTime + return TimeSource.Monotonic.markNow() - uptime.milliseconds +} fun main(args: Array) { Config.setGlobalFromArgs(args) @@ -17,7 +28,7 @@ fun main(args: Array) { if (Config.runServer) { // Start the benchmark server to receive results from browsers BenchmarksSaveServer.start() - } else if (Config.isModeEnabled(Mode.REAL)) { + } else if (Config.isModeEnabled(Mode.REAL) || Config.isModeEnabled(Mode.STARTUP)) { val device = GraphicsEnvironment.getLocalGraphicsEnvironment().defaultScreenDevice val frameRate = device.displayMode.refreshRate.takeIf { it > 0 } ?: 120 diff --git a/benchmarks/multiplatform/benchmarks/src/iosMain/kotlin/main.ios.kt b/benchmarks/multiplatform/benchmarks/src/iosMain/kotlin/main.ios.kt index e19a0250e6a..10d65c956b1 100644 --- a/benchmarks/multiplatform/benchmarks/src/iosMain/kotlin/main.ios.kt +++ b/benchmarks/multiplatform/benchmarks/src/iosMain/kotlin/main.ios.kt @@ -11,12 +11,15 @@ import platform.UIKit.UIApplication import platform.UIKit.UIScreen import platform.UIKit.UIViewController import kotlin.system.exitProcess +import kotlin.time.TimeSource + +actual val mainTime: TimeSource.Monotonic.ValueTimeMark = TimeSource.Monotonic.markNow() fun setGlobalFromArgs(args : List) { Config.setGlobalFromArgs(args.toTypedArray()) } -fun runReal() = Config.isModeEnabled(Mode.REAL) +fun runReal() = Config.isModeEnabled(Mode.REAL) || Config.isModeEnabled(Mode.STARTUP) fun runBenchmarks() { UIApplication.sharedApplication.setIdleTimerDisabled(true) diff --git a/benchmarks/multiplatform/benchmarks/src/macosMain/kotlin/main.macos.kt b/benchmarks/multiplatform/benchmarks/src/macosMain/kotlin/main.macos.kt index 9a1c8f0d03a..52ce3720f34 100644 --- a/benchmarks/multiplatform/benchmarks/src/macosMain/kotlin/main.macos.kt +++ b/benchmarks/multiplatform/benchmarks/src/macosMain/kotlin/main.macos.kt @@ -11,12 +11,15 @@ import platform.AppKit.NSApplication import platform.AppKit.NSScreen import platform.AppKit.maximumFramesPerSecond import kotlin.system.exitProcess +import kotlin.time.TimeSource + +actual val mainTime: TimeSource.Monotonic.ValueTimeMark = TimeSource.Monotonic.markNow() actual val isIosTarget: Boolean = true fun main(args : Array) { Config.setGlobalFromArgs(args) - if (Config.isModeEnabled(Mode.REAL)) { + if (Config.isModeEnabled(Mode.REAL) || Config.isModeEnabled(Mode.STARTUP)) { NSApplication.sharedApplication() val frameRate = (NSScreen.mainScreen?.maximumFramesPerSecond?.toInt()) ?: 120 Window( diff --git a/benchmarks/multiplatform/benchmarks/src/webMain/kotlin/Main.web.kt b/benchmarks/multiplatform/benchmarks/src/webMain/kotlin/Main.web.kt index 8ecb265970d..add7b880a25 100644 --- a/benchmarks/multiplatform/benchmarks/src/webMain/kotlin/Main.web.kt +++ b/benchmarks/multiplatform/benchmarks/src/webMain/kotlin/Main.web.kt @@ -6,7 +6,11 @@ import kotlinx.coroutines.* import org.w3c.dom.url.URLSearchParams import kotlin.js.ExperimentalWasmJsInterop import kotlin.js.toJsString +import kotlin.time.TimeSource +actual fun getProcessStartTime(): TimeSource.Monotonic.ValueTimeMark? = null + +actual val mainTime: TimeSource.Monotonic.ValueTimeMark = TimeSource.Monotonic.markNow() @OptIn(ExperimentalComposeUiApi::class, ExperimentalWasmJsInterop::class) fun mainBrowser() = MainScope().launch { @@ -19,10 +23,17 @@ fun mainBrowser() = MainScope().launch { Config.setGlobalFromArgs(args) val composeRoot = document.getElementById("root") ?: error("No root element found") - if (Config.isModeEnabled(Mode.REAL)) { + if (Config.isModeEnabled(Mode.REAL) || Config.isModeEnabled(Mode.STARTUP)) { val frameRate = 120 // can we get this from device? ComposeViewport("root") { - BenchmarkRunner(getBenchmarks(), frameRate, onExit = { composeRoot.remove() }) + BenchmarkRunner(getBenchmarks(), frameRate, onExit = { + composeRoot.remove() + GlobalScope.launch { + if (BenchmarksSaveServerClient.isServerAlive()) { + BenchmarksSaveServerClient.stopServer() + } + } + }) } } else { composeRoot.remove() diff --git a/benchmarks/multiplatform/run_benchmarks.main.kts b/benchmarks/multiplatform/run_benchmarks.main.kts index c8c9b8dfe08..1feabcbc5c0 100755 --- a/benchmarks/multiplatform/run_benchmarks.main.kts +++ b/benchmarks/multiplatform/run_benchmarks.main.kts @@ -33,8 +33,9 @@ fun main(args: Array) { val versionArg = argMap.remove("version") val version = versionArg ?: getCurrentComposeVersion() val benchmarkName = argMap["benchmarks"] - val separateProcess = argMap.remove("separateProcess")?.toBoolean() ?: false - + val startup = argMap["modes"]?.contains("startup", ignoreCase = true) ?: false + val separateProcess = argMap.remove("separateProcess")?.toBoolean() ?: startup + val runServer = platform == "web" val isWeb = platform == "web" val isIos = platform == "ios" @@ -43,6 +44,7 @@ fun main(args: Array) { println("Compose version: $version") println("Number of runs: $runs") println("Separate process: $separateProcess") + println("Startup mode: $startup") println("Run server: $runServer") benchmarkName?.let { println("Filtering by benchmark: $it") }