From 5c38e8406c0fb1948b00ad6bada39e078ad52d66 Mon Sep 17 00:00:00 2001 From: Konstantin Date: Mon, 9 Nov 2026 15:53:14 +0100 Subject: [PATCH 01/61] Import the benchmark project --- compose/mpp/benchmarks/build.gradle.kts | 207 ++++++ compose/mpp/benchmarks/gradle.properties | 1 + .../gradle/wasm-d8-runtime.gradle.kts | 72 ++ .../iosApp/Configuration/Config.xcconfig | 6 + .../iosApp/iosApp.xcodeproj/project.pbxproj | 382 ++++++++++ .../AccentColor.colorset/Contents.json | 11 + .../AppIcon.appiconset/Contents.json | 98 +++ .../iosApp/Assets.xcassets/Contents.json | 6 + .../iosApp/iosApp/ContentView.swift | 25 + .../mpp/benchmarks/iosApp/iosApp/Info.plist | 50 ++ .../Preview Assets.xcassets/Contents.json | 6 + .../mpp/benchmarks/iosApp/iosApp/iOSApp.swift | 37 + .../appleMain/kotlin/BenchmarksSave.apple.kt | 6 + .../appleMain/kotlin/GraphicsContext.apple.kt | 65 ++ .../kotlin/getProcessStartTime.apple.kt | 38 + .../src/appleMain/kotlin/runGC.native.kt | 7 + .../drawable/compose-multiplatform.png | Bin 0 -> 34968 bytes .../drawable/example1_cat.jpg | Bin 0 -> 37524 bytes .../drawable/example1_ic_call_answer.xml | 36 + .../drawable/example1_sailing.svg | 1 + .../example1_compose-community-primary.png | Bin 0 -> 17662 bytes .../font/jetbrainsmono_italic.ttf | Bin 0 -> 141572 bytes .../font/jetbrainsmono_regular.ttf | Bin 0 -> 136708 bytes .../kotlin/BenchmarkComposeScene.kt | 36 + .../src/commonMain/kotlin/Benchmarks.kt | 597 ++++++++++++++++ .../src/commonMain/kotlin/BenchmarksSave.kt | 92 +++ .../src/commonMain/kotlin/Config.kt | 236 +++++++ .../commonMain/kotlin/MeasureComposable.kt | 174 +++++ .../animation/AnimatedVisibility.kt | 47 ++ .../benchmarks/canvasdrawing/CanvasDrawing.kt | 159 +++++ .../complexlazylist/components/MainUI.kt | 110 +++ .../components/MultiLevelCell.kt | 351 ++++++++++ .../components/refresh/LoadingIndicator.kt | 159 +++++ .../components/refresh/SwipeProgress.kt | 17 + .../components/refresh/SwipeRefreshLayout.kt | 121 ++++ .../SwipeRefreshNestedScrollConnection.kt | 134 ++++ .../components/refresh/SwipeRefreshState.kt | 79 +++ .../complexlazylist/models/FollowItem.kt | 16 + .../complexlazylist/models/HotItem.kt | 16 + .../complexlazylist/models/IBaseViewModel.kt | 10 + .../models/ICompositionModel.kt | 21 + .../models/ViewModelFactory.kt | 56 ++ .../benchmarks/heavyshader/HeavyShader.kt | 135 ++++ .../kotlin/benchmarks/lazygrid/LazyGrid.kt | 97 +++ .../multipleComponents/Clickable.kt | 362 ++++++++++ .../benchmarks/multipleComponents/MenuIcon.kt | 49 ++ .../multipleComponents/MultipleComponents.kt | 662 ++++++++++++++++++ .../benchmarks/textlayout/TextLayout.kt | 117 ++++ .../benchmarks/visualeffects/HappyNY.kt | 321 +++++++++ .../kotlin/BenchmarksSave.desktop.kt | 8 + .../kotlin/BenchmarksSaveServer.kt | 92 +++ .../kotlin/GraphicsContext.desktop.kt | 43 ++ .../src/desktopMain/kotlin/main.desktop.kt | 51 ++ .../src/desktopMain/kotlin/runGC.jvm.kt | 3 + .../src/iosMain/kotlin/Platform.ios.kt | 12 + .../benchmarks/src/iosMain/kotlin/main.ios.kt | 35 + .../benchmarks/src/jsMain/kotlin/Main.js.kt | 4 + .../src/macosMain/kotlin/main.macos.kt | 39 ++ .../src/wasmJsMain/kotlin/Main.wasmJs.kt | 64 ++ .../src/webMain/kotlin/BenchmarksSave.web.kt | 116 +++ .../benchmarks/src/webMain/kotlin/Main.web.kt | 67 ++ .../src/webMain/kotlin/runGC.web.kt | 3 + .../src/webMain/resources/index.html | 22 + .../src/webMain/resources/launcher.mjs | 5 + .../webMain/resources/launcher_jetstream3.mjs | 16 + .../src/webMain/resources/polyfills.mjs | 83 +++ .../src/webMain/resources/styles.css | 28 + settings-fork.gradle | 1 + 68 files changed, 5920 insertions(+) create mode 100644 compose/mpp/benchmarks/build.gradle.kts create mode 100644 compose/mpp/benchmarks/gradle.properties create mode 100644 compose/mpp/benchmarks/gradle/wasm-d8-runtime.gradle.kts create mode 100644 compose/mpp/benchmarks/iosApp/Configuration/Config.xcconfig create mode 100644 compose/mpp/benchmarks/iosApp/iosApp.xcodeproj/project.pbxproj create mode 100644 compose/mpp/benchmarks/iosApp/iosApp/Assets.xcassets/AccentColor.colorset/Contents.json create mode 100644 compose/mpp/benchmarks/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 compose/mpp/benchmarks/iosApp/iosApp/Assets.xcassets/Contents.json create mode 100644 compose/mpp/benchmarks/iosApp/iosApp/ContentView.swift create mode 100644 compose/mpp/benchmarks/iosApp/iosApp/Info.plist create mode 100644 compose/mpp/benchmarks/iosApp/iosApp/Preview Content/Preview Assets.xcassets/Contents.json create mode 100644 compose/mpp/benchmarks/iosApp/iosApp/iOSApp.swift create mode 100644 compose/mpp/benchmarks/src/appleMain/kotlin/BenchmarksSave.apple.kt create mode 100644 compose/mpp/benchmarks/src/appleMain/kotlin/GraphicsContext.apple.kt create mode 100644 compose/mpp/benchmarks/src/appleMain/kotlin/getProcessStartTime.apple.kt create mode 100644 compose/mpp/benchmarks/src/appleMain/kotlin/runGC.native.kt create mode 100644 compose/mpp/benchmarks/src/commonMain/composeResources/drawable/compose-multiplatform.png create mode 100644 compose/mpp/benchmarks/src/commonMain/composeResources/drawable/example1_cat.jpg create mode 100644 compose/mpp/benchmarks/src/commonMain/composeResources/drawable/example1_ic_call_answer.xml create mode 100644 compose/mpp/benchmarks/src/commonMain/composeResources/drawable/example1_sailing.svg create mode 100644 compose/mpp/benchmarks/src/commonMain/composeResources/files/example1_compose-community-primary.png create mode 100644 compose/mpp/benchmarks/src/commonMain/composeResources/font/jetbrainsmono_italic.ttf create mode 100644 compose/mpp/benchmarks/src/commonMain/composeResources/font/jetbrainsmono_regular.ttf create mode 100644 compose/mpp/benchmarks/src/commonMain/kotlin/BenchmarkComposeScene.kt create mode 100644 compose/mpp/benchmarks/src/commonMain/kotlin/Benchmarks.kt create mode 100644 compose/mpp/benchmarks/src/commonMain/kotlin/BenchmarksSave.kt create mode 100644 compose/mpp/benchmarks/src/commonMain/kotlin/Config.kt create mode 100644 compose/mpp/benchmarks/src/commonMain/kotlin/MeasureComposable.kt create mode 100644 compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/animation/AnimatedVisibility.kt create mode 100644 compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/canvasdrawing/CanvasDrawing.kt create mode 100644 compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/complexlazylist/components/MainUI.kt create mode 100644 compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/complexlazylist/components/MultiLevelCell.kt create mode 100644 compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/complexlazylist/components/refresh/LoadingIndicator.kt create mode 100644 compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/complexlazylist/components/refresh/SwipeProgress.kt create mode 100644 compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/complexlazylist/components/refresh/SwipeRefreshLayout.kt create mode 100644 compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/complexlazylist/components/refresh/SwipeRefreshNestedScrollConnection.kt create mode 100644 compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/complexlazylist/components/refresh/SwipeRefreshState.kt create mode 100644 compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/complexlazylist/models/FollowItem.kt create mode 100644 compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/complexlazylist/models/HotItem.kt create mode 100644 compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/complexlazylist/models/IBaseViewModel.kt create mode 100644 compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/complexlazylist/models/ICompositionModel.kt create mode 100644 compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/complexlazylist/models/ViewModelFactory.kt create mode 100644 compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/heavyshader/HeavyShader.kt create mode 100644 compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/lazygrid/LazyGrid.kt create mode 100644 compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/multipleComponents/Clickable.kt create mode 100644 compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/multipleComponents/MenuIcon.kt create mode 100644 compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/multipleComponents/MultipleComponents.kt create mode 100644 compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/textlayout/TextLayout.kt create mode 100644 compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/visualeffects/HappyNY.kt create mode 100644 compose/mpp/benchmarks/src/desktopMain/kotlin/BenchmarksSave.desktop.kt create mode 100644 compose/mpp/benchmarks/src/desktopMain/kotlin/BenchmarksSaveServer.kt create mode 100644 compose/mpp/benchmarks/src/desktopMain/kotlin/GraphicsContext.desktop.kt create mode 100644 compose/mpp/benchmarks/src/desktopMain/kotlin/main.desktop.kt create mode 100644 compose/mpp/benchmarks/src/desktopMain/kotlin/runGC.jvm.kt create mode 100644 compose/mpp/benchmarks/src/iosMain/kotlin/Platform.ios.kt create mode 100644 compose/mpp/benchmarks/src/iosMain/kotlin/main.ios.kt create mode 100644 compose/mpp/benchmarks/src/jsMain/kotlin/Main.js.kt create mode 100644 compose/mpp/benchmarks/src/macosMain/kotlin/main.macos.kt create mode 100644 compose/mpp/benchmarks/src/wasmJsMain/kotlin/Main.wasmJs.kt create mode 100644 compose/mpp/benchmarks/src/webMain/kotlin/BenchmarksSave.web.kt create mode 100644 compose/mpp/benchmarks/src/webMain/kotlin/Main.web.kt create mode 100644 compose/mpp/benchmarks/src/webMain/kotlin/runGC.web.kt create mode 100644 compose/mpp/benchmarks/src/webMain/resources/index.html create mode 100644 compose/mpp/benchmarks/src/webMain/resources/launcher.mjs create mode 100644 compose/mpp/benchmarks/src/webMain/resources/launcher_jetstream3.mjs create mode 100644 compose/mpp/benchmarks/src/webMain/resources/polyfills.mjs create mode 100644 compose/mpp/benchmarks/src/webMain/resources/styles.css diff --git a/compose/mpp/benchmarks/build.gradle.kts b/compose/mpp/benchmarks/build.gradle.kts new file mode 100644 index 0000000000000..788a123f87d91 --- /dev/null +++ b/compose/mpp/benchmarks/build.gradle.kts @@ -0,0 +1,207 @@ +/* + * Copyright 2026 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:OptIn(ExperimentalWasmDsl::class) + +import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl +import org.jetbrains.kotlin.gradle.targets.js.webpack.KotlinWebpack +import org.jetbrains.kotlin.gradle.targets.js.webpack.KotlinWebpackConfig +import org.jetbrains.kotlin.gradle.targets.wasm.binaryen.BinaryenExec +import org.jetbrains.kotlin.gradle.targets.wasm.d8.D8Exec + +plugins { + id("org.jetbrains.compose") version "1.13.0-beta02" + id("org.jetbrains.kotlin.plugin.compose") version libs.versions.kotlin.get() + id("kotlin-multiplatform") + alias(libs.plugins.kotlinSerialization) +} + +version = "1.0-SNAPSHOT" + +kotlin { + jvm("desktop") + + listOf(iosArm64(), iosSimulatorArm64()).forEach { target -> + target.binaries.framework { + baseName = "benchmarks" + isStatic = true + } + } + + macosArm64 { + binaries.executable { + entryPoint = "main" + } + } + + wasmJs { + outputModuleName = "mpp-benchmarks" + browser { + commonWebpackConfig { + outputFileName = "benchmarks.js" + } + } + d8 { + compilerOptions.freeCompilerArgs.add("-Xwasm-attach-js-exception") + } + binaries.executable() + binaries.configureEach { + compilation.compileTaskProvider.configure { + compilerOptions.freeCompilerArgs.add("-Xwasm-use-new-exception-proposal") + } + } + } + + js { + outputModuleName = "mpp-benchmarks" + browser { + commonWebpackConfig { + outputFileName = "benchmarks.js" + } + } + binaries.executable() + } + + applyDefaultHierarchyTemplate() + + sourceSets { + commonMain.dependencies { + implementation(project(":compose:runtime:runtime")) + implementation(project(":compose:ui:ui")) + implementation(project(":compose:foundation:foundation")) + implementation(project(":compose:material:material")) + implementation("org.jetbrains.compose.components:components-resources:1.13.0-beta02") { + exclude(group = "org.jetbrains.compose.runtime") + exclude(group = "org.jetbrains.compose.ui") + } + + implementation(libs.skiko) + implementation(libs.kotlinCoroutinesCore) + implementation(libs.kotlinSerializationJson) + implementation("org.jetbrains.kotlinx:kotlinx-io-core:0.9.2") + implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.8.0") + } + + webMain.dependencies { + implementation(libs.kotlinXw3c) + implementation("io.ktor:ktor-client-core:3.6.1") + implementation("io.ktor:ktor-client-js:3.6.1") + implementation("io.ktor:ktor-client-content-negotiation:3.6.1") + implementation("io.ktor:ktor-serialization-kotlinx-json:3.6.1") + } + + val desktopMain by getting { + dependencies { + implementation(libs.kotlinCoroutinesSwing) + implementation(libs.skikoAwtRuntime) + implementation("io.ktor:ktor-server-core:3.6.1") + implementation("io.ktor:ktor-server-netty:3.6.1") + implementation("io.ktor:ktor-server-content-negotiation:3.6.1") + implementation("io.ktor:ktor-serialization-kotlinx-json:3.6.1") + implementation("io.ktor:ktor-client-core:3.6.1") + implementation("io.ktor:ktor-client-java:3.6.1") + implementation("io.ktor:ktor-client-content-negotiation:3.6.1") + implementation("io.ktor:ktor-server-cors:3.6.1") + } + } + } +} + +compose.resources { + packageOfResClass = "compose_benchmarks.benchmarks.generated.resources" +} + +val runArguments = providers.gradleProperty("runArguments").orElse("") +val versionInfo = "versionInfo=\"local (Kotlin ${libs.versions.kotlin.get()})\"" +val benchmarkArguments = runArguments.map { arguments -> + (arguments.split(" ").filter { it.isNotEmpty() } + versionInfo) + .map { it.replace(" ", "%20") } +} + +compose.desktop { + application { + mainClass = "Main_desktopKt" + args.addAll(benchmarkArguments.get()) + } +} + +apply(from = "gradle/wasm-d8-runtime.gradle.kts") + +tasks.withType().matching { it.name.startsWith("runReleaseExecutableMacos") }.configureEach { + args(benchmarkArguments.get()) +} + +tasks.named("wasmJsBrowserProductionRun") { + val query = benchmarkArguments.get() + .mapIndexed { index, arg -> "arg$index=$arg" } + .joinToString("&") + devServerProperty = (devServerProperty.orNull ?: KotlinWebpackConfig.DevServer()).copy( + open = "http://localhost:8080?$query" + ) +} + +tasks.named("wasmJsD8DevelopmentRun") { + dependsOn("prepareWasmJsD8DevelopmentRuntime") + inputFileProperty.set(layout.buildDirectory.file("d8/development/launcher.mjs")) +} + +tasks.named("wasmJsD8ProductionRun") { + dependsOn("prepareWasmJsD8ProductionRuntime") + inputFileProperty.set(layout.buildDirectory.file("d8/production/launcher.mjs")) +} + +tasks.withType().configureEach { + args(benchmarkArguments.get()) +} + +tasks.withType().configureEach { + binaryenArguments.add("-g") +} + +val benchmarkRepositoryDir = rootProject.layout.projectDirectory.asFile +val benchmarkGradlew = rootProject.layout.projectDirectory.file("gradlew").asFile + +tasks.register("runBrowserAndSaveStats") { + group = "application" + description = "Runs the benchmark results server and the Wasm browser benchmarks." + val repositoryDir = benchmarkRepositoryDir + val gradlew = benchmarkGradlew + val arguments = runArguments + doLast { + var serverProcess: Process? = null + var clientProcess: Process? = null + try { + serverProcess = ProcessBuilder( + gradlew.absolutePath, + ":compose:mpp:benchmarks:run", + "-PrunArguments=runServer=true saveStatsToJSON=true", + "--console=plain" + ).directory(repositoryDir).inheritIO().start() + + clientProcess = ProcessBuilder( + gradlew.absolutePath, + ":compose:mpp:benchmarks:wasmJsBrowserProductionRun", + "-PrunArguments=" + arguments.get() + " saveStatsToJSON=true", + "--console=plain" + ).directory(repositoryDir).inheritIO().start() + + serverProcess.waitFor() + } finally { + serverProcess?.destroy() + clientProcess?.destroy() + } + } +} diff --git a/compose/mpp/benchmarks/gradle.properties b/compose/mpp/benchmarks/gradle.properties new file mode 100644 index 0000000000000..d15154e785f1e --- /dev/null +++ b/compose/mpp/benchmarks/gradle.properties @@ -0,0 +1 @@ +org.jetbrains.compose.experimental.macos.enabled=true diff --git a/compose/mpp/benchmarks/gradle/wasm-d8-runtime.gradle.kts b/compose/mpp/benchmarks/gradle/wasm-d8-runtime.gradle.kts new file mode 100644 index 0000000000000..846161ad18069 --- /dev/null +++ b/compose/mpp/benchmarks/gradle/wasm-d8-runtime.gradle.kts @@ -0,0 +1,72 @@ +import org.gradle.api.tasks.Sync +import java.io.File +import kotlin.text.replace + +val jsJodaEsmFile = rootProject.layout.buildDirectory.file( + "wasm/node_modules/@js-joda/core/dist/js-joda.esm.js" +) +val wasmResourcesDirectory = project.layout.buildDirectory.dir("processedResources/wasmJs/main") +val skikoRuntime = tasks.named("unpackSkikoRuntimeForWasmJs") + +object D8RuntimePatcher { + private fun File.replaceRequired(oldValue: String, newValue: String) { + check(isFile) { "Expected D8 runtime file does not exist: $this" } + + val original = readText() + check(oldValue in original) { + "Cannot prepare the D8 runtime: expected content was not found in $this" + } + writeText(original.replace(oldValue, newValue)) + } + + fun patch(directory: File) { + directory.resolve("mpp-benchmarks.import-object.mjs").apply { + replaceRequired("from './skiko.mjs'", "from './skikod8.mjs'") + replaceRequired("from '@js-joda/core'", "from './js-joda.esm.js'") + } + + directory.resolve("skikod8.mjs").replaceRequired( + oldValue = "loadSkikoWASM().then(", + newValue = """ + loadSkikoWASM({ + locateFile: (path) => String(import.meta.url) + .replace(/^file:\/\//, '') + .replace(/\/[^/]+${'$'}/, '/' + path) + }).then( + """.trimIndent() + ) + } +} + +fun registerD8Runtime(variant: String, compiledPath: String, outputPath: String) = + tasks.register("prepareWasmJsD8${variant}Runtime") { + dependsOn("wasmJs${variant}ExecutableCompileSync", ":kotlinWasmNpmInstall") + from(project.layout.buildDirectory.dir(compiledPath)) + from(wasmResourcesDirectory) + from(skikoRuntime) + from(jsJodaEsmFile) + into(project.layout.buildDirectory.dir(outputPath)) + doLast { + D8RuntimePatcher.patch(destinationDir) + } + } + +val d8DevelopmentDirectory = project.layout.buildDirectory.dir("d8/development") +val d8ProductionDirectory = project.layout.buildDirectory.dir("d8/production") +val prepareWasmJsD8DevelopmentRuntime = registerD8Runtime( + variant = "Development", + compiledPath = "compileSync/wasmJs/main/developmentExecutable/kotlin", + outputPath = "d8/development" +) +val prepareWasmJsD8ProductionRuntime = registerD8Runtime( + variant = "Production", + compiledPath = "compileSync/wasmJs/main/productionExecutable/optimized", + outputPath = "d8/production" +) + +tasks.register("buildD8Distribution", org.gradle.api.tasks.bundling.Zip::class.java) { + dependsOn(prepareWasmJsD8ProductionRuntime) + from(d8ProductionDirectory) + archiveFileName.set("d8-distribution.zip") + destinationDirectory.set(project.layout.buildDirectory.dir("distributions")) +} diff --git a/compose/mpp/benchmarks/iosApp/Configuration/Config.xcconfig b/compose/mpp/benchmarks/iosApp/Configuration/Config.xcconfig new file mode 100644 index 0000000000000..0efa34475184f --- /dev/null +++ b/compose/mpp/benchmarks/iosApp/Configuration/Config.xcconfig @@ -0,0 +1,6 @@ +TEAM_ID= +BUNDLE_ID=org.jetbrains.benchmarks +APP_NAME=ComposeBenchmarks + +// Override CMP_OUT_DIR when the Gradle build uses a custom OUT_DIR. +CMP_OUT_DIR=$(SRCROOT)/../../../../out diff --git a/compose/mpp/benchmarks/iosApp/iosApp.xcodeproj/project.pbxproj b/compose/mpp/benchmarks/iosApp/iosApp.xcodeproj/project.pbxproj new file mode 100644 index 0000000000000..073ef13b7fab2 --- /dev/null +++ b/compose/mpp/benchmarks/iosApp/iosApp.xcodeproj/project.pbxproj @@ -0,0 +1,382 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 50; + objects = { + +/* Begin PBXBuildFile section */ + 058557BB273AAA24004C7B11 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 058557BA273AAA24004C7B11 /* Assets.xcassets */; }; + 058557D9273AAEEB004C7B11 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 058557D8273AAEEB004C7B11 /* Preview Assets.xcassets */; }; + 2152FB042600AC8F00CF470E /* iOSApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2152FB032600AC8F00CF470E /* iOSApp.swift */; }; + 7555FF83242A565900829871 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7555FF82242A565900829871 /* ContentView.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 058557BA273AAA24004C7B11 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 058557D8273AAEEB004C7B11 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; + 2152FB032600AC8F00CF470E /* iOSApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = iOSApp.swift; sourceTree = ""; }; + 7555FF7B242A565900829871 /* ComposeBenchmarks.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ComposeBenchmarks.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 7555FF82242A565900829871 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; + 7555FF8C242A565B00829871 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + AB3632DC29227652001CCB65 /* Config.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Config.xcconfig; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + F85CB1118929364A9C6EFABC /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 058557D7273AAEEB004C7B11 /* Preview Content */ = { + isa = PBXGroup; + children = ( + 058557D8273AAEEB004C7B11 /* Preview Assets.xcassets */, + ); + path = "Preview Content"; + sourceTree = ""; + }; + 7555FF72242A565900829871 = { + isa = PBXGroup; + children = ( + AB1DB47929225F7C00F7AF9C /* Configuration */, + 7555FF7D242A565900829871 /* iosApp */, + 7555FF7C242A565900829871 /* Products */, + ); + sourceTree = ""; + }; + 7555FF7C242A565900829871 /* Products */ = { + isa = PBXGroup; + children = ( + 7555FF7B242A565900829871 /* ComposeBenchmarks.app */, + ); + name = Products; + sourceTree = ""; + }; + 7555FF7D242A565900829871 /* iosApp */ = { + isa = PBXGroup; + children = ( + 058557BA273AAA24004C7B11 /* Assets.xcassets */, + 7555FF82242A565900829871 /* ContentView.swift */, + 7555FF8C242A565B00829871 /* Info.plist */, + 2152FB032600AC8F00CF470E /* iOSApp.swift */, + 058557D7273AAEEB004C7B11 /* Preview Content */, + ); + path = iosApp; + sourceTree = ""; + }; + AB1DB47929225F7C00F7AF9C /* Configuration */ = { + isa = PBXGroup; + children = ( + AB3632DC29227652001CCB65 /* Config.xcconfig */, + ); + path = Configuration; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 7555FF7A242A565900829871 /* iosApp */ = { + isa = PBXNativeTarget; + buildConfigurationList = 7555FFA5242A565B00829871 /* Build configuration list for PBXNativeTarget "iosApp" */; + buildPhases = ( + 05E104082A8AADF800D11C3D /* Compile Kotlin */, + 7555FF77242A565900829871 /* Sources */, + 7555FF79242A565900829871 /* Resources */, + F85CB1118929364A9C6EFABC /* Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = iosApp; + productName = iosApp; + productReference = 7555FF7B242A565900829871 /* ComposeBenchmarks.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 7555FF73242A565900829871 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 1130; + LastUpgradeCheck = 1130; + ORGANIZATIONNAME = orgName; + TargetAttributes = { + 7555FF7A242A565900829871 = { + CreatedOnToolsVersion = 11.3.1; + }; + }; + }; + buildConfigurationList = 7555FF76242A565900829871 /* Build configuration list for PBXProject "iosApp" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 7555FF72242A565900829871; + productRefGroup = 7555FF7C242A565900829871 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 7555FF7A242A565900829871 /* iosApp */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 7555FF79242A565900829871 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 058557D9273AAEEB004C7B11 /* Preview Assets.xcassets in Resources */, + 058557BB273AAA24004C7B11 /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 05E104082A8AADF800D11C3D /* Compile Kotlin */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = "Compile Kotlin"; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "cd \"$SRCROOT/../../../..\"\n./gradlew :compose:mpp:benchmarks:embedAndSignAppleFrameworkForXcode --console=plain\n"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 7555FF77242A565900829871 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 2152FB042600AC8F00CF470E /* iOSApp.swift in Sources */, + 7555FF83242A565900829871 /* ContentView.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 7555FFA3242A565B00829871 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = AB3632DC29227652001CCB65 /* Config.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 14.1; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 7555FFA4242A565B00829871 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = AB3632DC29227652001CCB65 /* Config.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 14.1; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 7555FFA6242A565B00829871 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_ASSET_PATHS = "\"iosApp/Preview Content\""; + DEVELOPMENT_TEAM = "${TEAM_ID}"; + ENABLE_PREVIEWS = YES; + FRAMEWORK_SEARCH_PATHS = "$(CMP_OUT_DIR)/compose-multiplatform-core/compose/mpp/benchmarks/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)"; + INFOPLIST_FILE = iosApp/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 14.1; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + OTHER_LDFLAGS = ( + "$(inherited)", + "-framework", + "benchmarks", + ); + PRODUCT_BUNDLE_IDENTIFIER = "${BUNDLE_ID}${TEAM_ID}"; + PRODUCT_NAME = "${APP_NAME}"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 7555FFA7242A565B00829871 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_ASSET_PATHS = "\"iosApp/Preview Content\""; + DEVELOPMENT_TEAM = "${TEAM_ID}"; + ENABLE_PREVIEWS = YES; + FRAMEWORK_SEARCH_PATHS = "$(CMP_OUT_DIR)/compose-multiplatform-core/compose/mpp/benchmarks/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)"; + INFOPLIST_FILE = iosApp/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 14.1; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + OTHER_LDFLAGS = ( + "$(inherited)", + "-framework", + "benchmarks", + ); + PRODUCT_BUNDLE_IDENTIFIER = "${BUNDLE_ID}${TEAM_ID}"; + PRODUCT_NAME = "${APP_NAME}"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 7555FF76242A565900829871 /* Build configuration list for PBXProject "iosApp" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 7555FFA3242A565B00829871 /* Debug */, + 7555FFA4242A565B00829871 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 7555FFA5242A565B00829871 /* Build configuration list for PBXNativeTarget "iosApp" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 7555FFA6242A565B00829871 /* Debug */, + 7555FFA7242A565B00829871 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 7555FF73242A565900829871 /* Project object */; +} diff --git a/compose/mpp/benchmarks/iosApp/iosApp/Assets.xcassets/AccentColor.colorset/Contents.json b/compose/mpp/benchmarks/iosApp/iosApp/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000000000..ee7e3ca03f870 --- /dev/null +++ b/compose/mpp/benchmarks/iosApp/iosApp/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} \ No newline at end of file diff --git a/compose/mpp/benchmarks/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/Contents.json b/compose/mpp/benchmarks/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000000000..fb88a396bf389 --- /dev/null +++ b/compose/mpp/benchmarks/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,98 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "60x60" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "60x60" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "20x20" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "29x29" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "40x40" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "40x40" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "76x76" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "76x76" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "83.5x83.5" + }, + { + "idiom" : "ios-marketing", + "scale" : "1x", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} \ No newline at end of file diff --git a/compose/mpp/benchmarks/iosApp/iosApp/Assets.xcassets/Contents.json b/compose/mpp/benchmarks/iosApp/iosApp/Assets.xcassets/Contents.json new file mode 100644 index 0000000000000..4aa7c5350bfce --- /dev/null +++ b/compose/mpp/benchmarks/iosApp/iosApp/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} \ No newline at end of file diff --git a/compose/mpp/benchmarks/iosApp/iosApp/ContentView.swift b/compose/mpp/benchmarks/iosApp/iosApp/ContentView.swift new file mode 100644 index 0000000000000..dad9234931cfd --- /dev/null +++ b/compose/mpp/benchmarks/iosApp/iosApp/ContentView.swift @@ -0,0 +1,25 @@ +import UIKit +import SwiftUI +import benchmarks + +struct ContentView: View { + var body: some View { + if (Main_iosKt.runReal()) { + ComposeView() + .ignoresSafeArea(.all) // Compose has own keyboard handler + } else { + VStack { + Text("Hello, world!") + } + .padding() + } + } +} + +struct ComposeView: UIViewControllerRepresentable { + func makeUIViewController(context: Context) -> UIViewController { + Main_iosKt.MainViewController() + } + + func updateUIViewController(_ uiViewController: UIViewController, context: Context) {} +} diff --git a/compose/mpp/benchmarks/iosApp/iosApp/Info.plist b/compose/mpp/benchmarks/iosApp/iosApp/Info.plist new file mode 100644 index 0000000000000..412e3781280f8 --- /dev/null +++ b/compose/mpp/benchmarks/iosApp/iosApp/Info.plist @@ -0,0 +1,50 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + LSRequiresIPhoneOS + + CADisableMinimumFrameDurationOnPhone + + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + + UILaunchScreen + + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/compose/mpp/benchmarks/iosApp/iosApp/Preview Content/Preview Assets.xcassets/Contents.json b/compose/mpp/benchmarks/iosApp/iosApp/Preview Content/Preview Assets.xcassets/Contents.json new file mode 100644 index 0000000000000..4aa7c5350bfce --- /dev/null +++ b/compose/mpp/benchmarks/iosApp/iosApp/Preview Content/Preview Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} \ No newline at end of file diff --git a/compose/mpp/benchmarks/iosApp/iosApp/iOSApp.swift b/compose/mpp/benchmarks/iosApp/iosApp/iOSApp.swift new file mode 100644 index 0000000000000..2ea04889350db --- /dev/null +++ b/compose/mpp/benchmarks/iosApp/iosApp/iOSApp.swift @@ -0,0 +1,37 @@ +import SwiftUI +import benchmarks +import Combine + +class MemoryWarningMonitor: ObservableObject { + private var cancellable: AnyCancellable? + + init() { + self.cancellable = NotificationCenter.default + .publisher(for: UIApplication.didReceiveMemoryWarningNotification) + .sink { _ in + self.didReceiveMemoryWarning() + } + } + + private func didReceiveMemoryWarning() { +// print("Memory warning received! Cleaning resources.") + RunGC_nativeKt.runGC() + } +} + +@main +struct iOSApp: App { + init() { + MemoryWarningMonitor() + Main_iosKt.setGlobalFromArgs(args: ProcessInfo.processInfo.arguments) + if (!Main_iosKt.runReal()) { + Main_iosKt.runBenchmarks() + } + } + + var body: some Scene { + WindowGroup { + ContentView() + } + } +} \ No newline at end of file diff --git a/compose/mpp/benchmarks/src/appleMain/kotlin/BenchmarksSave.apple.kt b/compose/mpp/benchmarks/src/appleMain/kotlin/BenchmarksSave.apple.kt new file mode 100644 index 0000000000000..815b87fb501a2 --- /dev/null +++ b/compose/mpp/benchmarks/src/appleMain/kotlin/BenchmarksSave.apple.kt @@ -0,0 +1,6 @@ +/* + * Copyright 2020-2025 JetBrains s.r.o. and respective authors and developers. + * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. + */ + +actual fun saveBenchmarkStats(name: String, stats: BenchmarkStats) = saveBenchmarkStatsOnDisk(name, stats) diff --git a/compose/mpp/benchmarks/src/appleMain/kotlin/GraphicsContext.apple.kt b/compose/mpp/benchmarks/src/appleMain/kotlin/GraphicsContext.apple.kt new file mode 100644 index 0000000000000..fb6564fcc41cf --- /dev/null +++ b/compose/mpp/benchmarks/src/appleMain/kotlin/GraphicsContext.apple.kt @@ -0,0 +1,65 @@ +import kotlinx.cinterop.ExperimentalForeignApi +import kotlinx.cinterop.objcPtr +import org.jetbrains.skia.BackendRenderTarget +import org.jetbrains.skia.ColorSpace +import org.jetbrains.skia.DirectContext +import org.jetbrains.skia.PixelGeometry +import org.jetbrains.skia.Surface +import org.jetbrains.skia.SurfaceColorFormat +import org.jetbrains.skia.SurfaceOrigin +import org.jetbrains.skia.SurfaceProps +import platform.Metal.MTLCreateSystemDefaultDevice +import platform.Metal.MTLPixelFormatBGRA8Unorm +import platform.Metal.MTLTextureDescriptor +import platform.Metal.MTLTextureType2D +import platform.Metal.MTLTextureUsageRenderTarget +import platform.Metal.MTLTextureUsageShaderRead +import platform.Metal.MTLTextureUsageShaderWrite +import kotlin.coroutines.resume +import kotlin.coroutines.suspendCoroutine + +@OptIn(ExperimentalForeignApi::class) +fun graphicsContext() = object : GraphicsContext { + private val device = MTLCreateSystemDefaultDevice() ?: throw IllegalStateException("Can't create MTLDevice") + private val commandQueue = device.newCommandQueue() ?: throw IllegalStateException("Can't create MTLCommandQueue") + private val directContext = DirectContext.makeMetal(device.objcPtr(), commandQueue.objcPtr()) + + override fun surface(width: Int, height: Int): Surface { + val descriptor = MTLTextureDescriptor() + descriptor.width = width.toULong() + descriptor.height = height.toULong() + descriptor.usage = MTLTextureUsageShaderRead or MTLTextureUsageShaderWrite or MTLTextureUsageRenderTarget + descriptor.textureType = MTLTextureType2D + descriptor.pixelFormat = MTLPixelFormatBGRA8Unorm + descriptor.mipmapLevelCount = 1UL + + val texture = device.newTextureWithDescriptor(descriptor) ?: throw IllegalStateException("Can't create MTLTexture") + + val renderTarget = BackendRenderTarget.makeMetal(width, height, texture.objcPtr()) + + return Surface.makeFromBackendRenderTarget( + directContext, + renderTarget, + SurfaceOrigin.TOP_LEFT, + SurfaceColorFormat.BGRA_8888, + ColorSpace.sRGB, + SurfaceProps(pixelGeometry = PixelGeometry.UNKNOWN) + ) ?: throw IllegalStateException("Can't create Surface") + } + + override suspend fun awaitGPUCompletion() { + val commandBuffer = commandQueue.commandBuffer() ?: return + + suspendCoroutine { continuation -> + commandBuffer.addCompletedHandler { + continuation.resume(Unit) + } + + commandBuffer.commit() + } + } +} + + + + diff --git a/compose/mpp/benchmarks/src/appleMain/kotlin/getProcessStartTime.apple.kt b/compose/mpp/benchmarks/src/appleMain/kotlin/getProcessStartTime.apple.kt new file mode 100644 index 0000000000000..d02de1d7c330a --- /dev/null +++ b/compose/mpp/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/compose/mpp/benchmarks/src/appleMain/kotlin/runGC.native.kt b/compose/mpp/benchmarks/src/appleMain/kotlin/runGC.native.kt new file mode 100644 index 0000000000000..7b499db732553 --- /dev/null +++ b/compose/mpp/benchmarks/src/appleMain/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 diff --git a/compose/mpp/benchmarks/src/commonMain/composeResources/drawable/compose-multiplatform.png b/compose/mpp/benchmarks/src/commonMain/composeResources/drawable/compose-multiplatform.png new file mode 100644 index 0000000000000000000000000000000000000000..8b74accd0beb7831c819df311cd38e47cb57f601 GIT binary patch literal 34968 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 literal 0 HcmV?d00001 diff --git a/compose/mpp/benchmarks/src/commonMain/composeResources/drawable/example1_cat.jpg b/compose/mpp/benchmarks/src/commonMain/composeResources/drawable/example1_cat.jpg new file mode 100644 index 0000000000000000000000000000000000000000..62f987d6dcf1aeff1e227fdcea04c6bed7c26875 GIT binary patch literal 37524 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 Hf4=?~qAD`< literal 0 HcmV?d00001 diff --git a/compose/mpp/benchmarks/src/commonMain/composeResources/drawable/example1_ic_call_answer.xml b/compose/mpp/benchmarks/src/commonMain/composeResources/drawable/example1_ic_call_answer.xml new file mode 100644 index 0000000000000..da8a42f54a3ec --- /dev/null +++ b/compose/mpp/benchmarks/src/commonMain/composeResources/drawable/example1_ic_call_answer.xml @@ -0,0 +1,36 @@ + + + + diff --git a/compose/mpp/benchmarks/src/commonMain/composeResources/drawable/example1_sailing.svg b/compose/mpp/benchmarks/src/commonMain/composeResources/drawable/example1_sailing.svg new file mode 100644 index 0000000000000..ba23d0fe090c4 --- /dev/null +++ b/compose/mpp/benchmarks/src/commonMain/composeResources/drawable/example1_sailing.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/compose/mpp/benchmarks/src/commonMain/composeResources/files/example1_compose-community-primary.png b/compose/mpp/benchmarks/src/commonMain/composeResources/files/example1_compose-community-primary.png new file mode 100644 index 0000000000000000000000000000000000000000..a73ddd79899482e2f1252de2a3e9d4b8225f140b GIT binary patch literal 17662 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 literal 0 HcmV?d00001 diff --git a/compose/mpp/benchmarks/src/commonMain/composeResources/font/jetbrainsmono_italic.ttf b/compose/mpp/benchmarks/src/commonMain/composeResources/font/jetbrainsmono_italic.ttf new file mode 100644 index 0000000000000000000000000000000000000000..44e1f4a7f3acd89e15ab5050607ee7bb497f5d1a GIT binary patch literal 141572 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

-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 literal 0 HcmV?d00001 diff --git a/compose/mpp/benchmarks/src/commonMain/kotlin/BenchmarkComposeScene.kt b/compose/mpp/benchmarks/src/commonMain/kotlin/BenchmarkComposeScene.kt new file mode 100644 index 0000000000000..7354aea9df219 --- /dev/null +++ b/compose/mpp/benchmarks/src/commonMain/kotlin/BenchmarkComposeScene.kt @@ -0,0 +1,36 @@ +import androidx.compose.runtime.Composable +import androidx.compose.ui.InternalComposeUiApi +import androidx.compose.ui.graphics.Canvas +import androidx.compose.ui.platform.FrameRecomposer +import androidx.compose.ui.platform.registerSkikoComposeImplementation +import androidx.compose.ui.scene.CanvasLayersComposeScene +import androidx.compose.ui.unit.IntSize +import kotlinx.coroutines.Dispatchers + +@OptIn(InternalComposeUiApi::class) +internal class BenchmarkComposeScene(width: Int, height: Int) { + init { + registerSkikoComposeImplementation() + } + + private val frameRecomposer = FrameRecomposer(Dispatchers.Unconfined) + private val scene = CanvasLayersComposeScene( + frameRecomposer = frameRecomposer, + size = IntSize(width, height) + ) + + fun setContent(content: @Composable () -> Unit) { + scene.setContent(content = content) + } + + fun render(canvas: Canvas, nanoTime: Long) { + frameRecomposer.performFrame(nanoTime) + scene.measureAndLayout() + scene.draw(canvas) + } + + fun close() { + scene.close() + frameRecomposer.close() + } +} diff --git a/compose/mpp/benchmarks/src/commonMain/kotlin/Benchmarks.kt b/compose/mpp/benchmarks/src/commonMain/kotlin/Benchmarks.kt new file mode 100644 index 0000000000000..e2fd20870c4fe --- /dev/null +++ b/compose/mpp/benchmarks/src/commonMain/kotlin/Benchmarks.kt @@ -0,0 +1,597 @@ +import androidx.compose.runtime.Composable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.runtime.withFrameNanos +import kotlin.time.Duration.Companion.nanoseconds +import kotlin.time.TimeSource +import benchmarks.animation.AnimatedVisibility +import benchmarks.complexlazylist.components.MainUiNoImageUseModel +import benchmarks.multipleComponents.MultipleComponentsExample +import benchmarks.lazygrid.LazyGrid +import benchmarks.visualeffects.NYContent +import benchmarks.textlayout.TextLayout +import benchmarks.canvasdrawing.CanvasDrawing +import benchmarks.heavyshader.HeavyShader +import kotlinx.coroutines.delay +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.Json +import kotlin.math.roundToInt +import kotlin.time.Duration +import kotlin.time.Duration.Companion.milliseconds +import kotlin.time.Duration.Companion.seconds +import kotlin.time.DurationUnit + +enum class BenchmarkFrameTimeKind { + CPU, GPU +} + + +fun BenchmarkFrameTimeKind.toPrettyPrintString(): String = + when (this) { + BenchmarkFrameTimeKind.CPU -> "CPU" + BenchmarkFrameTimeKind.GPU -> "GPU" + } + +data class BenchmarkFrame( + val cpuDuration: Duration, + val gpuDuration: Duration +) { + fun duration(kind: BenchmarkFrameTimeKind): Duration = + when (kind) { + BenchmarkFrameTimeKind.CPU -> cpuDuration + BenchmarkFrameTimeKind.GPU -> gpuDuration + } +} + +@Serializable +data class BenchmarkConditions( + val frameCount: Int, + val warmupCount: Int +) { + fun prettyPrint() { + println("$frameCount frames (warmup $warmupCount)") + } + + fun putFormattedValuesTo(map: MutableMap) { + map.put("Frames/warmup", "$frameCount/$warmupCount") + } +} + +@Serializable +data class FPSInfo( + val fps: Double +) { + + fun prettyPrint() { + println("Average FPS: $fps") + } + + fun putFormattedValuesTo(map: MutableMap) { + map.put("Average FPS", fps.toString()) + } +} + +@Serializable +data class FrameInfo( + val cpuTime: Duration, + val gpuTime: Duration, +) { + val totalTime = cpuTime + gpuTime + + fun prettyPrint() { + if (gpuTime == Duration.ZERO) { + println("Average frame time: $totalTime") + } else { + println("CPU average frame time: $cpuTime") + println("GPU average frame time: $gpuTime") + println("TOTAL average frame time: $totalTime") + } + } + + fun putFormattedValuesTo(map: MutableMap) { + map.put("CPU avg frame (ms)", cpuTime.formatAsMilliseconds()) + map.put("GPU avg frame (ms)", gpuTime.formatAsMilliseconds()) + map.put("TOTAL avg frame (ms)", totalTime.formatAsMilliseconds()) + } +} + +@Serializable +data class BenchmarkPercentileAverage( + val percentile: Double, + val average: Duration +) + +@Serializable +data class MissedFrames( + val count: Int, + val ratio: Double +) { + fun prettyPrint(description: String) { + println( + """ + Missed frames ($description): + - count: $count + - ratio: $ratio + + """.trimIndent() + ) + } + + fun putFormattedValuesTo(description: String, map: MutableMap) { + map.put("Missed frames ($description)", "$ratio") + } +} + +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, + val frameBudget: Duration, + val conditions: BenchmarkConditions, + val averageFrameInfo: FrameInfo?, + val averageFPSInfo: FPSInfo, + val percentileCPUAverage: List, + val percentileGPUAverage: List, + val noBufferingMissedFrames: MissedFrames, + val doubleBufferingMissedFrames: MissedFrames, + val startupTimeInfo: StartupTimeInfo? = null, +) { + fun prettyPrint() { + println("# $name") + val versionInfo = Config.versionInfo + if (versionInfo != null) { + println("Version: $versionInfo") + } + 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() + println() + } + if (Config.isModeEnabled(Mode.VSYNC_EMULATION)) { + percentileCPUAverage.prettyPrint(BenchmarkFrameTimeKind.CPU) + println() + percentileGPUAverage.prettyPrint(BenchmarkFrameTimeKind.GPU) + println() + noBufferingMissedFrames.prettyPrint("no buffering") + doubleBufferingMissedFrames.prettyPrint("double buffering") + } + if (Config.isModeEnabled(Mode.REAL)) { + averageFPSInfo.prettyPrint() + averageFrameInfo?.prettyPrint() + noBufferingMissedFrames.prettyPrint("estimation of real") + println() + } + } + + fun putFormattedValuesTo(map: MutableMap) { + val versionInfo = Config.versionInfo + if (versionInfo != null) { + 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) + } + if (Config.isModeEnabled(Mode.VSYNC_EMULATION)) { + percentileCPUAverage.putFormattedValuesTo(BenchmarkFrameTimeKind.CPU, map) + percentileGPUAverage.putFormattedValuesTo(BenchmarkFrameTimeKind.GPU, map) + noBufferingMissedFrames.putFormattedValuesTo("no buffering", map) + doubleBufferingMissedFrames.putFormattedValuesTo("double buffering", map) + } + if (Config.isModeEnabled(Mode.REAL)) { + averageFPSInfo.putFormattedValuesTo(map) + } + } + + private fun List.prettyPrint(kind: BenchmarkFrameTimeKind) { + forEach { + println("Worst p${(it.percentile * 100).roundToInt()} ${kind.toPrettyPrintString()} (ms): ${it.average}") + } + } + + private fun List.putFormattedValuesTo( + kind: BenchmarkFrameTimeKind, + map: MutableMap + ) { + forEach { + map.put( + "Worst p${(it.percentile * 100).roundToInt()} ${kind.toPrettyPrintString()} (ms)", + it.average.formatAsMilliseconds() + ) + } + } + + fun toJsonString(): String = json.encodeToString(serializer(), this) +} + +class BenchmarkResult( + private val name: String, + private val frameBudget: Duration, + private val conditions: BenchmarkConditions, + 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) + + val startIndex = ((frames.size - 1) * percentile).roundToInt() + + val percentileFrames = frames.sortedBy { it.duration(kind) }.subList(frames.size - startIndex - 1, frames.size) + + return averageDuration(percentileFrames) { + it.duration(kind) + } + } + + fun generateStats(): BenchmarkStats { + val noBufferingMissedFramesCount = frames.count { + it.cpuDuration + it.gpuDuration > frameBudget + } + + val doubleBufferingMissedFrames = frames.count { + maxOf(it.cpuDuration, it.gpuDuration) > frameBudget + } + + return BenchmarkStats( + name, + frameBudget, + conditions, + averageFrameInfo, + averageFPSInfo, + listOf(0.01, 0.02, 0.05, 0.1, 0.25, 0.5).map { percentile -> + val average = percentileAverageFrameTime(percentile, BenchmarkFrameTimeKind.CPU) + + BenchmarkPercentileAverage(percentile, average) + }, + listOf(0.01, 0.1, 0.5).map { percentile -> + val average = percentileAverageFrameTime(percentile, BenchmarkFrameTimeKind.GPU) + + BenchmarkPercentileAverage(percentile, average) + }, + MissedFrames(noBufferingMissedFramesCount, noBufferingMissedFramesCount / frames.size.toDouble()), + MissedFrames(doubleBufferingMissedFrames, doubleBufferingMissedFrames / frames.size.toDouble()), + startupTimeInfo = startupTimeInfo, + ) + } + + private fun averageDuration(frames: List, selector: (BenchmarkFrame) -> Duration): Duration = + frames.fold(Duration.ZERO) { acc, frame -> + acc + selector(frame) + } / frames.size + +} + +private fun Duration.formatAsMilliseconds(): String = (inWholeMicroseconds / 1000.0).toString() + +data class Benchmark( + val name: String, + val frameCount: Int, + val content: @Composable () -> Unit +) + +fun Benchmark(name: String, content: @Composable () -> Unit): Benchmark { + return Benchmark(name, Config.getBenchmarkProblemSize(name, Config.frameCount), content) +} + +expect val isSvgSupported: Boolean + +fun getBenchmarks(): List = listOf( + Benchmark("AnimatedVisibility") { AnimatedVisibility() }, + Benchmark("LazyGrid") { LazyGrid() }, + Benchmark("LazyGrid-ItemLaunchedEffect") { + LazyGrid(smoothScroll = false, withLaunchedEffectInItem = true) + }, + Benchmark("LazyGrid-SmoothScroll") { + LazyGrid(smoothScroll = true) + }, + Benchmark("LazyGrid-SmoothScroll-ItemLaunchedEffect") { + LazyGrid(smoothScroll = true, withLaunchedEffectInItem = true) + }, + Benchmark("VisualEffects") { + NYContent(1920, 1080) + }, + Benchmark("LazyList") { MainUiNoImageUseModel() }, + if (isSvgSupported) Benchmark("MultipleComponents") { MultipleComponentsExample(isVectorGraphicsSupported = true) } else null, + Benchmark("MultipleComponents-NoVectorGraphics") { + MultipleComponentsExample(isVectorGraphicsSupported = false) + }, + Benchmark("TextLayout") { TextLayout() }, + Benchmark("CanvasDrawing") { CanvasDrawing() }, + Benchmark("HeavyShader") { HeavyShader() } +).filterNotNull().sortedBy { it.name } + +suspend fun runBenchmark( + benchmark: Benchmark, + width: Int, + height: Int, + targetFps: Int, + graphicsContext: GraphicsContext?, + warmupCount: Int = Config.warmupCount, +) { + if (Config.isBenchmarkEnabled(benchmark.name)) { + val stats = measureComposable( + name = benchmark.name, + warmupCount = warmupCount, + frameCount = benchmark.frameCount, + width = width, + height = height, + targetFps = targetFps, + 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 && printJsonToOutput) { + println("JSON_START") + println(stats.toJsonString()) + println("JSON_END") + } + if (Config.saveStats()) { + saveBenchmarkStats(name = stats.name, stats = stats) + } +} + +suspend fun runBenchmarks( + benchmarks: List, + width: Int = 1920, + height: Int = 1080, + targetFps: Int = 120, + warmupCount: Int = Config.warmupCount, + graphicsContext: GraphicsContext? = null +) { + if (Config.listBenchmarks) { + println("AVAILABLE_BENCHMARKS_START") + benchmarks.forEach { println(it.name) } + println("AVAILABLE_BENCHMARKS_END") + return + } + println() + println("Running emulating $targetFps FPS") + println() + for (benchmark in benchmarks) { + runBenchmark(benchmark, width, height, targetFps, graphicsContext, warmupCount) + } +} + +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, + deviceFrameRate: Int, + onExit: () -> Unit +) { + var currentBenchmarkIndex by remember { mutableStateOf(0) } + 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 + + if (currentBenchmarkIndex == benchmarks.size) { + if (Config.reportAtTheEnd) { + results.forEach { it.prettyPrint() } + } + onExit() + 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 (phase != BenchmarkPhase.EMPTY_SCREEN) { + benchmark.content() + } + + 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 + } + BenchmarkPhase.WARMUP -> { + repeat(Config.warmupCount) { + withFrameNanos { } + } + phaseBeforeEmptyScreen = BenchmarkPhase.WARMUP + phase = BenchmarkPhase.EMPTY_SCREEN + } + BenchmarkPhase.EMPTY_SCREEN -> { + delay(Config.emptyScreenDelay.milliseconds) + withFrameNanos { } + 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") + } + } + 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 + } + } + } + } + } else { + LaunchedEffect(benchmark.name) { + currentBenchmarkIndex++ + } + } +} + +suspend fun runBenchmarks( + width: Int = 1920, + height: Int = 1080, + targetFps: Int = 120, + warmupCount: Int = Config.warmupCount, + graphicsContext: GraphicsContext? = null +) { + runBenchmarks(getBenchmarks(), width, height, targetFps, warmupCount, graphicsContext) +} diff --git a/compose/mpp/benchmarks/src/commonMain/kotlin/BenchmarksSave.kt b/compose/mpp/benchmarks/src/commonMain/kotlin/BenchmarksSave.kt new file mode 100644 index 0000000000000..8eb8e39ee81fc --- /dev/null +++ b/compose/mpp/benchmarks/src/commonMain/kotlin/BenchmarksSave.kt @@ -0,0 +1,92 @@ +/* + * Copyright 2020-2025 JetBrains s.r.o. and respective authors and developers. + * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. + */ + +import kotlinx.datetime.LocalDateTime +import kotlinx.datetime.TimeZone +import kotlinx.datetime.format.FormatStringsInDatetimeFormats +import kotlinx.datetime.format.byUnicodePattern +import kotlinx.datetime.toLocalDateTime +import kotlinx.io.IOException +import kotlinx.io.RawSink +import kotlinx.io.RawSource +import kotlinx.io.buffered +import kotlinx.io.files.Path +import kotlinx.io.files.SystemFileSystem +import kotlinx.io.readByteArray +import kotlin.time.Clock + +// port for the benchmarks save server +val BENCHMARK_SERVER_PORT = 8090 + +private val BENCHMARKS_SAVE_DIR = "build/benchmarks" +private fun pathToCsv(name: String) = Path("$BENCHMARKS_SAVE_DIR/$name.csv") +private fun pathToJson(name: String) = Path("$BENCHMARKS_SAVE_DIR/json-reports/$name.json") + +internal fun saveJson(benchmarkName: String, jsonString: String) { + val jsonPath = pathToJson(benchmarkName) + SystemFileSystem.createDirectories(jsonPath.parent!!) + SystemFileSystem.sink(jsonPath).writeText(jsonString) + println("JSON results saved to ${SystemFileSystem.resolve(jsonPath)}") +} + +fun saveBenchmarkStatsOnDisk(name: String, stats: BenchmarkStats) { + try { + if (Config.saveStatsToCSV) { + val path = pathToCsv(name) + + val keyToValue = mutableMapOf() + keyToValue.put("Date", currentFormattedDate) + stats.putFormattedValuesTo(keyToValue) + + var text = if (SystemFileSystem.exists(path)) { + SystemFileSystem.source(path).readText() + } else { + keyToValue.keys.joinToString(",") + "\n" + } + + fun escapeForCSV(value: String) = value.replace(",", ";") + text += keyToValue.values.joinToString(",", transform = ::escapeForCSV) + "\n" + + SystemFileSystem.createDirectories(path.parent!!) + SystemFileSystem.sink(path).writeText(text) + println("CSV results saved to ${SystemFileSystem.resolve(path)}") + println() + } else if (Config.saveStatsToJSON) { + saveJson(name, stats.toJsonString()) + println() + } + } catch (_: IOException) { + // IOException "Read-only file system" is thrown on iOS without writing permissions + } catch (_: UnsupportedOperationException) { + // UnsupportedOperationException is thrown in browser + } +} + +/** + * Saves benchmark statistics to disk or sends them to a server. + * This is an expect function with platform-specific implementations. + */ +expect fun saveBenchmarkStats(name: String, stats: BenchmarkStats) + +expect val printJsonToOutput: Boolean + +private fun RawSource.readText() = use { + it.buffered().readByteArray().decodeToString() +} + +internal fun RawSink.writeText(text: String) = use { + it.buffered().apply { + write(text.encodeToByteArray()) + flush() + } +} + +@OptIn(FormatStringsInDatetimeFormats::class) +private val currentFormattedDate: String get() { + val currentTime = Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault()) + return LocalDateTime.Format { + byUnicodePattern("dd-MM-yyyy HH:mm:ss") + }.format(currentTime) +} \ No newline at end of file diff --git a/compose/mpp/benchmarks/src/commonMain/kotlin/Config.kt b/compose/mpp/benchmarks/src/commonMain/kotlin/Config.kt new file mode 100644 index 0000000000000..ec5279e61b95b --- /dev/null +++ b/compose/mpp/benchmarks/src/commonMain/kotlin/Config.kt @@ -0,0 +1,236 @@ +enum class Mode { + SIMPLE, + VSYNC_EMULATION, + REAL, + STARTUP +} + +object Args { + + private fun argToSet(arg: String): Set = arg.substring(arg.indexOf('=') + 1) + .split(",").filter{!it.isEmpty()}.map{it.uppercase()}.toSet() + + + private fun argToMap(arg: String): Map = arg.substring(arg.indexOf('=') + 1) + .split(",").filter { !it.isEmpty() }.map { it.uppercase() }.associate { + if (it.contains('(') && it.contains(')')) { + it.substringBefore('(') to it.substringAfter('(').substringBefore(')').toInt() + } else { + it to -1 + } + } + + private fun String.decodeArg() = replace("%20", " ") + + /** + * Parses command line arguments to create a [Config] for benchmarks run. + * + * @param args an array of strings representing the command line arguments. + * Each argument can specify either of these settings: + * modes, benchmarks, disabledBenchmarks - comma separated values, + * versionInfo, saveStatsToCSV, saveStatsToJSON, parallel, warmupCount, frameCount, emptyScreenDelay, reportAtTheEnd, listBenchmarks - single values. + * + * Example: benchmarks=AnimatedVisibility(100),modes=SIMPLE,versionInfo=Kotlin_2_1_20,saveStatsToCSV=true,warmupCount=50,frameCount=100,emptyScreenDelay=2000,reportAtTheEnd=true + */ + fun parseArgs(args: Array): Config { + val modes = mutableSetOf() + val benchmarks = mutableMapOf() + val disabledBenchmarks = mutableSetOf() + var versionInfo: String? = null + var saveStatsToCSV: Boolean = false + var saveStatsToJSON: Boolean = false + var runServer: Boolean = false + var parallelRendering: Boolean = false + var warmupCount: Int? = null + var frameCount: Int? = null + 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)) { + modes.addAll(argToSet(arg.decodeArg()).map { Mode.valueOf(it) }) + } else if (arg.startsWith("benchmarks=", ignoreCase = true)) { + benchmarks += argToMap(arg.decodeArg()) + } else if (arg.startsWith("versionInfo=", ignoreCase = true)) { + versionInfo = arg.substringAfter("=").decodeArg() + } else if (arg.startsWith("saveStatsToCSV=", ignoreCase = true)) { + saveStatsToCSV = arg.substringAfter("=").toBoolean() + } else if (arg.startsWith("saveStatsToJSON=", ignoreCase = true)) { + saveStatsToJSON = arg.substringAfter("=").toBoolean() + } else if (arg.startsWith("disabledBenchmarks=", ignoreCase = true)) { + disabledBenchmarks += argToMap(arg.decodeArg()).keys + } else if (arg.startsWith("runServer=", ignoreCase = true)) { + runServer = arg.substringAfter("=").toBoolean() + } else if (arg.startsWith("parallel=", ignoreCase = true)) { + parallelRendering = arg.substringAfter("=").toBoolean() + } else if (arg.startsWith("warmupCount=", ignoreCase = true)) { + warmupCount = arg.substringAfter("=").toInt() + } else if (arg.startsWith("frameCount=", ignoreCase = true)) { + frameCount = arg.substringAfter("=").toInt() + } else if (arg.startsWith("emptyScreenDelay=", ignoreCase = true)) { + emptyScreenDelay = arg.substringAfter("=").toLong() + } else if (arg.startsWith("reportAtTheEnd=", ignoreCase = true)) { + 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( + modes = modes, + benchmarks = benchmarks, + disabledBenchmarks = disabledBenchmarks, + versionInfo = versionInfo, + saveStatsToCSV = saveStatsToCSV, + saveStatsToJSON = saveStatsToJSON, + runServer = runServer, + parallelRendering = parallelRendering, + warmupCount = warmupCount ?: defaultWarmupCount, + frameCount = frameCount ?: 1000, + emptyScreenDelay = emptyScreenDelay ?: 2000L, + reportAtTheEnd = reportAtTheEnd, + listBenchmarks = listBenchmarks, + startupFrameCount = startupFrameCount ?: 30, + startupLongestFramesCount = startupLongestFramesCount ?: 3 + ) + } +} + +/** + * Represents the benchmarks configuration parsed from command line arguments or configured programmatically. + * + * @property modes The set of enabled execution modes. If empty, all modes are considered enabled by default checks. + * @property benchmarks A map of explicitly mentioned benchmarks to their specific problem sizes. + * A value of -1 indicates the benchmark is enabled but should use its default size. + * If the map is empty, all benchmarks are considered enabled by default checks. + * @property disabledBenchmarks A set of benchmarks to skip. + * @property versionInfo Optional string containing version information. + * @property saveStatsToCSV Flag indicating whether statistics should be saved to a CSV file. + * @property saveStatsToJSON Flag indicating whether statistics should be saved to a JSON file. + * @property parallelRendering Flag indicating whether we should use prallelRenderin on iOS + * @property warmupCount Number of warmup frames to run before starting the benchmark. + * @property frameCount Number of frames to run for each benchmark. + * @property emptyScreenDelay Delay in milliseconds between warmup and benchmark. + * @property reportAtTheEnd Flag indicating whether we should report results at the end of all benchmarks. + * @property listBenchmarks Flag indicating whether we should print available benchmarks and exit. + */ +data class Config( + val modes: Set = emptySet(), + val benchmarks: Map = emptyMap(), // Name -> Problem Size (-1 for default) + val disabledBenchmarks: Set = emptySet(), + val versionInfo: String? = null, + val saveStatsToCSV: Boolean = false, + val saveStatsToJSON: Boolean = false, + val runServer: Boolean = false, + val parallelRendering: Boolean = false, + val warmupCount: Int = 100, + val frameCount: Int = 1000, + val emptyScreenDelay: Long = 2000L, + val reportAtTheEnd: 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.contains(mode) + + /** + * Checks if a specific benchmark is enabled + */ + fun isBenchmarkEnabled(benchmark: String): Boolean { + val normalizedName = benchmark.uppercase() + // Enabled if the benchmarks map is empty OR if the specific benchmark is present + return (benchmarks.isEmpty() || benchmarks.containsKey(normalizedName)) + && !disabledBenchmarks.contains(normalizedName) + && !disabledBenchmarks.contains(benchmark) + } + + /** + * Returns the problem size configured for [benchmark], or [default] if not set. + * + * @param benchmark Benchmark name (case-insensitive). + * @param default Fallback size when no configuration is found. + * @return The problem size to use. + */ + fun getBenchmarkProblemSize(benchmark: String, default: Int): Int { + val normalizedName = benchmark.uppercase() + val problemSize = benchmarks[normalizedName] ?: -1 + return if (problemSize == -1) default else problemSize + } + + companion object { + private var global: Config = Config() + + val versionInfo: String? + get() = global.versionInfo + + val saveStatsToCSV: Boolean + get() = global.saveStatsToCSV + + val saveStatsToJSON: Boolean + get() = global.saveStatsToJSON + + val runServer: Boolean + get() = global.runServer + + val parallelRendering: Boolean + get() = global.parallelRendering + + val warmupCount: Int + get() = global.warmupCount + + val frameCount: Int + get() = global.frameCount + + val emptyScreenDelay: Long + get() = global.emptyScreenDelay + + val reportAtTheEnd: Boolean + get() = global.reportAtTheEnd + + 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 + } + + fun setGlobalFromArgs(args: Array) { + this.global = Args.parseArgs(args) + } + + fun isModeEnabled(mode: Mode): Boolean = + global.isModeEnabled(mode) + + fun isBenchmarkEnabled(benchmark: String): Boolean = + global.isBenchmarkEnabled(benchmark) + + fun getBenchmarkProblemSize(benchmark: String, default: Int): Int = + global.getBenchmarkProblemSize(benchmark, default) + + fun saveStats() = saveStatsToCSV || saveStatsToJSON + } +} \ No newline at end of file diff --git a/compose/mpp/benchmarks/src/commonMain/kotlin/MeasureComposable.kt b/compose/mpp/benchmarks/src/commonMain/kotlin/MeasureComposable.kt new file mode 100644 index 0000000000000..b38d513eb8a4f --- /dev/null +++ b/compose/mpp/benchmarks/src/commonMain/kotlin/MeasureComposable.kt @@ -0,0 +1,174 @@ +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.asComposeCanvas +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.DurationUnit +import kotlin.time.TimeSource.Monotonic.markNow +import kotlin.time.measureTime + +const val nanosPerSecond = 1E9.toLong() + +interface GraphicsContext { + fun surface(width: Int, height: Int): Surface + + suspend fun awaitGPUCompletion() +} + +expect fun runGC() + +suspend inline fun preciseDelay(duration: Duration) { + val liveDelay: Duration + if (duration.inWholeMilliseconds > 2) { + //experiments have shown that for precise delay we should do live delay at least 2 ms + val delayMillis = duration.inWholeMilliseconds - 2 + val delayStart = markNow() + delay(delayMillis) + liveDelay = duration - delayStart.elapsedNow() + } else { + liveDelay = duration + } + val liveDelayStart = markNow() + while (liveDelayStart.elapsedNow() < liveDelay){} +} + +/** + * Some of the benchmarks involved an asynchronous fetch operation for resources when running in a browser. + * To let the fetch operation result be handled by compose, the benchmark loop must yield the event loop. + * Otherwise, such benchmark do not run a part of workload, making the stats less meaningful. + * + * It makes sense for all platforms, since there could be some work scheduled to run on the same Thread + * as the benchmarks runner. But without yielding, it won't be dispatched. + */ +private suspend inline fun yieldEventLoop() { + yield() +} + +@OptIn(ExperimentalTime::class) +internal suspend fun measureComposable( + name: String, + warmupCount: Int, + frameCount: Int, + width: Int, + height: Int, + targetFps: Int, + graphicsContext: GraphicsContext?, + content: @Composable () -> Unit +): BenchmarkResult { + val surface = graphicsContext?.surface(width, height) ?: Surface.makeNull(width, height) + val scene = BenchmarkComposeScene(width, height) + try { + val nanosPerFrame = (1.0 / targetFps.toDouble() * nanosPerSecond).toLong() + scene.setContent(content) + + // warmup + repeat(warmupCount) { + scene.mimicSkikoRender(surface, it * nanosPerFrame, width, height) + surface.flushAndSubmit(false) + graphicsContext?.awaitGPUCompletion() + yieldEventLoop() + } + + runGC() + + var cpuTotalTime = Duration.ZERO + var gpuTotalTime = Duration.ZERO + if (Config.isModeEnabled(Mode.SIMPLE)) { + cpuTotalTime = measureTime { + repeat(frameCount) { + scene.mimicSkikoRender(surface, it * nanosPerFrame, width, height) + surface.flushAndSubmit(false) + gpuTotalTime += measureTime { + graphicsContext?.awaitGPUCompletion() + } + yieldEventLoop() + } + } + cpuTotalTime -= gpuTotalTime + } + + val frames = MutableList(frameCount) { + BenchmarkFrame(Duration.INFINITE, Duration.INFINITE) + } + + if (Config.isModeEnabled(Mode.VSYNC_EMULATION)) { + var nextVSync = Duration.ZERO + var missedFrames = 0; + + runGC() + + val start = markNow() + + repeat(frameCount) { + val frameStart = start + nextVSync + + scene.mimicSkikoRender(surface, it * nextVSync.inWholeNanoseconds, width, height) + surface.flushAndSubmit(false) + + val cpuTime = frameStart.elapsedNow() + + graphicsContext?.awaitGPUCompletion() + + val frameTime = frameStart.elapsedNow() + + frames[it] = BenchmarkFrame(cpuTime, frameTime - cpuTime) + + 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 + preciseDelay(timeUntilNextVSync) + } + + yieldEventLoop() + } + } + + return BenchmarkResult( + name, + nanosPerFrame.nanoseconds, + BenchmarkConditions(frameCount, warmupCount), + FrameInfo(cpuTotalTime / frameCount, gpuTotalTime / frameCount), + FPSInfo(frameCount.toDouble() / (cpuTotalTime + gpuTotalTime).toDouble(DurationUnit.SECONDS)), + frames, + ) + } finally { + scene.close() + surface.close() + runGC() // cleanup for next benchmarks + } +} + +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 a simplified logic and it still can differ from the real cases: + * - Rendering into an intermediate picture. Benchmarks can show incorrect results without it. + * For example, we had a case when they showed an improvement by 10%, + * but there was a regression by 10% + * - Clearing the canvas each frame + * + * 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. + */ +private fun BenchmarkComposeScene.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() +} diff --git a/compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/animation/AnimatedVisibility.kt b/compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/animation/AnimatedVisibility.kt new file mode 100644 index 0000000000000..5b802922a61bb --- /dev/null +++ b/compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/animation/AnimatedVisibility.kt @@ -0,0 +1,47 @@ +package benchmarks.animation + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.EnterExitState +import androidx.compose.animation.core.Transition +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.runtime.withFrameNanos +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import compose_benchmarks.benchmarks.generated.resources.Res +import compose_benchmarks.benchmarks.generated.resources.compose_multiplatform +import org.jetbrains.compose.resources.ExperimentalResourceApi +import org.jetbrains.compose.resources.painterResource + +@OptIn(ExperimentalResourceApi::class) +@Composable +fun AnimatedVisibility() { + MaterialTheme { + var showImage by remember { mutableStateOf(false) } + var transition: Transition? = null + LaunchedEffect(showImage) { + do { + withFrameNanos {} + } while (transition?.isRunning == true) + + showImage = !showImage + } + Column(Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) { + AnimatedVisibility(showImage) { + transition = this.transition + Image( + painterResource(Res.drawable.compose_multiplatform), + null + ) + } + } + } +} \ No newline at end of file diff --git a/compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/canvasdrawing/CanvasDrawing.kt b/compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/canvasdrawing/CanvasDrawing.kt new file mode 100644 index 0000000000000..bad963ec2ddae --- /dev/null +++ b/compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/canvasdrawing/CanvasDrawing.kt @@ -0,0 +1,159 @@ +/* + * Copyright 2020-2026 JetBrains s.r.o. and respective authors and developers. + * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. + */ + +package benchmarks.canvasdrawing + +import androidx.compose.animation.core.LinearEasing +import androidx.compose.animation.core.RepeatMode +import androidx.compose.animation.core.animateFloat +import androidx.compose.animation.core.infiniteRepeatable +import androidx.compose.animation.core.rememberInfiniteTransition +import androidx.compose.animation.core.tween +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.background +import androidx.compose.foundation.gestures.scrollBy +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.runtime.withFrameMillis +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Path +import androidx.compose.ui.unit.dp +import kotlin.math.PI +import kotlin.math.cos +import kotlin.math.sin +import kotlin.random.Random +import kotlinx.coroutines.isActive + +private const val ITEM_COUNT = 1200 + +@Composable +fun CanvasDrawing() { + val listState = rememberLazyListState() + var scrollForward by remember { mutableStateOf(true) } + + LaunchedEffect(Unit) { + while (isActive) { + withFrameMillis { } + val currentItem = listState.firstVisibleItemIndex + if (currentItem == 0) scrollForward = true + if (currentItem > ITEM_COUNT - 100) scrollForward = false + listState.scrollBy(if (scrollForward) 33f else -33f) + } + } + + LazyColumn( + state = listState, + modifier = Modifier.fillMaxSize().background(Color.Black) + ) { + items(ITEM_COUNT) { index -> + CanvasDrawingItem(index) + } + } +} + +@Composable +private fun CanvasDrawingItem(index: Int) { + val animatedValue by rememberInfiniteTransition().animateFloat( + initialValue = 0f, + targetValue = 360f, + animationSpec = infiniteRepeatable( + animation = tween(3000, easing = LinearEasing), + repeatMode = RepeatMode.Restart + ) + ) + + Canvas( + modifier = Modifier + .fillMaxWidth() + .height(300.dp) + .padding(8.dp) + ) { + val seed = index * 12345L + val random = Random(seed) + + drawRect( + brush = Brush.verticalGradient( + colors = listOf( + Color(random.nextInt(0xFFFFFF) or 0xFF000000.toInt()), + Color(random.nextInt(0xFFFFFF) or 0xFF000000.toInt()) + ) + ) + ) + + repeat(200) { i -> + val angle = animatedValue + i * 7.2f + val radius = size.minDimension / 4f + val x = size.width / 2 + cos(angle * PI / 180.0).toFloat() * radius * random.nextFloat() + val y = size.height / 2 + sin(angle * PI / 180.0).toFloat() * radius * random.nextFloat() + + val path = Path().apply { + moveTo(x, y) + repeat(40) { j -> + val pointAngle = angle + j * 45f + animatedValue + val pointRadius = 20f + random.nextFloat() * 30f + val px = x + cos(pointAngle * PI / 180.0).toFloat() * pointRadius + val py = y + sin(pointAngle * PI / 180.0).toFloat() * pointRadius + if (j % 2 == 0) { + lineTo(px, py) + } else { + quadraticBezierTo( + x + random.nextFloat() * 50f - 25f, + y + random.nextFloat() * 50f - 25f, + px, py + ) + } + } + close() + } + + drawPath( + path = path, + brush = Brush.radialGradient( + colors = listOf( + Color(random.nextInt(0xFFFFFF) or 0x80000000.toInt()), + Color(random.nextInt(0xFFFFFF) or 0x40000000.toInt()) + ), + center = Offset(x, y) + ) + ) + } + + repeat(70) { i -> + val lineY = i * (size.height / 30) + drawLine( + color = Color(random.nextInt(0xFFFFFF) or 0xFF000000.toInt()), + start = Offset(0f, lineY), + end = Offset(size.width, lineY + sin(animatedValue * i).toFloat() * 20f), + strokeWidth = 2f + ) + } + + repeat(70) { i -> + val circleX = (i % 8) * (size.width / 8) + (size.width / 16) + val circleY = (i / 8) * (size.height / 5) + (size.height / 10) + val circleRadius = 15f + sin(animatedValue + i).toFloat() * 10f + + drawCircle( + color = Color(random.nextInt(0xFFFFFF) or 0xFF000000.toInt()), + radius = circleRadius, + center = Offset(circleX, circleY), + alpha = 0.7f + ) + } + } +} diff --git a/compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/complexlazylist/components/MainUI.kt b/compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/complexlazylist/components/MainUI.kt new file mode 100644 index 0000000000000..47b05d6a528cf --- /dev/null +++ b/compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/complexlazylist/components/MainUI.kt @@ -0,0 +1,110 @@ +package benchmarks.complexlazylist.components + +import androidx.compose.foundation.gestures.scrollBy +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material.MaterialTheme +import androidx.compose.runtime.* +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import benchmarks.complexlazylist.components.refresh.LOADING_MORE +import benchmarks.complexlazylist.components.refresh.LoadingIndicatorDefault +import benchmarks.complexlazylist.components.refresh.NORMAL +import benchmarks.complexlazylist.components.refresh.REFRESHING +import benchmarks.complexlazylist.components.refresh.SwipeRefreshLayout +import benchmarks.complexlazylist.components.refresh.rememberSwipeRefreshState +import benchmarks.complexlazylist.models.IBaseViewModel +import benchmarks.complexlazylist.models.ICompositionModel +import benchmarks.complexlazylist.models.fetchCompositionModels + +// just for demo +internal var models: MutableList = mutableStateListOf() + +@Composable +fun MainUiNoImageUseModel() { + MaterialTheme { + MainLazyColumnItemsList(noImage = true, useJson = false) + DisposableEffect(Unit) { + onDispose { + models.clear() + } + } + } +} + +@Composable +fun MainLazyColumnItemsList(noImage: Boolean, useJson: Boolean) { + val scope = rememberCoroutineScope() + val state = rememberSwipeRefreshState(NORMAL) + + LaunchedEffect(scope) { + scope.launch(Dispatchers.Default) { + fetchCompositionModels(false) { list -> + for (item in list) + models.add(item) + } + } + } + + SwipeRefreshLayout( + state = state, + indicator = { modifier, s, indicatorHeight -> + LoadingIndicatorDefault(modifier, s, indicatorHeight) + }, + onRefresh = { + scope.launch { + state.loadState = REFRESHING + delay(2000) + fetchCompositionModels(useJson) { + models.clear() + for (item in it) + models.add(item) + state.loadState = NORMAL + } + } + + }, + onLoadMore = { + scope.launch { + state.loadState = LOADING_MORE + delay(2000L) + fetchCompositionModels(useJson) { + for (item in it) + models.add(item) + state.loadState = NORMAL + } + } + } + ) { modifier -> + run { + val state = rememberLazyListState() + LaunchedEffect(Unit) { + while (true) { + withFrameMillis { } + state.scrollBy(20f) + } + } + LazyColumn(modifier, state = state) { + itemsIndexed( + items = models, + key = { index, _ -> + index + } + ) { _, item -> + when (item) { + is ICompositionModel -> MultiCellUI(item) + // .. todo need more types + else -> throw RuntimeException("Unexpected") + } + } + } + } + } +} + +@Composable +fun MultiCellUI(item: IBaseViewModel) { + DecoratedCell(item) +} \ No newline at end of file diff --git a/compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/complexlazylist/components/MultiLevelCell.kt b/compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/complexlazylist/components/MultiLevelCell.kt new file mode 100644 index 0000000000000..77ef62ce5ecbe --- /dev/null +++ b/compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/complexlazylist/components/MultiLevelCell.kt @@ -0,0 +1,351 @@ +package benchmarks.complexlazylist.components + +import androidx.compose.animation.core.animate +import androidx.compose.animation.core.animateDpAsState +import androidx.compose.animation.core.tween +import androidx.compose.foundation.* +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.MaterialTheme.typography +import androidx.compose.material.Text +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.scale +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.graphics.painter.ColorPainter +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import benchmarks.complexlazylist.models.IBaseViewModel +import benchmarks.complexlazylist.models.ICompositionItem +import benchmarks.complexlazylist.models.ICompositionModel +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.launch + +@Composable +internal fun DecoratedCell(model: IBaseViewModel) { + Column( + modifier = Modifier + .fillMaxWidth() + .border(1.dp, Color.LightGray, RoundedCornerShape(4.dp)) + .background( + brush = Brush.linearGradient( + colors = listOf( + Color.LightGray.copy(1f), + Color.LightGray.copy(0.5f), + Color.LightGray.copy(0f) + ), + start = Offset(320f, 0f), + end = Offset(320f, 164f) + ) + ), + verticalArrangement = Arrangement.spacedBy((-60).dp) + ) { + CompositionView(model) + Box( + modifier = Modifier.fillMaxWidth() + .heightIn(164.dp), + contentAlignment = Alignment.BottomCenter, + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .heightIn(64.dp) + .padding(8.dp) + .border(1.dp, Color.Cyan.copy(0.3f), RoundedCornerShape(4.dp)) + .padding(8.dp) + .background(color = Color.Cyan.copy(alpha = 0.1f)) + ) { + + Avatar( + modifier = Modifier + .padding(4.dp) + .align(Alignment.CenterVertically) + ) + + TextInfo( + Modifier + .weight(1f) + .align(Alignment.CenterVertically) + ) + + FollowBtn( + Modifier.align(Alignment.CenterVertically) + ) + } + } + } +} + +@Composable +internal fun CompositionView(model: IBaseViewModel) { + if (model !is ICompositionModel) + return + Box( + modifier = Modifier.clip(RoundedCornerShape(10.dp)).background(Color.White).padding(10.dp) + ) { + model.overlyTopLeft?.let { overlyTopLeft(it) } + model.overlyView1?.let { overlyView1(it) } + model.overlyView2?.let { overlyView2(it) } + model.overlyView3?.let { overlyView3(it) } + model.overlyTopRight?.let { overlyTopRight(it) } + model.label?.let { overlyLabel(it) } + } +} + +@Composable +internal fun overlyTopLeft(compositionItem: ICompositionItem) { + compositionItem.bgColor?.let { Color.DarkGray }?.let { ColorPainter(it) }?.let { + compositionItem.alpha?.toFloat()?.let { it1 -> + compositionItem.radius?.toFloat()?.let { RoundedCornerShape(it.dp) }?.let { it2 -> + Modifier + .padding(start = 0.dp, top = 0.dp) + .width(50.dp) + .height(50.dp) + .alpha(it1) + .clip(shape = it2) + .shadow(3.dp) + } + }?.let { it2 -> + Image( + painter = it, + modifier = it2, + contentDescription = null + ) + } + } +} + +@Composable +internal fun overlyView1(compositionItem: ICompositionItem) { + compositionItem.bgColor?.let { Color.Red }?.let { ColorPainter(it) }?.let { + compositionItem.alpha?.toFloat()?.let { it1 -> + compositionItem.radius?.toFloat()?.let { RoundedCornerShape(it.dp) }?.let { it2 -> + Modifier + .padding(start = 20.dp, top = 20.dp) + .width(150.dp) + .height(100.dp) + .alpha(it1) + .clip(shape = it2) + .shadow(3.dp) + } + }?.let { it2 -> + Image( + painter = it, + modifier = it2, + contentDescription = null + ) + } + } +} + +@Composable +internal fun overlyView2(compositionItem: ICompositionItem) { + compositionItem.bgColor?.let { Color.Yellow }?.let { ColorPainter(it) }?.let { + compositionItem.alpha?.toFloat()?.let { it1 -> + compositionItem.radius?.toFloat()?.let { RoundedCornerShape(it.dp) }?.let { it2 -> + Modifier + .padding(start = 100.dp, top = 50.dp) + .width(150.dp) + .height(100.dp) + .alpha(it1) + .clip(shape = it2) + .shadow(3.dp) + } + }?.let { it2 -> + Image( + painter = it, + modifier = it2, + contentDescription = null + ) + } + } +} + +@Composable +internal fun overlyView3(compositionItem: ICompositionItem) { + compositionItem.bgColor?.let { Color.Green }?.let { ColorPainter(it) }?.let { + compositionItem.alpha?.toFloat()?.let { it1 -> + compositionItem.radius?.toFloat()?.let { RoundedCornerShape(it.dp) }?.let { it2 -> + Modifier + .padding(start = 110.dp, top = 60.dp) + .width(130.dp) + .height(80.dp) + .alpha(it1) + .clip(shape = it2) + .shadow(3.dp) + } + }?.let { it2 -> + Image( + painter = it, + modifier = it2, + contentDescription = null + ) + } + } +} + +@Composable +internal fun overlyTopRight(compositionItem: ICompositionItem) { + compositionItem.bgColor?.let { Color.LightGray }?.let { ColorPainter(it) }?.let { + compositionItem.alpha?.toFloat()?.let { it1 -> + compositionItem.radius?.toFloat()?.let { RoundedCornerShape(it.dp) }?.let { it2 -> + Modifier + .padding(start = 160.dp, top = 15.dp) + .width(90.dp) + .height(60.dp) + .alpha(it1) + .clip(shape = it2) + .shadow(3.dp) + } + }?.let { it2 -> + Image( + painter = it, + modifier = it2, + contentDescription = null + ) + } + } +} + +@Composable +internal fun overlyLabel(compositionItem: ICompositionItem) { + Box(modifier = Modifier.fillMaxSize().padding(), contentAlignment = Alignment.TopCenter) { + compositionItem.text?.let { + Text( + text = it, + modifier = Modifier.background(Color.Black.copy(0.5f)).alpha(0.5f) + .padding(start = 10.dp, end = 10.dp).width(200.dp).height(100.dp), + color = Color.Cyan + ) + } + } +} + +@Composable +internal fun Avatar(modifier: Modifier) { + Image( + modifier = modifier + .size(50.dp) + .clip(CircleShape) + .border( + shape = CircleShape, + border = BorderStroke( + width = 2.dp, + brush = Brush.linearGradient( + colors = listOf(Color.Blue, Color.Yellow, Color.Green, Color.Red), + start = Offset(0f, 0f), + end = Offset(100f, 100f) + ) + ) + ) + .border( + shape = CircleShape, + border = BorderStroke(4.dp, SolidColor(Color.White)) + ), + painter = ColorPainter(color = Color.Gray), + contentDescription = null, // decorative + ) +} + +@Composable +internal fun TextInfo(modifier: Modifier) { + Column(modifier = modifier) { + Text( + text = "name", + color = Color.Black, + maxLines = 1, + style = TextStyle( + fontWeight = FontWeight.Bold, + fontSize = 16.sp, + letterSpacing = 0.15.sp + ) + ) + Text( + text = "desc", + color = Color.Black.copy(alpha = 0.75f), + maxLines = 1, + style = TextStyle( // here + fontWeight = FontWeight.Normal, + fontSize = 14.sp, + letterSpacing = 0.25.sp + ) + ) + } +} + +@Composable +internal fun FollowBtn(modifier: Modifier) { + var checked by remember { mutableStateOf(false) } + val backgroundShape: Shape = remember { RoundedCornerShape(4.dp) } + var clickCount by remember { mutableStateOf(1) } + val offsetX by animateDpAsState( + if (checked) (-80).dp else (-10).dp, + animationSpec = tween(durationMillis = 3000) + ) + + var alpha by remember { mutableStateOf(1f) } + var scale by remember { mutableStateOf(1f) } + val scope = rememberCoroutineScope() + Row( + horizontalArrangement = Arrangement.spacedBy(10.dp), + verticalAlignment = Alignment.CenterVertically, + modifier = modifier.offset(x = offsetX), + ) { + Text( + text = if (checked) "unfollow" else "follow", + style = typography.body1.copy(color = Color.White), + textAlign = TextAlign.Center, + modifier = modifier + .width(100.dp) + .clickable(onClick = { + clickCount++ + checked = !checked + scope.launch { + coroutineScope { + launch { + animate(1f, 0.5f) { value, _ -> alpha = value } + } + launch { + animate(1f, 0.5f) { value, _ -> scale = value } + } + } + coroutineScope { + launch { + animate(0.5f, 1f) { value, _ -> alpha = value } + } + launch { + animate(0.5f, 1f) { value, _ -> scale = value } + } + } + } + }) + .shadow(5.dp, shape = backgroundShape) + .clip(backgroundShape) + .background( + brush = Brush.verticalGradient( + colors = listOf( + Color.Red, + Color.Yellow, + ), + startY = 0f, + endY = 80f + ) + ) + .padding(6.dp) + .scale(scale) + .alpha(alpha) + ) + } +} diff --git a/compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/complexlazylist/components/refresh/LoadingIndicator.kt b/compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/complexlazylist/components/refresh/LoadingIndicator.kt new file mode 100644 index 0000000000000..359f4ea74fd67 --- /dev/null +++ b/compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/complexlazylist/components/refresh/LoadingIndicator.kt @@ -0,0 +1,159 @@ +package benchmarks.complexlazylist.components.refresh + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.LinearEasing +import androidx.compose.animation.core.RepeatMode +import androidx.compose.animation.core.animate +import androidx.compose.animation.core.infiniteRepeatable +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.rotate +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.StrokeCap +import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.max + +@Composable +internal fun BoxScope.LoadingIndicatorDefault( + modifier: Modifier, + state: SwipeRefreshState, + indicatorHeight: Dp +) { + val height = max(30.dp, with(LocalDensity.current) { + state.progress.offset.toDp() + }) + var prev by remember { mutableStateOf(Offset(0f, 0f)) } + var prevOffsetAngle by remember { mutableStateOf(0f) } + var offsetAngle by remember { mutableStateOf(0f) } + var startAngle by remember { mutableStateOf(prev.x) } + var endAngle by remember { mutableStateOf(prev.y) } + + LaunchedEffect(state.loadState == REFRESHING || state.loadState == LOADING_MORE) { + if (!(state.loadState == REFRESHING || state.loadState == LOADING_MORE)) { + prevOffsetAngle = offsetAngle + return@LaunchedEffect + } + animate( + initialValue = prevOffsetAngle, + targetValue = prevOffsetAngle + 360f, + animationSpec = infiniteRepeatable( + animation = tween(5000, easing = LinearEasing), + repeatMode = RepeatMode.Restart + ) + ) { value, _ -> + offsetAngle = value + } + } + + LaunchedEffect(state.loadState == REFRESHING || state.loadState == LOADING_MORE, endAngle) { + if (!(state.loadState == REFRESHING || state.loadState == LOADING_MORE)) return@LaunchedEffect + if ((endAngle - startAngle).toInt() == 270) { + animate( + initialValue = startAngle, + targetValue = endAngle - 10f, + animationSpec = tween(durationMillis = 700) + ) { value, _ -> + startAngle = value + } + } + } + + LaunchedEffect(state.loadState == REFRESHING || state.loadState == LOADING_MORE, startAngle) { + if (!(state.loadState == REFRESHING || state.loadState == LOADING_MORE)) if (!(state.loadState == REFRESHING || state.loadState == LOADING_MORE)) { + prev = Offset(startAngle, endAngle) + return@LaunchedEffect + } + if ((endAngle - startAngle).toInt() == 10 + || endAngle.toInt() == prev.y.toInt() + ) { + animate( + initialValue = endAngle, + targetValue = startAngle + 270f, + animationSpec = tween(durationMillis = 700), + ) { value, _ -> + endAngle = value + } + } + } + + Box( + modifier + .fillMaxWidth() + .height(height), contentAlignment = Alignment.Center + ) { + if (state.isSwipeInProgress) { + if (state.progress.offset <= with(LocalDensity.current) { indicatorHeight.toPx() }) { + Text(text = if (state.progress.location == TOP) "下拉刷新" else "上拉加载更多") + } else { + Text(text = if (state.progress.location == TOP) "松开刷新" else "松开加载更多") + } + } else { + AnimatedVisibility( + visible = state.loadState == REFRESHING || state.loadState == LOADING_MORE, + modifier = Modifier.align(Alignment.Center), + enter = fadeIn(), + exit = fadeOut() + ) { + Row( + modifier = Modifier + .align(alignment = Alignment.Center), + horizontalArrangement = Arrangement.SpaceEvenly + ) { + Box( + modifier = Modifier + .padding(10.dp) + .height(20.dp).width(20.dp) + .aspectRatio(1f) + .rotate(offsetAngle) + ) { + Canvas( + modifier = Modifier.height(20.dp).width(20.dp) + ) { + drawArc( + color = Color.Gray, + startAngle = startAngle, + sweepAngle = endAngle - startAngle, + useCenter = false, + style = Stroke(width = 5f, cap = StrokeCap.Round) + ) + } + } + + Text( + modifier = Modifier.padding(10.dp), + text = "Loading...", + color = Color.Gray, + fontWeight = FontWeight.W500, + textAlign = TextAlign.Center + ) + } + } + } + } +} \ No newline at end of file diff --git a/compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/complexlazylist/components/refresh/SwipeProgress.kt b/compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/complexlazylist/components/refresh/SwipeProgress.kt new file mode 100644 index 0000000000000..3c8e0486d8762 --- /dev/null +++ b/compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/complexlazylist/components/refresh/SwipeProgress.kt @@ -0,0 +1,17 @@ +package benchmarks.complexlazylist.components.refresh + +import androidx.compose.runtime.Immutable + +internal const val NONE = 0 + +internal const val TOP = 1 + +internal const val BOTTOM = 2 + +@Immutable +internal data class SwipeProgress( + val location: Int = NONE, + val offset: Float = 0f, + /*@FloatRange(from = 0.0, to = 1.0)*/ + val fraction: Float = 0f +) diff --git a/compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/complexlazylist/components/refresh/SwipeRefreshLayout.kt b/compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/complexlazylist/components/refresh/SwipeRefreshLayout.kt new file mode 100644 index 0000000000000..6d9142da34ffd --- /dev/null +++ b/compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/complexlazylist/components/refresh/SwipeRefreshLayout.kt @@ -0,0 +1,121 @@ +package benchmarks.complexlazylist.components.refresh + +import androidx.compose.animation.core.animate +import androidx.compose.animation.core.tween +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import kotlin.math.min + +@Composable +internal fun SwipeRefreshLayout( + state: SwipeRefreshState, + onRefresh: () -> Unit, + onLoadMore: () -> Unit, + modifier: Modifier = Modifier, + refreshTriggerDistance: Dp = 120.dp, + indicationHeight: Dp = 60.dp, + refreshEnabled: Boolean = true, + loadMoreEnabled: Boolean = true, + indicator: @Composable BoxScope.(modifier: Modifier, state: SwipeRefreshState, indicatorHeight: Dp) -> Unit = { m, s, height -> + LoadingIndicatorDefault(m, s, height) + }, + content: @Composable (modifier: Modifier) -> Unit, +) { + val refreshTriggerPx = with(LocalDensity.current) { refreshTriggerDistance.toPx() } + val indicationHeightPx = with(LocalDensity.current) { indicationHeight.toPx() } + + // Our LaunchedEffect, which animates the indicator to its resting position + LaunchedEffect(state.isSwipeInProgress) { + if (!state.isSwipeInProgress) { + // If there's not a swipe in progress, rest the indicator at 0f + state.animateOffsetTo(0f) + } + } + + val coroutineScope = rememberCoroutineScope() + val updatedOnRefresh = rememberUpdatedState(onRefresh) + val updatedOnLoadMore = rememberUpdatedState(onLoadMore) + + val nestedScrollConnection = remember(state, coroutineScope) { + SwipeRefreshNestedScrollConnection( + state, + coroutineScope, + onRefresh = { updatedOnRefresh.value.invoke() }, + onLoadMore = { updatedOnLoadMore.value.invoke() } + ) + }.apply { + this.refreshEnabled = refreshEnabled + this.loadMoreEnabled = loadMoreEnabled + this.refreshTrigger = refreshTriggerPx + this.indicatorHeight = indicationHeightPx + } + + BoxWithConstraints(modifier.nestedScroll(connection = nestedScrollConnection)) { + if (!state.isSwipeInProgress) + LaunchedEffect((state.loadState == REFRESHING || state.loadState == LOADING_MORE)) { + animate( + animationSpec = tween(durationMillis = 300), + initialValue = state.progress.offset, + targetValue = when (state.loadState) { + LOADING_MORE -> indicationHeightPx + REFRESHING -> indicationHeightPx + else -> 0f + } + ) { value, _ -> + if (!state.isSwipeInProgress) { + state.progress = state.progress.copy( + offset = value, + fraction = min(1f, value / refreshTriggerPx) + ) + } + } + } + + val offsetDp = with(LocalDensity.current) { + state.progress.offset.toDp() + } + content( + when (state.progress.location) { + TOP -> Modifier.padding(top = offsetDp) + BOTTOM -> Modifier.padding(bottom = offsetDp) + else -> Modifier + } + ) + if (state.progress.location != NONE) { + Box(modifier = Modifier + .fillMaxWidth() + .height(refreshTriggerDistance) + .graphicsLayer { + translationY = + if (state.progress.location == LOADING_MORE) constraints.maxHeight - state.progress.offset + else state.progress.offset - refreshTriggerPx + } + ) { + indicator( + Modifier.align(if (state.progress.location == TOP) Alignment.BottomStart else Alignment.TopStart), + state, + indicationHeight + ) + } + } + } +} + +@Composable +internal fun rememberSwipeRefreshState(state: Int) = remember { SwipeRefreshState(state) } \ No newline at end of file diff --git a/compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/complexlazylist/components/refresh/SwipeRefreshNestedScrollConnection.kt b/compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/complexlazylist/components/refresh/SwipeRefreshNestedScrollConnection.kt new file mode 100644 index 0000000000000..9ce64478ad945 --- /dev/null +++ b/compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/complexlazylist/components/refresh/SwipeRefreshNestedScrollConnection.kt @@ -0,0 +1,134 @@ +package benchmarks.complexlazylist.components.refresh + +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.input.nestedscroll.NestedScrollConnection +import androidx.compose.ui.input.nestedscroll.NestedScrollSource +import androidx.compose.ui.unit.Velocity +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import kotlin.math.abs +import kotlin.math.absoluteValue +import kotlin.math.roundToInt + +internal class SwipeRefreshNestedScrollConnection( + private val state: SwipeRefreshState, + private val coroutineScope: CoroutineScope, + private val onRefresh: () -> Unit, + private val onLoadMore: () -> Unit +) : NestedScrollConnection { + var refreshEnabled: Boolean = false + var loadMoreEnabled: Boolean = false + var refreshTrigger: Float = 100f + var indicatorHeight: Float = 50f + + private var isTop = false + private var isBottom = false + + override fun onPreScroll( + available: Offset, + source: NestedScrollSource + ): Offset = when { + !refreshEnabled && !loadMoreEnabled -> Offset.Zero + state.loadState != NORMAL -> Offset.Zero + source == NestedScrollSource.Drag -> { + if (available.y > 0 && isBottom) { + onScroll(available) + } else if (available.y < 0 && isTop) { + onScroll(available) + } else { + Offset.Zero + } + } + else -> Offset.Zero + } + + override fun onPostScroll( + consumed: Offset, + available: Offset, + source: NestedScrollSource + ): Offset { + + if (!refreshEnabled && !loadMoreEnabled) { + return Offset.Zero + } + + else if (state.loadState != NORMAL) { + return Offset.Zero + } else if (source == NestedScrollSource.Drag) { + if (available.y < 0) { + if (!isBottom) { + isBottom = true + } + if (isBottom) { + return onScroll(available) + } + + } else if (available.y > 0) { + if (!isTop) { + isTop = true + } + if (isTop) { + return onScroll(available) + } + } + } + return Offset.Zero + } + + private fun onScroll(available: Offset): Offset { + if (!isBottom && !isTop) { + return Offset.Zero + } + if (available.y > 0 && isTop) { + state.isSwipeInProgress = true + } else if (available.y < 0 && isBottom) { + state.isSwipeInProgress = true + } else if (state.indicatorOffset.roundToInt() == 0) { + state.isSwipeInProgress = false + } + + val newOffset = (available.y + state.indicatorOffset).let { + if (isTop) it.coerceAtLeast(0.0F) else it.coerceAtMost(0.0F) + } + val dragConsumed = newOffset - state.indicatorOffset + + return if (dragConsumed.absoluteValue >= 0.5f) { + coroutineScope.launch { + state.dispatchScrollDelta( + dragConsumed, + if (isTop) TOP else BOTTOM, + refreshTrigger, + ) + } + // Return the consumed Y + Offset(x = 0f, y = dragConsumed) + } else { + Offset.Zero + } + } + + override suspend fun onPreFling(available: Velocity): Velocity { + // If we're dragging, not currently refreshing and scrolled + // past the trigger point, refresh! + if (state.loadState == NORMAL && abs(state.indicatorOffset) >= indicatorHeight) { + if (isTop) { + onRefresh() + } else if (isBottom) { + onLoadMore() + } + } + + // Reset the drag in progress state + state.isSwipeInProgress = false + + // Don't consume any velocity, to allow the scrolling layout to fling + return Velocity.Zero + } + + override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity { + return Velocity.Zero.also { + isTop = false + isBottom = false + } + } +} \ No newline at end of file diff --git a/compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/complexlazylist/components/refresh/SwipeRefreshState.kt b/compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/complexlazylist/components/refresh/SwipeRefreshState.kt new file mode 100644 index 0000000000000..5194d64a57000 --- /dev/null +++ b/compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/complexlazylist/components/refresh/SwipeRefreshState.kt @@ -0,0 +1,79 @@ +package benchmarks.complexlazylist.components.refresh + +import androidx.compose.animation.core.Animatable +import androidx.compose.foundation.MutatePriority +import androidx.compose.foundation.MutatorMutex +import androidx.compose.runtime.Stable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import kotlin.math.abs +import kotlin.math.min + +internal const val NORMAL = 0 + + +internal const val REFRESHING = 1 + + +internal const val LOADING_MORE = 2 + +@Stable +internal class SwipeRefreshState( + loadState: Int, +) { + private val _indicatorOffset = Animatable(0f) + private val mutatorMutex = MutatorMutex() + + var loadState: Int by mutableStateOf(loadState) + + /** + * Whether a swipe/drag is currently in progress. + */ + var isSwipeInProgress: Boolean by mutableStateOf(false) + internal set + + var progress: SwipeProgress by mutableStateOf(SwipeProgress()) + internal set + + /** + * The current offset for the indicator, in pixels. + */ + internal val indicatorOffset: Float get() = _indicatorOffset.value + + internal suspend fun animateOffsetTo( + offset: Float, + ) { + mutatorMutex.mutate { + _indicatorOffset.animateTo(offset) + } + } + + /** + * Dispatch scroll delta in pixels from touch events. + */ + internal suspend fun dispatchScrollDelta( + delta: Float, + location: Int, + maxOffsetY: Float, + ) { + mutatorMutex.mutate(MutatePriority.UserInput) { + _indicatorOffset.snapTo((_indicatorOffset.value + delta).toFloat()) + updateProgress( + location = location, + maxOffsetY = maxOffsetY, + ) + } + } + + private fun updateProgress( + offsetY: Float = abs(indicatorOffset), + location: Int, + maxOffsetY: Float, + ) { + val offsetPercent = min(1f, offsetY / maxOffsetY) + + val offset = min(maxOffsetY, offsetY) + progress = SwipeProgress(location, offset, offsetPercent) + } +} diff --git a/compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/complexlazylist/models/FollowItem.kt b/compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/complexlazylist/models/FollowItem.kt new file mode 100644 index 0000000000000..04949ab8f24ab --- /dev/null +++ b/compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/complexlazylist/models/FollowItem.kt @@ -0,0 +1,16 @@ +package benchmarks.complexlazylist.models + +class FollowItem() : IBaseViewModel { + override val blockId: String? + get() = TODO("Not yet implemented") + override val reportInfo: Map? + get() = TODO("Not yet implemented") + override val operations: Map? + get() = TODO("Not yet implemented") + override val flipInfos: Map? + get() = TODO("Not yet implemented") + override val extraData: Map? + get() = TODO("Not yet implemented") + override val data: Map? + get() = TODO("Not yet implemented") +} \ No newline at end of file diff --git a/compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/complexlazylist/models/HotItem.kt b/compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/complexlazylist/models/HotItem.kt new file mode 100644 index 0000000000000..8c79eb07354a6 --- /dev/null +++ b/compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/complexlazylist/models/HotItem.kt @@ -0,0 +1,16 @@ +package benchmarks.complexlazylist.models + +class HotItem() : IBaseViewModel { + override val blockId: String? + get() = TODO("Not yet implemented") + override val reportInfo: Map? + get() = TODO("Not yet implemented") + override val operations: Map? + get() = TODO("Not yet implemented") + override val flipInfos: Map? + get() = TODO("Not yet implemented") + override val extraData: Map? + get() = TODO("Not yet implemented") + override val data: Map? + get() = TODO("Not yet implemented") +} diff --git a/compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/complexlazylist/models/IBaseViewModel.kt b/compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/complexlazylist/models/IBaseViewModel.kt new file mode 100644 index 0000000000000..0c541f6be773c --- /dev/null +++ b/compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/complexlazylist/models/IBaseViewModel.kt @@ -0,0 +1,10 @@ +package benchmarks.complexlazylist.models + +interface IBaseViewModel { + val blockId: String? + val reportInfo: Map? + val operations: Map? + val flipInfos: Map? + val extraData: Map? + val data: Map? +} \ No newline at end of file diff --git a/compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/complexlazylist/models/ICompositionModel.kt b/compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/complexlazylist/models/ICompositionModel.kt new file mode 100644 index 0000000000000..004bb51a0f2eb --- /dev/null +++ b/compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/complexlazylist/models/ICompositionModel.kt @@ -0,0 +1,21 @@ +package benchmarks.complexlazylist.models + +interface ICompositionItem { + val bgColor: String? + val radius: String? + val alpha: String? + val shadowColor: String? + val textColor: String? + val text: String? +} + +interface ICompositionModel : IBaseViewModel { + val title: String? + val subtitle: String? + val overlyTopLeft: ICompositionItem? + val overlyView1: ICompositionItem? + val overlyView2: ICompositionItem? + val overlyView3: ICompositionItem? + val overlyTopRight: ICompositionItem? + val label: ICompositionItem? +} \ No newline at end of file diff --git a/compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/complexlazylist/models/ViewModelFactory.kt b/compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/complexlazylist/models/ViewModelFactory.kt new file mode 100644 index 0000000000000..d1fdc8b78277c --- /dev/null +++ b/compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/complexlazylist/models/ViewModelFactory.kt @@ -0,0 +1,56 @@ +package benchmarks.complexlazylist.models + + +val fakeItem2 = object : ICompositionItem { + override val bgColor: String? + get() = "" + override val radius: String? + get() = "0.5" + override val alpha: String? + get() = "0.5" + override val shadowColor: String? + get() = "" + override val textColor: String? + get() = "" + override val text: String? + get() = "yqhwjmjdsk" + + "fksghksjhgksjh" + + "gsgjlksgjlk" +} + +open class FakeItem : ICompositionModel { + override val title: String? + get() = "aghjagj" + override val subtitle: String? + get() = "ghjghjghj" + override val overlyTopLeft: ICompositionItem? + get() = fakeItem2 + override val overlyView1: ICompositionItem? + get() = fakeItem2 + override val overlyView2: ICompositionItem? + get() = fakeItem2 + override val overlyView3: ICompositionItem? + get() = fakeItem2 + override val overlyTopRight: ICompositionItem? + get() = fakeItem2 + override val label: ICompositionItem? + get() = fakeItem2 + override val blockId: String? + get() = "" + override val reportInfo: Map? + get() = TODO("Not yet implemented") + override val operations: Map? + get() = TODO("Not yet implemented") + override val flipInfos: Map? + get() = TODO("Not yet implemented") + override val extraData: Map? + get() = TODO("Not yet implemented") + override val data: Map? + get() = TODO("Not yet implemented") +} + +fun createFakeItem(): ICompositionModel = FakeItem() + +internal fun fetchCompositionModels(useJSON: Boolean, callback: (List) -> Unit) { + callback(List(250, { createFakeItem() })) +} diff --git a/compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/heavyshader/HeavyShader.kt b/compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/heavyshader/HeavyShader.kt new file mode 100644 index 0000000000000..633e6c7ba805c --- /dev/null +++ b/compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/heavyshader/HeavyShader.kt @@ -0,0 +1,135 @@ +/* + * Copyright 2020-2026 JetBrains s.r.o. and respective authors and developers. + * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. + */ + +package benchmarks.heavyshader + +import androidx.compose.animation.core.LinearEasing +import androidx.compose.animation.core.RepeatMode +import androidx.compose.animation.core.animateFloat +import androidx.compose.animation.core.infiniteRepeatable +import androidx.compose.animation.core.rememberInfiniteTransition +import androidx.compose.animation.core.tween +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.background +import androidx.compose.foundation.gestures.scrollBy +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.runtime.withFrameMillis +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.BlendMode +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import kotlin.math.PI +import kotlin.math.cos +import kotlin.math.sin +import kotlinx.coroutines.isActive + +private const val ITEM_COUNT = 800 + +@Composable +fun HeavyShader() { + val listState = rememberLazyListState() + var scrollForward by remember { mutableStateOf(true) } + + LaunchedEffect(Unit) { + while (isActive) { + withFrameMillis { } + val currentItem = listState.firstVisibleItemIndex + if (currentItem == 0) scrollForward = true + if (currentItem > ITEM_COUNT - 100) scrollForward = false + listState.scrollBy(if (scrollForward) 33f else -33f) + } + } + + LazyColumn( + state = listState, + modifier = Modifier.fillMaxSize().background(Color.Black) + ) { + items(ITEM_COUNT) { + HeavyShaderItem() + } + } +} + +@Composable +private fun HeavyShaderItem() { + val time by rememberInfiniteTransition().animateFloat( + initialValue = 0f, + targetValue = 100f, + animationSpec = infiniteRepeatable( + animation = tween(5000, easing = LinearEasing), + repeatMode = RepeatMode.Restart + ) + ) + + Canvas( + modifier = Modifier + .fillMaxWidth() + .height(120.dp) + .padding(8.dp) + ) { + val layers = 60 + + repeat(layers) { layer -> + val offset = layer * 10f + val alpha = 1f - (layer.toFloat() / layers) + + drawRect( + brush = Brush.radialGradient( + colors = listOf( + Color(0xFF00FF00).copy(alpha = alpha), + Color(0xFF0000FF).copy(alpha = alpha * 0.7f), + Color(0xFFFF0000).copy(alpha = alpha * 0.5f), + Color(0xFFFFFF00).copy(alpha = alpha * 0.3f), + Color(0xFF00FFFF).copy(alpha = alpha * 0.1f) + ), + center = Offset( + size.width / 2 + cos(time + layer).toFloat() * 150f, + size.height / 2 + sin(time + layer).toFloat() * 150f + ), + radius = size.minDimension / 2 + offset + ), + blendMode = when (layer % 3) { + 0 -> BlendMode.Screen + 1 -> BlendMode.Overlay + else -> BlendMode.Multiply + } + ) + } + + repeat(60) { i -> + val angle = i * 7.2f + time * 10 + val distance = size.minDimension / 2 + val x = size.width / 2 + cos(angle * PI / 180.0).toFloat() * distance + val y = size.height / 2 + sin(angle * PI / 180.0).toFloat() * distance + + drawCircle( + brush = Brush.radialGradient( + colors = listOf( + Color.White.copy(alpha = 0.8f), + Color.Transparent + ), + center = Offset(x, y), + radius = 30f + ), + radius = 30f, + center = Offset(x, y), + blendMode = BlendMode.Plus + ) + } + } +} diff --git a/compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/lazygrid/LazyGrid.kt b/compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/lazygrid/LazyGrid.kt new file mode 100644 index 0000000000000..a10fafe31b38e --- /dev/null +++ b/compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/lazygrid/LazyGrid.kt @@ -0,0 +1,97 @@ +package benchmarks.lazygrid + +import androidx.compose.foundation.gestures.scrollBy +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.lazy.grid.items +import androidx.compose.foundation.lazy.grid.rememberLazyGridState +import androidx.compose.material.Card +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import kotlinx.coroutines.isActive +import kotlin.coroutines.suspendCoroutine + +@Composable +fun LazyGrid(smoothScroll: Boolean = false, withLaunchedEffectInItem: Boolean = false) { + val itemCount = 12000 + val entries = remember {List(itemCount) { Entry("$it") }} + val state = rememberLazyGridState() + + MaterialTheme { + Column { + LazyVerticalGrid( + columns = GridCells.Fixed(4), + modifier = Modifier.fillMaxWidth().semantics { contentDescription = "IamLazy" }, + state = state + ) { + items(entries) { + ListCell(it, withLaunchedEffectInItem) + } + } + } + + } + + var curItem by remember { mutableStateOf(0) } + var direct by remember { mutableStateOf(true) } + if (smoothScroll) { + LaunchedEffect(Unit) { + while (isActive) { + withFrameMillis { } + curItem = state.firstVisibleItemIndex + if (curItem == 0) direct = true + if (curItem > itemCount - 100) direct = false + state.scrollBy(if (direct) 55f else -55f) + } + } + } else { + LaunchedEffect(Unit) { + while(isActive) { + withFrameMillis {} + curItem += if (direct) 50 else -50 + if (curItem >= itemCount) { + direct = false + curItem = itemCount - 1 + } else if (curItem <= 0) { + direct = true + curItem = 0 + } + state.scrollToItem(curItem) + } + } + } +} + +data class Entry(val contents: String) + +@Composable +private fun ListCell(entry: Entry, withLaunchedEffect: Boolean = false) { + Card( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp) + ) { + Text( + text = entry.contents, + textAlign = TextAlign.Center, + style = MaterialTheme.typography.h5, + modifier = Modifier.padding(16.dp) + ) + + if (withLaunchedEffect) { + LaunchedEffect(Unit) { + // Never resumed to imitate some async task running in an item's scope + suspendCoroutine { } + } + } + } +} \ No newline at end of file diff --git a/compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/multipleComponents/Clickable.kt b/compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/multipleComponents/Clickable.kt new file mode 100644 index 0000000000000..b8b364d6cc43f --- /dev/null +++ b/compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/multipleComponents/Clickable.kt @@ -0,0 +1,362 @@ +/* + * Copyright 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// copy of https://github.com/JetBrains/compose-multiplatform-core/blob/d9e875b62e7bb4dd47b6b155d3a787251ff5bd38/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/Clickable.desktop.kt#L78 +// inefficient implementation of clickable for benchmarking purposes (Modifier.Node variant in AOSP is more optimized) + +package benchmarks.multipleComponents + +import androidx.compose.foundation.Indication +import androidx.compose.foundation.focusable +import androidx.compose.foundation.gestures.awaitEachGesture +import androidx.compose.foundation.hoverable +import androidx.compose.foundation.indication +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.PressInteraction +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.State +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.ui.Modifier +import androidx.compose.ui.composed +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.input.key.Key +import androidx.compose.ui.input.key.KeyEvent +import androidx.compose.ui.input.key.KeyEventType.Companion.KeyDown +import androidx.compose.ui.input.key.KeyEventType.Companion.KeyUp +import androidx.compose.ui.input.key.key +import androidx.compose.ui.input.key.onKeyEvent +import androidx.compose.ui.input.key.type +import androidx.compose.ui.input.pointer.AwaitPointerEventScope +import androidx.compose.ui.input.pointer.PointerButtons +import androidx.compose.ui.input.pointer.PointerEvent +import androidx.compose.ui.input.pointer.PointerEventPass +import androidx.compose.ui.input.pointer.PointerInputScope +import androidx.compose.ui.input.pointer.PointerKeyboardModifiers +import androidx.compose.ui.input.pointer.changedToDown +import androidx.compose.ui.input.pointer.changedToUp +import androidx.compose.ui.input.pointer.isOutOfBounds +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.input.pointer.positionChangeConsumed +import androidx.compose.ui.node.ModifierNodeElement +import androidx.compose.ui.node.SemanticsModifierNode +import androidx.compose.ui.platform.InspectorInfo +import androidx.compose.ui.platform.debugInspectorInfo +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.semantics.SemanticsPropertyReceiver +import androidx.compose.ui.semantics.disabled +import androidx.compose.ui.semantics.onClick +import androidx.compose.ui.semantics.onLongClick +import androidx.compose.ui.unit.center +import androidx.compose.ui.unit.toOffset +import androidx.compose.ui.util.fastAll +import androidx.compose.ui.util.fastAny +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch + +@Immutable +class MouseClickScope( + val buttons: PointerButtons, + val keyboardModifiers: PointerKeyboardModifiers +) + +@Suppress("DEPRECATION") +internal val EmptyClickContext = MouseClickScope( + PointerButtons(0), PointerKeyboardModifiers(0) +) + +fun Modifier.mouseClickable( + enabled: Boolean = true, + onClickLabel: String? = null, + role: Role? = null, + @Suppress("DEPRECATION") onClick: MouseClickScope.() -> Unit +) = composed( + factory = { + val onClickState = rememberUpdatedState(onClick) + val centreOffset = remember { mutableStateOf(Offset.Zero) } + val currentKeyPressInteractions = remember { mutableMapOf() } + val focusRequester = remember { FocusRequester() } + val gesture = if (enabled) { + Modifier.pointerInput(Unit) { + centreOffset.value = size.center.toOffset() + detectTapWithContext( + onTap = { down, _ -> + focusRequester.requestFocus() + onClickState.value.invoke( + MouseClickScope( + down.buttons, + down.keyboardModifiers + ) + ) + } + ) + } + } else { + Modifier + } + Modifier + .focusRequester(focusRequester) + .genericClickableWithoutGesture( + interactionSource = remember { MutableInteractionSource() }, + indication = null, + indicationScope = rememberCoroutineScope(), + keyClickOffset = centreOffset, + enabled = enabled, + onClickLabel = onClickLabel, + currentKeyPressInteractions = currentKeyPressInteractions, + role = role, + onLongClickLabel = null, + onLongClick = null, + onClick = { onClick(EmptyClickContext) } + ) + .then(gesture) + }, + inspectorInfo = debugInspectorInfo { + name = "clickable" + properties["enabled"] = enabled + properties["onClickLabel"] = onClickLabel + properties["role"] = role + properties["onClick"] = onClick + } +) + +internal suspend fun PointerInputScope.detectTapWithContext( + onTap: ((PointerEvent, PointerEvent) -> Unit)? = null +) { + awaitEachGesture { + val down = awaitEventFirstDown().also { + it.changes.forEach { it.consume() } + } + + val up = waitForFirstInboundUpOrCancellation() + if (up != null) { + up.changes.forEach { it.consume() } + onTap?.invoke(down, up) + } + } +} + +private suspend fun AwaitPointerEventScope.awaitEventFirstDown(): PointerEvent { + var event: PointerEvent + do { + event = awaitPointerEvent() + } while ( + !event.changes.fastAll { it.changedToDown() } + ) + return event +} + +private suspend fun AwaitPointerEventScope.waitForFirstInboundUpOrCancellation(): PointerEvent? { + while (true) { + val event = awaitPointerEvent(PointerEventPass.Main) + if (event.changes.fastAll { it.changedToUp() }) { + // All pointers are up + return event + } + + if (event.changes.fastAny { + it.consumed.downChange || it.isOutOfBounds(size, extendedTouchPadding) + } + ) { + return null // Canceled + } + + // Check for cancel by position consumption. We can look on the Final pass of the + // existing pointer event because it comes after the Main pass we checked above. + val consumeCheck = awaitPointerEvent(PointerEventPass.Final) + if (consumeCheck.changes.fastAny { it.positionChangeConsumed() }) { + return null + } + } +} + +internal fun Modifier.genericClickableWithoutGesture( + interactionSource: MutableInteractionSource, + indication: Indication?, + indicationScope: CoroutineScope, + currentKeyPressInteractions: MutableMap, + keyClickOffset: State, + enabled: Boolean = true, + onClickLabel: String? = null, + role: Role? = null, + onLongClickLabel: String? = null, + onLongClick: (() -> Unit)? = null, + onClick: () -> Unit +): Modifier { + fun Modifier.detectPressAndClickFromKey() = this.onKeyEvent { keyEvent -> + when { + enabled && keyEvent.isPress -> { + // If the key already exists in the map, keyEvent is a repeat event. + // We ignore it as we only want to emit an interaction for the initial key press. + if (!currentKeyPressInteractions.containsKey(keyEvent.key)) { + val press = PressInteraction.Press(keyClickOffset.value) + currentKeyPressInteractions[keyEvent.key] = press + indicationScope.launch { interactionSource.emit(press) } + true + } else { + false + } + } + enabled && keyEvent.isClick -> { + currentKeyPressInteractions.remove(keyEvent.key)?.let { + indicationScope.launch { + interactionSource.emit(PressInteraction.Release(it)) + } + } + onClick() + true + } + else -> false + } + } + return this then + ClickableSemanticsElement( + enabled = enabled, + role = role, + onLongClickLabel = onLongClickLabel, + onLongClick = onLongClick, + onClickLabel = onClickLabel, + onClick = onClick + ) + .detectPressAndClickFromKey() + .indication(interactionSource, indication) + .hoverable(enabled = enabled, interactionSource = interactionSource) + .focusable(enabled = enabled, interactionSource = interactionSource) +} + +private class ClickableSemanticsElement( + private val enabled: Boolean, + private val role: Role?, + private val onLongClickLabel: String?, + private val onLongClick: (() -> Unit)?, + private val onClickLabel: String?, + private val onClick: () -> Unit +) : ModifierNodeElement() { + override fun create() = ClickableSemanticsNode( + enabled = enabled, + role = role, + onLongClickLabel = onLongClickLabel, + onLongClick = onLongClick, + onClickLabel = onClickLabel, + onClick = onClick + ) + + override fun update(node: ClickableSemanticsNode) { + node.update(enabled, onClickLabel, role, onClick, onLongClickLabel, onLongClick) + } + + override fun InspectorInfo.inspectableProperties() = Unit + + override fun hashCode(): Int { + var result = enabled.hashCode() + result = 31 * result + role.hashCode() + result = 31 * result + onLongClickLabel.hashCode() + result = 31 * result + onLongClick.hashCode() + result = 31 * result + onClickLabel.hashCode() + result = 31 * result + onClick.hashCode() + return result + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is ClickableSemanticsElement) return false + + if (enabled != other.enabled) return false + if (role != other.role) return false + if (onLongClickLabel != other.onLongClickLabel) return false + if (onLongClick !== other.onLongClick) return false + if (onClickLabel != other.onClickLabel) return false + if (onClick !== other.onClick) return false + + return true + } +} + +internal class ClickableSemanticsNode( + private var enabled: Boolean, + private var onClickLabel: String?, + private var role: Role?, + private var onClick: () -> Unit, + private var onLongClickLabel: String?, + private var onLongClick: (() -> Unit)?, +) : SemanticsModifierNode, Modifier.Node() { + fun update( + enabled: Boolean, + onClickLabel: String?, + role: Role?, + onClick: () -> Unit, + onLongClickLabel: String?, + onLongClick: (() -> Unit)?, + ) { + this.enabled = enabled + this.onClickLabel = onClickLabel + this.role = role + this.onClick = onClick + this.onLongClickLabel = onLongClickLabel + this.onLongClick = onLongClick + } + + override val shouldMergeDescendantSemantics: Boolean + get() = true + + override fun SemanticsPropertyReceiver.applySemantics() { + if (this@ClickableSemanticsNode.role != null) { + role = this@ClickableSemanticsNode.role!! + } + onClick( + action = { onClick(); true }, + label = onClickLabel + ) + if (onLongClick != null) { + onLongClick( + action = { onLongClick?.invoke(); true }, + label = onLongClickLabel + ) + } + if (!enabled) { + disabled() + } + } +} + + +/** + * Whether the specified [KeyEvent] should trigger a press for a clickable component, i.e. whether + * it is associated with a press of an enter key or dpad centre. + */ +private val KeyEvent.isPress: Boolean + get() = type == KeyDown && isEnter + +/** + * Whether the specified [KeyEvent] should trigger a click for a clickable component, i.e. whether + * it is associated with a release of an enter key or dpad centre. + */ +private val KeyEvent.isClick: Boolean + get() = type == KeyUp && isEnter + +private val KeyEvent.isEnter: Boolean + get() = + when (key) { + Key.DirectionCenter, + Key.Enter, + Key.NumPadEnter, + Key.Spacebar -> true + else -> false + } diff --git a/compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/multipleComponents/MenuIcon.kt b/compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/multipleComponents/MenuIcon.kt new file mode 100644 index 0000000000000..fba042afa95d3 --- /dev/null +++ b/compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/multipleComponents/MenuIcon.kt @@ -0,0 +1,49 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package benchmarks.multipleComponents + +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.graphics.vector.path +import androidx.compose.ui.unit.dp + +internal val MenuIcon: ImageVector = ImageVector.Builder( + name = "Menu", + defaultWidth = 24.dp, + defaultHeight = 24.dp, + viewportWidth = 24f, + viewportHeight = 24f +).apply { + path(fill = SolidColor(Color.Black)) { + moveTo(3f, 18f) + horizontalLineTo(21f) + verticalLineTo(16f) + horizontalLineTo(3f) + close() + moveTo(3f, 13f) + horizontalLineTo(21f) + verticalLineTo(11f) + horizontalLineTo(3f) + close() + moveTo(3f, 6f) + verticalLineTo(8f) + horizontalLineTo(21f) + verticalLineTo(6f) + close() + } +}.build() diff --git a/compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/multipleComponents/MultipleComponents.kt b/compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/multipleComponents/MultipleComponents.kt new file mode 100644 index 0000000000000..e5066ecd6c5bf --- /dev/null +++ b/compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/multipleComponents/MultipleComponents.kt @@ -0,0 +1,662 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// copy of https://github.com/JetBrains/compose-multiplatform-core/blob/d9e875b62e7bb4dd47b6b155d3a787251ff5bd38/compose/desktop/desktop/samples/src/jvmMain/kotlin/androidx/compose/desktop/examples/example1/Main.jvm.kt#L17 + +package benchmarks.multipleComponents + +import androidx.compose.animation.animateColorAsState +import androidx.compose.animation.core.TweenSpec +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.Image +import androidx.compose.foundation.ScrollState +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.text.InlineTextContent +import androidx.compose.foundation.text.appendInlineContent +import androidx.compose.foundation.text.selection.SelectionContainer +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.BottomAppBar +import androidx.compose.material.Button +import androidx.compose.material.ButtonDefaults +import androidx.compose.material.Checkbox +import androidx.compose.material.CircularProgressIndicator +import androidx.compose.material.DropdownMenu +import androidx.compose.material.DropdownMenuItem +import androidx.compose.material.ExtendedFloatingActionButton +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Scaffold +import androidx.compose.material.Slider +import androidx.compose.material.Switch +import androidx.compose.material.Text +import androidx.compose.material.TextField +import androidx.compose.material.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.produceState +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusProperties +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Color.Companion.Black +import androidx.compose.ui.graphics.Color.Companion.Blue +import androidx.compose.ui.graphics.Color.Companion.Green +import androidx.compose.ui.graphics.Color.Companion.Red +import androidx.compose.ui.graphics.Color.Companion.Yellow +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.graphics.ImageShader +import androidx.compose.ui.graphics.ShaderBrush +import androidx.compose.ui.graphics.Shadow +import androidx.compose.ui.graphics.TileMode +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.input.key.Key +import androidx.compose.ui.input.key.isMetaPressed +import androidx.compose.ui.input.key.isShiftPressed +import androidx.compose.ui.input.key.key +import androidx.compose.ui.input.key.onPreviewKeyEvent +import androidx.compose.ui.input.pointer.PointerEventType +import androidx.compose.ui.input.pointer.PointerIcon +import androidx.compose.ui.input.pointer.isAltPressed +import androidx.compose.ui.input.pointer.isBackPressed +import androidx.compose.ui.input.pointer.isCtrlPressed +import androidx.compose.ui.input.pointer.isForwardPressed +import androidx.compose.ui.input.pointer.isMetaPressed +import androidx.compose.ui.input.pointer.isPrimaryPressed +import androidx.compose.ui.input.pointer.isSecondaryPressed +import androidx.compose.ui.input.pointer.isShiftPressed +import androidx.compose.ui.input.pointer.isTertiaryPressed +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.input.pointer.pointerHoverIcon +import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.text.ExperimentalTextApi +import androidx.compose.ui.text.Placeholder +import androidx.compose.ui.text.PlaceholderVerticalAlign +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.text.style.TextDecoration.Companion.Underline +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.em +import androidx.compose.ui.unit.sp +import compose_benchmarks.benchmarks.generated.resources.Res +import compose_benchmarks.benchmarks.generated.resources.example1_cat +import compose_benchmarks.benchmarks.generated.resources.example1_ic_call_answer +import compose_benchmarks.benchmarks.generated.resources.example1_sailing +import compose_benchmarks.benchmarks.generated.resources.jetbrainsmono_italic +import compose_benchmarks.benchmarks.generated.resources.jetbrainsmono_regular +import org.jetbrains.compose.resources.ExperimentalResourceApi +import org.jetbrains.compose.resources.Font +import org.jetbrains.compose.resources.InternalResourceApi +import org.jetbrains.compose.resources.decodeToImageBitmap +import org.jetbrains.compose.resources.painterResource +import kotlin.random.Random + +@Composable +fun MultipleComponentsExample(isVectorGraphicsSupported: Boolean = true) { + val uriHandler = LocalUriHandler.current + MaterialTheme { + Scaffold( + topBar = { + TopAppBar( + title = { + Row(verticalAlignment = Alignment.CenterVertically) { + if (isVectorGraphicsSupported) { + Image( + painterResource(Res.drawable.example1_sailing), + contentDescription = "Star" + ) + } + Text("Desktop Compose Elements") + } + } + ) + }, + floatingActionButton = { + ExtendedFloatingActionButton( + text = { Text("Open URL") }, + onClick = { + uriHandler.openUri("https://google.com") + } + ) + }, + isFloatingActionButtonDocked = true, + bottomBar = { + BottomAppBar(cutoutShape = CircleShape) { + IconButton( + onClick = {} + ) { + Icon(MenuIcon, "Menu", Modifier.size(ButtonDefaults.IconSize)) + } + } + }, + content = { innerPadding -> + Row(Modifier.padding(innerPadding)) { + LeftColumn(Modifier.weight(1f)) + MiddleColumn(Modifier.width(500.dp), isVectorGraphicsSupported = isVectorGraphicsSupported) + RightColumn(Modifier.width(200.dp)) + } + } + ) + } +} + +@Composable +private fun LeftColumn(modifier: Modifier) = Box(modifier.fillMaxSize()) { + val state = rememberScrollState() + ScrollableContent(state) +} + +@OptIn(ExperimentalComposeUiApi::class) +@Composable +private fun ScrollableContent(scrollState: ScrollState) { + val italicFont = FontFamily( + Font(Res.font.jetbrainsmono_italic) + ) + + val dispatchedFonts = FontFamily( + Font(Res.font.jetbrainsmono_regular, style = FontStyle.Italic), + Font(Res.font.jetbrainsmono_italic, style = FontStyle.Normal) + ) + + val amount = remember { mutableStateOf(0f) } + val animation = remember { mutableStateOf(true) } + Column(Modifier.fillMaxSize().verticalScroll(scrollState)) { + Text( + text = "Привет! 你好! Desktop Compose use: ${amount.value}", + color = Color.Black, + modifier = Modifier + .background(Color.Blue) + .height(56.dp) + .wrapContentSize(Alignment.Center) + ) + + val inlineIndicatorId = "indicator" + + Text( + text = buildAnnotatedString { + append("The quick ") + if (animation.value) { + appendInlineContent(inlineIndicatorId) + } + pushStyle( + SpanStyle( + color = Color(0xff964B00), + shadow = Shadow(Color.Green, offset = Offset(1f, 1f)) + ) + ) + append("brown fox") + pop() + pushStyle(SpanStyle(background = Color.Yellow)) + append(" 🦊 ate a ") + pop() + pushStyle(SpanStyle(fontSize = 30.sp, textDecoration = Underline)) + append("zesty hamburgerfons") + pop() + append(" 🍔.\nThe 👩‍👩‍👧‍👧 laughed.") + addStyle(SpanStyle(color = Color.Green), 25, 35) + }, + color = Color.Black, + inlineContent = mapOf( + inlineIndicatorId to InlineTextContent( + Placeholder( + width = 1.em, + height = 1.em, + placeholderVerticalAlign = PlaceholderVerticalAlign.AboveBaseline + ) + ) { + CircularProgressIndicator(Modifier.padding(end = 3.dp)) + } + ) + ) + + val loremColors = listOf( + Color.Black, + Color.Yellow, + Color.Green, + Color.Blue + ) + var loremColor by remember { mutableStateOf(0) } + + val loremDecorations = listOf( + TextDecoration.None, + TextDecoration.Underline, + TextDecoration.LineThrough + ) + val lorem = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do" + + " eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad" + + " minim veniam, quis nostrud exercitation ullamco laboris nisi ut" + + " aliquipex ea commodo consequat. Duis aute irure dolor in reprehenderit" + + " in voluptate velit esse cillum dolore eu fugiat nulla pariatur." + + " Excepteur" + + " sint occaecat cupidatat non proident, sunt in culpa qui officia" + + " deserunt mollit anim id est laborum." + var loremDecoration by remember { mutableStateOf(0) } + Text( + text = lorem, + color = loremColors[loremColor], + textAlign = TextAlign.Center, + textDecoration = loremDecorations[loremDecoration], + modifier = Modifier.clickable { + if (loremColor < loremColors.size - 1) { + loremColor += 1 + } else { + loremColor = 0 + } + + if (loremDecoration < loremDecorations.size - 1) { + loremDecoration += 1 + } else { + loremDecoration = 0 + } + } + ) + + Text( + text = lorem, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + Row { + Box(modifier = Modifier.size(200.dp, 200.dp).border(1.dp, Black)) { + BrushTextGradient(lorem) + } + Box(modifier = Modifier.size(200.dp, 200.dp).border(1.dp, Black)) { + BrushTextImage(lorem) + } + } + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceEvenly + ) { + Text( + "Default", + ) + + Text( + "SansSerif", + fontFamily = FontFamily.SansSerif + ) + + Text( + "Serif", + fontFamily = FontFamily.Serif + ) + + Text( + "Monospace", + fontFamily = FontFamily.Monospace + ) + + Text( + "Cursive", + fontFamily = FontFamily.Cursive + ) + } + + var overText by remember { mutableStateOf("Move mouse over text:") } + Text(overText, style = TextStyle(letterSpacing = 10.sp)) + + SelectionContainer { + Text( + text = "fun > List.quickSort(): List = when {\n" + + " size < 2 -> this\n" + + " else -> {\n" + + " val pivot = first()\n" + + " val (smaller, greater) = drop(1).partition { it <= pivot }\n" + + " smaller.quickSort() + pivot + greater.quickSort()\n" + + " }\n" + + "}", + fontFamily = italicFont, + modifier = Modifier + .padding(10.dp) + .pointerInput(Unit) { + awaitPointerEventScope { + while (true) { + val event = awaitPointerEvent() + when (event.type) { + PointerEventType.Move -> { + val position = event.changes.first().position + overText = "Move position: $position" + } + PointerEventType.Enter -> overText = "Over enter" + PointerEventType.Exit -> overText = "Over exit" + } + } + } + } + ) + } + Text( + text = buildAnnotatedString { + append("resolved: NotoSans-Regular.ttf ") + pushStyle( + SpanStyle( + fontStyle = FontStyle.Italic + ) + ) + append("NotoSans-italic.ttf.") + }, + fontFamily = dispatchedFonts, + ) + + Row(verticalAlignment = Alignment.CenterVertically) { + Button( + modifier = Modifier.padding(4.dp).pointerHoverIcon(PointerIcon.Hand), + onClick = { + amount.value++ + } + ) { + Text("Base") + } + + var clickableText by remember { mutableStateOf("Click me!") } + Text( + modifier = Modifier.mouseClickable( + onClick = { + clickableText = buildString { + append("Buttons pressed:\n") + append("primary: ${buttons.isPrimaryPressed}\t") + append("secondary: ${buttons.isSecondaryPressed}\t") + append("tertiary: ${buttons.isTertiaryPressed}\t") + append("primary: ${buttons.isPrimaryPressed}\t") + append("back: ${buttons.isBackPressed}\t") + append("forward: ${buttons.isForwardPressed}\t") + + append("\n\nKeyboard modifiers pressed:\n") + + append("alt: ${keyboardModifiers.isAltPressed}\t") + append("ctrl: ${keyboardModifiers.isCtrlPressed}\t") + append("meta: ${keyboardModifiers.isMetaPressed}\t") + append("shift: ${keyboardModifiers.isShiftPressed}\t") + } + } + ), + text = clickableText + ) + } + + Row( + modifier = Modifier.padding(vertical = 10.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Row { + Column { + Switch( + animation.value, + onCheckedChange = { + animation.value = it + } + ) + Row( + modifier = Modifier.padding(4.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Checkbox( + animation.value, + onCheckedChange = { + animation.value = it + } + ) + Text("Animation") + } + } + + Button( + modifier = Modifier.padding(4.dp), + onClick = { } + ) { + Text("Window") + } + } + + Animations(isCircularEnabled = animation.value) + } + + Slider( + value = amount.value / 100f, + onValueChange = { amount.value = (it * 100) } + ) + val dropDownMenuExpanded = remember { mutableStateOf(false) } + Button(onClick = { dropDownMenuExpanded.value = true }) { + Text("Expand Menu") + } + DropdownMenu( + expanded = dropDownMenuExpanded.value, + onDismissRequest = { + dropDownMenuExpanded.value = false + println("OnDismissRequest") + } + ) { + DropdownMenuItem(modifier = Modifier, onClick = { + println("Item 1 clicked") + }) { + Text("Item 1") + } + DropdownMenuItem(modifier = Modifier, onClick = { + println("Item 2 clicked") + }) { + Text("Item 2") + } + DropdownMenuItem(modifier = Modifier, onClick = { + println("Item 3 clicked") + }) { + Text("Item 3") + } + } + TextField( + value = amount.value.toString(), + onValueChange = { amount.value = it.toFloatOrNull() ?: 42f }, + label = { Text(text = "Input1") } + ) + + } +} + +@Composable +fun MiddleColumn(modifier: Modifier, isVectorGraphicsSupported: Boolean) = Column(modifier) { + val (focusItem1, focusItem2) = FocusRequester.createRefs() + val text = remember { + mutableStateOf("Hello \uD83E\uDDD1\uD83C\uDFFF\u200D\uD83E\uDDB0") + } + + TextField( + value = text.value, + onValueChange = { text.value = it }, + label = { Text(text = "Input2") }, + placeholder = { + Text(text = "Important input") + }, + maxLines = 1, + modifier = Modifier.onPreviewKeyEvent { + when { + (it.isMetaPressed && it.key == Key.Enter) -> { + if (it.isShiftPressed) { + text.value = "Cleared with shift!" + } else { + text.value = "Cleared!" + } + true + } + else -> false + } + }.focusRequester(focusItem1) + .focusProperties { + next = focusItem2 + } + ) + + var text2 by remember { + val initText = buildString { + (1..1000).forEach { + append("$it\n") + } + } + mutableStateOf(initText) + } + TextField( + text2, + modifier = Modifier + .height(200.dp) + .focusRequester(focusItem2) + .focusProperties { + previous = focusItem1 + }, + onValueChange = { text2 = it } + ) + + Row { + Image( + painterResource(Res.drawable.example1_cat), + "Localized description", + Modifier.size(200.dp) + ) + + if (isVectorGraphicsSupported) { + Icon( + painterResource(Res.drawable.example1_ic_call_answer), + "Localized description", + Modifier.size(100.dp).align(Alignment.CenterVertically), + tint = Color.Blue.copy(alpha = 0.5f) + ) + } + } + + Box( + modifier = Modifier.size(150.dp).background(Color.Gray).pointerHoverIcon( + PointerIcon.Hand + ) + ) { + Box( + modifier = Modifier.offset(20.dp, 20.dp).size(100.dp).background(Color.Blue).pointerHoverIcon( + PointerIcon.Crosshair + ) + ) { + Text("pointerHoverIcon test with Ctrl") + } + } +} + +@Composable +fun Animations(isCircularEnabled: Boolean) = Row { + if (isCircularEnabled) { + CircularProgressIndicator(Modifier.padding(10.dp)) + } + + val enabled = remember { mutableStateOf(true) } + val color by animateColorAsState( + if (enabled.value) Color.Green else Color.Red, + animationSpec = TweenSpec(durationMillis = 2000) + ) + + MaterialTheme { + Box( + Modifier + .size(70.dp) + .clickable { enabled.value = !enabled.value } + .background(color) + ) + } +} + +@OptIn(ExperimentalFoundationApi::class) +@Composable +private fun RightColumn(modifier: Modifier) = Box(modifier) { + val state = rememberLazyListState() + val itemCount = 100000 + val heights = remember { + val random = Random(24) + (0 until itemCount).map { random.nextFloat() } + } + + LazyColumn(Modifier.fillMaxSize().graphicsLayer(alpha = 0.5f), state = state) { + items((0 until itemCount).toList()) { i -> + val itemHeight = 20.dp + 20.dp * heights[i] + Text(i.toString(), Modifier.graphicsLayer(alpha = 0.5f).height(itemHeight)) + } + } + +} + +@OptIn(ExperimentalTextApi::class) +@Composable +fun BrushTextGradient(text: String) { + Text( + text = text, + style = TextStyle( + fontSize = 12.sp, + fontWeight = FontWeight.Bold, + brush = Brush.linearGradient( + colors = listOf(Red, Yellow, Green, Blue) + ) + ) + ) +} + +@OptIn(ExperimentalTextApi::class, ExperimentalResourceApi::class, InternalResourceApi::class) +@Composable +fun BrushTextImage(text: String) { + val image: ImageBitmap? by produceState(null) { + value = Res.readBytes("files/example1_compose-community-primary.png").decodeToImageBitmap() + } + if (image != null) { + Text( + text = text, + style = TextStyle( + fontSize = 12.sp, + fontWeight = FontWeight.Bold, + brush = ShaderBrush( + ImageShader(image!!, TileMode.Repeated, TileMode.Repeated) + ) + ) + ) + } +} \ No newline at end of file diff --git a/compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/textlayout/TextLayout.kt b/compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/textlayout/TextLayout.kt new file mode 100644 index 0000000000000..c0e3efff09c92 --- /dev/null +++ b/compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/textlayout/TextLayout.kt @@ -0,0 +1,117 @@ +/* + * Copyright 2020-2026 JetBrains s.r.o. and respective authors and developers. + * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. + */ + +package benchmarks.textlayout + +import androidx.compose.foundation.background +import androidx.compose.foundation.gestures.scrollBy +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.runtime.withFrameMillis +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import kotlin.random.Random +import kotlinx.coroutines.isActive + +private const val ITEM_COUNT = 2000 + +@Composable +fun TextLayout() { + val listState = rememberLazyListState() + var frame by remember { mutableStateOf(0) } + var scrollForward by remember { mutableStateOf(true) } + + LaunchedEffect(Unit) { + while (isActive) { + withFrameMillis { } + val currentItem = listState.firstVisibleItemIndex + if (currentItem == 0) scrollForward = true + if (currentItem > ITEM_COUNT - 100) scrollForward = false + listState.scrollBy(if (scrollForward) 67f else -67f) + frame++ + } + } + + LazyColumn( + state = listState, + modifier = Modifier.fillMaxSize() + ) { + items(ITEM_COUNT) { index -> + TextLayoutItem(index, frame) + } + } +} + +@Composable +private fun TextLayoutItem(index: Int, frame: Int) { + Column( + modifier = Modifier + .fillMaxWidth() + .background(Color(0xFF2A2A2A)) + ) { + repeat(12) { row -> + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween + ) { + repeat(10) { col -> + Column( + modifier = Modifier + .weight(1f) + .background(Color(0xFF3A3A3A)) + ) { + Text( + "$frame R$row:C$col", + color = Color.White, + fontSize = 6.sp + ) + Row { + Box( + modifier = Modifier + .size(10.dp) + .background(Color(Random.nextInt(0xFFFFFF) or 0xFF000000.toInt())) + ) + Column { + Text("$frame #$index", color = Color.Gray, fontSize = 6.sp) + Text("Item", color = Color.Gray, fontSize = 6.sp) + } + } + Spacer(modifier = Modifier.height(4.dp)) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween + ) { + repeat(10) { + Box( + modifier = Modifier + .size(10.dp) + .background(Color(0xFF4A4A4A)) + ) + } + } + } + } + } + } + } +} diff --git a/compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/visualeffects/HappyNY.kt b/compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/visualeffects/HappyNY.kt new file mode 100644 index 0000000000000..ae07af509e08d --- /dev/null +++ b/compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/visualeffects/HappyNY.kt @@ -0,0 +1,321 @@ +package benchmarks.visualeffects + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Surface +import androidx.compose.runtime.* +import androidx.compose.runtime.snapshots.SnapshotStateList +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.* +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp + +import kotlin.math.* +import kotlin.random.Random + +const val snowCount = 80 +const val starCount = 60 +const val rocketPartsCount = 30 + +data class SnowFlake( + var x: Dp, + var y: Dp, + val scale: Float, + var v: Double, + var alpha: Float, + var angle: Float, + var rotate: Int, + var phase: Double +) + +data class Star(val x: Dp, val y: Dp, val color: Color, val size: Dp) + +val random = Random(123) + +fun random(): Float = random.nextFloat() + +class DoubleRocket(val particle: Particle) { + private val STATE_ROCKET = 0 + private val STATE_SMALL_ROCKETS = 1 + var state = STATE_ROCKET + var rockets: Array = emptyArray() + private fun checkState(time: Long) { + if (particle.vy > -3.0 && state == STATE_ROCKET) { + explode(time) + } + if (state == STATE_SMALL_ROCKETS) { + var done = true + rockets.forEach { + if (!it.exploded) { + it.checkExplode(time) + } + if (!it.checkDone()) { + done = false + } + } + if (done) { + reset() + } + } + } + + private fun reset() { + state = STATE_ROCKET + particle.x = 0.0 + particle.y = 1000.0 + particle.vx = 2.1 + particle.vy = -12.5 + } + + private fun explode(time: Long) { + val colors = arrayOf(Color(0xff, 0, 0), Color(192, 255, 192), Color(192, 212, 255)) + rockets = Array(7) { + val v = 1.2f + 1.0 * random() + val angle = 2 * PI * random() + Rocket( + Particle( + particle.x, + particle.y, + v * sin(angle) + particle.vx, + v * cos(angle) + particle.vy - 0.5f, + colors[it % colors.size] + ), colors[it % colors.size], time + ) + } + state = STATE_SMALL_ROCKETS + } + + fun move(time: Long, prevTime: Long) { + if (rocket.state == rocket.STATE_ROCKET) { + rocket.particle.move(time, prevTime) + rocket.particle.gravity(time, prevTime) + } else { + rocket.rockets.forEach { + it.move(time, prevTime) + } + } + rocket.checkState(time) + } + + @Composable + fun draw() { + if (state == rocket.STATE_ROCKET) { + particle.draw() + } else { + rockets.forEach { + it.draw() + } + } + } + +} + +class Rocket(val particle: Particle, val color: Color, val startTime: Long = 0) { + var exploded = false + var parts: Array = emptyArray() + + fun checkExplode(time: Long) { + if (time - startTime > 1200000000) { + explode() + } + } + + private fun explode() { + parts = Array(rocketPartsCount) { + val v = 0.5f + 1.5 * random() + val angle = 2 * PI * random() + Particle(particle.x, particle.y, v * sin(angle) + particle.vx, v * cos(angle) + particle.vy, color, 1) + } + exploded = true + } + + fun checkDone(): Boolean { + if (!exploded) return false + parts.forEach { + if (it.y < 800) return false + } + return true + } + + fun move(time: Long, prevTime: Long) { + if (!exploded) { + particle.move(time, prevTime) + particle.gravity(time, prevTime) + checkExplode(time) + } else { + parts.forEach { + it.move(time, prevTime) + it.gravity(time, prevTime) + } + } + } + + @Composable + fun draw() { + if (!exploded) { + particle.draw() + } else { + parts.forEach { + it.draw() + } + } + } +} + +class Particle(var x: Double, var y: Double, var vx: Double, var vy: Double, val color: Color, val type: Int = 0) { + fun move(time: Long, prevTime: Long) { + x = (x + vx * (time - prevTime) / 30000000) + y = (y + vy * (time - prevTime) / 30000000) + } + + fun gravity(time: Long, prevTime: Long) { + vy = vy + 1.0f * (time - prevTime) / 300000000 + } + + @Composable + fun draw() { + val alphaFactor = if (type == 0) 1.0f else 1 / (1 + abs(vy / 5)).toFloat() + Box(Modifier.size(5.dp).offset(x.dp, y.dp).alpha(alphaFactor).clip(CircleShape).background(color)) + for (i in 1..5) { + Box( + Modifier.size(4.dp).offset((x - vx / 2 * i).dp, (y - vy / 2 * i).dp) + .alpha(alphaFactor * (1 - 0.18f * i)).clip(CircleShape).background(color) + ) + } + } +} + +val rocket = DoubleRocket(Particle(0.0, 1000.0, 2.1, -12.5, Color.White)) + +fun prepareStarsAndSnowFlakes(stars: SnapshotStateList, snowFlakes: SnapshotStateList, width: Int, height: Int) { + for (i in 0..snowCount) { + snowFlakes.add( + SnowFlake( + (50 + (width - 50) * random()).dp, + (height * random()).dp, + 0.1f + 0.2f * random(), + 1.5 + 3 * random(), + (0.4f + 0.4 * random()).toFloat(), + 60 * random(), + Random.nextInt(1, 5) - 3, + random() * 2 * PI + ) + ) + } + val colors = arrayOf(Color.Red, Color.Yellow, Color.Green, Color.Yellow, Color.Cyan, Color.Magenta, Color.White) + for (i in 0..starCount) { + stars.add( + Star( + (width * random()).dp, + (height * random()).dp, + colors[Random.nextInt(colors.size)], + (3 + 5 * random()).dp + ) + ) + } +} + +@Composable +fun NYContent(width: Int, height: Int) { + var time by remember { mutableStateOf(0L) } + val startTime = remember { 0L } + var prevTime by remember { mutableStateOf(0L) } + val snowFlakes = remember { mutableStateListOf() } + val stars = remember { mutableStateListOf() } + var flickering2 by remember { mutableStateOf(true) } + remember { prepareStarsAndSnowFlakes(stars, snowFlakes, width, height) } + + Surface( + modifier = Modifier.fillMaxSize().padding(5.dp).shadow(3.dp, RoundedCornerShape(20.dp)), + color = Color.Black, + shape = RoundedCornerShape(20.dp) + ) { + + LaunchedEffect(Unit) { + while (true) { + withFrameNanos { + prevTime = time + time = it + } + } + } + + if (flickering2) { + if (time - startTime > 15500000000) { //note, that startTime has been updated above + flickering2 = false + } + } + + rocket.move(time, prevTime) + + Box(Modifier.fillMaxSize()) { + + snow(time, prevTime, snowFlakes, startTime, height) + + starrySky(stars) + + rocket.draw() + + } + } +} + + +@Composable +fun starrySky(stars: SnapshotStateList) { + stars.forEach { + star(it.x, it.y, it.color, size = it.size) + } +} + +@Composable +fun star(x: Dp, y: Dp, color: Color = Color.White, size: Dp) { + Box(Modifier.offset(x, y).scale(1.0f, 0.2f).rotate(45f).size(size).background(color)) + Box(Modifier.offset(x, y).scale(0.2f, 1.0f).rotate(45f).size(size).background(color)) +} + +@Composable +fun snow(time: Long, prevTime: Long, snowFlakes: SnapshotStateList, startTime: Long, height: Int) { + val deltaAngle = (time - startTime) / 100000000 + with(LocalDensity.current) { + snowFlakes.forEach { + var y = it.y + ((it.v * (time - prevTime)) / 300000000).dp + if (y > (height + 20).dp) { + y = -20.dp + } + it.y = y + val x = it.x + (15 * sin(time.toDouble() / 3000000000 + it.phase)).dp + snowFlake(Modifier.offset(x, y).scale(it.scale).rotate(it.angle + deltaAngle * it.rotate), it.alpha) + } + } +} + +@Composable +fun snowFlake(modifier: Modifier, alpha: Float = 0.8f) { + Box(modifier) { + snowFlakeInt(0, 0f, 30.dp, 0.dp, alpha) + snowFlakeInt(0, 60f, 15.dp, 25.dp, alpha) + snowFlakeInt(0, 120f, -15.dp, 25.dp, alpha) + snowFlakeInt(0, 180f, -30.dp, 0.dp, alpha) + snowFlakeInt(0, 240f, -15.dp, -25.dp, alpha) + snowFlakeInt(0, 300f, 15.dp, -25.dp, alpha) + } + +} + +@Composable +fun snowFlakeInt(level: Int, angle: Float, shiftX: Dp, shiftY: Dp, alpha: Float) { + if (level > 3) return + Box( + Modifier.offset(shiftX, shiftY).rotate(angle).width(100.dp).height(10.dp).scale(0.6f).alpha(1f) + .background(Color.White.copy(alpha = alpha)) + ) { + snowFlakeInt(level + 1, 30f, 12.dp, 20.dp, alpha * 0.8f) + snowFlakeInt(level + 1, -30f, 12.dp, -20.dp, alpha * 0.8f) + } +} + diff --git a/compose/mpp/benchmarks/src/desktopMain/kotlin/BenchmarksSave.desktop.kt b/compose/mpp/benchmarks/src/desktopMain/kotlin/BenchmarksSave.desktop.kt new file mode 100644 index 0000000000000..1ac7b1bad0c47 --- /dev/null +++ b/compose/mpp/benchmarks/src/desktopMain/kotlin/BenchmarksSave.desktop.kt @@ -0,0 +1,8 @@ +/* + * Copyright 2020-2025 JetBrains s.r.o. and respective authors and developers. + * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. + */ + +actual fun saveBenchmarkStats(name: String, stats: BenchmarkStats) = saveBenchmarkStatsOnDisk(name, stats) + +actual val printJsonToOutput: Boolean = false diff --git a/compose/mpp/benchmarks/src/desktopMain/kotlin/BenchmarksSaveServer.kt b/compose/mpp/benchmarks/src/desktopMain/kotlin/BenchmarksSaveServer.kt new file mode 100644 index 0000000000000..4e0ac67c1b082 --- /dev/null +++ b/compose/mpp/benchmarks/src/desktopMain/kotlin/BenchmarksSaveServer.kt @@ -0,0 +1,92 @@ +/* + * Copyright 2020-2025 JetBrains s.r.o. and respective authors and developers. + * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. + */ + +import io.ktor.http.* +import io.ktor.serialization.kotlinx.json.* +import io.ktor.server.application.* +import io.ktor.server.engine.* +import io.ktor.server.netty.* +import io.ktor.server.plugins.contentnegotiation.* +import io.ktor.server.plugins.cors.routing.CORS +import io.ktor.server.request.* +import io.ktor.server.response.* +import io.ktor.server.routing.* +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import kotlinx.io.files.Path +import kotlinx.io.files.SystemFileSystem +import kotlinx.serialization.Serializable + +/** + * Data class for receiving benchmark results from client. + */ +@Serializable +data class BenchmarkResultFromClient( + val name: String, + val stats: String // JSON string of BenchmarkStats +) + +/** + * Starts a Ktor server to receive benchmark results from browsers + * and save them to disk in the same format as the direct disk saving mechanism. + */ +object BenchmarksSaveServer { + private var server: EmbeddedServer? = null + + fun start(port: Int = BENCHMARK_SERVER_PORT) { + if (server != null) { + println("Benchmark server is already running") + return + } + + server = embeddedServer(Netty, port = port) { + install(ContentNegotiation) { + json() + } + install(CORS) { + allowMethod(HttpMethod.Get) + allowMethod(HttpMethod.Post) + allowHeader(HttpHeaders.ContentType) + anyHost() + } + routing { + post("/benchmark") { + val result = call.receive() + if (result.name.isEmpty()) { + println("Stopping server! Received empty name from client") + call.respond(HttpStatusCode.OK, "Server stopped.") + stop() + return@post + } + println("Received benchmark result for: ${result.name}") + + withContext(Dispatchers.IO) { + if (Config.saveStatsToJSON) { + saveJson(result.name, result.stats) + } + + if (Config.saveStatsToCSV) { + // TODO: for CSV, we would need to convert JSON to the values + println("CSV results are not yet supported for the browser.") + } + } + + call.respond(HttpStatusCode.OK, "Benchmark result saved") + } + + get("/") { + call.respondText("Benchmark server is running", ContentType.Text.Plain) + } + } + }.start(wait = true) + } + + fun stop() { + server?.stop(1000, 2000) + server = null + println("Benchmark server stopped") + System.exit(0) + } +} diff --git a/compose/mpp/benchmarks/src/desktopMain/kotlin/GraphicsContext.desktop.kt b/compose/mpp/benchmarks/src/desktopMain/kotlin/GraphicsContext.desktop.kt new file mode 100644 index 0000000000000..df9eb367aba24 --- /dev/null +++ b/compose/mpp/benchmarks/src/desktopMain/kotlin/GraphicsContext.desktop.kt @@ -0,0 +1,43 @@ +import org.jetbrains.skia.ColorSpace +import org.jetbrains.skia.PixelGeometry +import org.jetbrains.skia.Surface +import org.jetbrains.skia.SurfaceColorFormat +import org.jetbrains.skia.SurfaceOrigin +import org.jetbrains.skia.SurfaceProps +import org.jetbrains.skiko.ExperimentalSkikoApi +import org.jetbrains.skiko.graphicapi.DirectXOffscreenContext +import org.jetbrains.skiko.hostOs + +@OptIn(ExperimentalSkikoApi::class) +fun graphicsContext(): GraphicsContext? = when { + hostOs.isWindows -> DirectXGraphicsContext() + else -> { + println("Unsupported desktop host OS: $hostOs. Using non-GPU graphic context") + null + } +} + +@OptIn(ExperimentalSkikoApi::class) +class DirectXGraphicsContext() : GraphicsContext { + // Note: we don't close `context` and `texture` after using, + // because it is created once in the main function + private val context = DirectXOffscreenContext() + private var texture: DirectXOffscreenContext.Texture? = null + + override fun surface(width: Int, height: Int): Surface { + texture?.close() + texture = context.Texture(width, height) + return Surface.makeFromBackendRenderTarget( + context.directContext, + texture!!.backendRenderTarget, + SurfaceOrigin.TOP_LEFT, + SurfaceColorFormat.BGRA_8888, + ColorSpace.sRGB, + SurfaceProps(pixelGeometry = PixelGeometry.UNKNOWN) + ) ?: throw IllegalStateException("Can't create Surface") + } + + override suspend fun awaitGPUCompletion() { + texture?.waitForCompletion() + } +} diff --git a/compose/mpp/benchmarks/src/desktopMain/kotlin/main.desktop.kt b/compose/mpp/benchmarks/src/desktopMain/kotlin/main.desktop.kt new file mode 100644 index 0000000000000..53da7526ca278 --- /dev/null +++ b/compose/mpp/benchmarks/src/desktopMain/kotlin/main.desktop.kt @@ -0,0 +1,51 @@ +/* + * Copyright 2020-2021 JetBrains s.r.o. and respective authors and developers. + * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. + */ + +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Window +import androidx.compose.ui.window.application +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 val isSvgSupported: Boolean = true + +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) + + if (Config.runServer) { + // Start the benchmark server to receive results from browsers + BenchmarksSaveServer.start() + } else if (Config.isModeEnabled(Mode.REAL) || Config.isModeEnabled(Mode.STARTUP)) { + val device = GraphicsEnvironment.getLocalGraphicsEnvironment().defaultScreenDevice + val frameRate = device.displayMode.refreshRate.takeIf { it > 0 } ?: 120 + + application { + Window( + onCloseRequest = ::exitApplication, + alwaysOnTop = true, + state = rememberWindowState( + width = 1920.dp, height = 1080.dp + ) + ) { + BenchmarkRunner(getBenchmarks(), frameRate, { System.exit(0) }) + } + } + } else { + runBlocking(Dispatchers.Main) { runBenchmarks(graphicsContext = graphicsContext()) } + } +} diff --git a/compose/mpp/benchmarks/src/desktopMain/kotlin/runGC.jvm.kt b/compose/mpp/benchmarks/src/desktopMain/kotlin/runGC.jvm.kt new file mode 100644 index 0000000000000..3095a116b1ca0 --- /dev/null +++ b/compose/mpp/benchmarks/src/desktopMain/kotlin/runGC.jvm.kt @@ -0,0 +1,3 @@ +actual fun runGC() { + System.gc() +} \ No newline at end of file diff --git a/compose/mpp/benchmarks/src/iosMain/kotlin/Platform.ios.kt b/compose/mpp/benchmarks/src/iosMain/kotlin/Platform.ios.kt new file mode 100644 index 0000000000000..3f7ddd75eda31 --- /dev/null +++ b/compose/mpp/benchmarks/src/iosMain/kotlin/Platform.ios.kt @@ -0,0 +1,12 @@ +/* + * Copyright 2020-2021 JetBrains s.r.o. and respective authors and developers. + * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. + */ + +import kotlin.time.TimeSource + +actual val mainTime: TimeSource.Monotonic.ValueTimeMark = TimeSource.Monotonic.markNow() + +actual val printJsonToOutput: Boolean = true + +actual val isSvgSupported: Boolean = true diff --git a/compose/mpp/benchmarks/src/iosMain/kotlin/main.ios.kt b/compose/mpp/benchmarks/src/iosMain/kotlin/main.ios.kt new file mode 100644 index 0000000000000..8c4f5623fa6b6 --- /dev/null +++ b/compose/mpp/benchmarks/src/iosMain/kotlin/main.ios.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2020-2021 JetBrains s.r.o. and respective authors and developers. + * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. + */ + +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.window.ComposeUIViewController +import kotlinx.coroutines.MainScope +import kotlinx.coroutines.launch +import platform.UIKit.UIApplication +import platform.UIKit.UIScreen +import platform.UIKit.UIViewController +import kotlin.system.exitProcess + +fun setGlobalFromArgs(args : List) { + Config.setGlobalFromArgs(args.toTypedArray()) +} + +fun runReal() = Config.isModeEnabled(Mode.REAL) || Config.isModeEnabled(Mode.STARTUP) + +fun runBenchmarks() { + UIApplication.sharedApplication.setIdleTimerDisabled(true) + MainScope().launch { + runBenchmarks(graphicsContext = graphicsContext()) + println("Completed!") + exitProcess(0) + } +} + +@OptIn(ExperimentalComposeUiApi::class) +fun MainViewController(): UIViewController { + return ComposeUIViewController(configure = { parallelRendering = Config.parallelRendering }) { + BenchmarkRunner(getBenchmarks(), UIScreen.mainScreen.maximumFramesPerSecond.toInt(), { exitProcess(0) }) + } +} diff --git a/compose/mpp/benchmarks/src/jsMain/kotlin/Main.js.kt b/compose/mpp/benchmarks/src/jsMain/kotlin/Main.js.kt new file mode 100644 index 0000000000000..4e3892edd68a5 --- /dev/null +++ b/compose/mpp/benchmarks/src/jsMain/kotlin/Main.js.kt @@ -0,0 +1,4 @@ + +fun main() { + mainBrowser() +} \ No newline at end of file diff --git a/compose/mpp/benchmarks/src/macosMain/kotlin/main.macos.kt b/compose/mpp/benchmarks/src/macosMain/kotlin/main.macos.kt new file mode 100644 index 0000000000000..2099537f9b1e4 --- /dev/null +++ b/compose/mpp/benchmarks/src/macosMain/kotlin/main.macos.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2020-2021 JetBrains s.r.o. and respective authors and developers. + * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. + */ +import androidx.compose.ui.unit.DpSize +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Window +import kotlinx.coroutines.runBlocking +import platform.AppKit.NSApp +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 printJsonToOutput: Boolean = false + +actual val isSvgSupported: Boolean = true + +fun main(args : Array) { + Config.setGlobalFromArgs(args) + if (Config.isModeEnabled(Mode.REAL) || Config.isModeEnabled(Mode.STARTUP)) { + NSApplication.sharedApplication() + val frameRate = (NSScreen.mainScreen?.maximumFramesPerSecond?.toInt()) ?: 120 + Window( + "Benchmarks", + DpSize( + width = 1920.dp, height = 1080.dp + ) + ) { + BenchmarkRunner(getBenchmarks(), frameRate, { exitProcess(0) }) + } + NSApp?.run() + } else { + runBlocking { runBenchmarks(graphicsContext = graphicsContext()) } + } +} \ No newline at end of file diff --git a/compose/mpp/benchmarks/src/wasmJsMain/kotlin/Main.wasmJs.kt b/compose/mpp/benchmarks/src/wasmJsMain/kotlin/Main.wasmJs.kt new file mode 100644 index 0000000000000..c3d7a73c2afdb --- /dev/null +++ b/compose/mpp/benchmarks/src/wasmJsMain/kotlin/Main.wasmJs.kt @@ -0,0 +1,64 @@ +import kotlinx.coroutines.MainScope +import kotlinx.coroutines.promise +import kotlin.js.Promise + +val jsOne = 1.toJsNumber() + +fun main(args: Array) { + if (isD8env().toBoolean()) { + mainD8(args) + } else { + mainBrowser() + } +} + +// Currently, the initialization can't be adjusted to avoid calling the fun main, but +// we don't want use the default fun main, because Jetstream3 requires running the workloads separately / independently of each other. +// Also, they require that a benchmark completes before the function exists, which is not possible with if they just call fun main. +// Therefore, they'll rely on fun customLaunch, which returns a Promise (can be awaited for). +fun mainD8(args: Array) { + println("mainD8 is intentionally doing nothing. Read the comments in main.wasmJs.kt") +} + +private val basicConfigForD8 = Config( + // Using only SIMPLE mode, because VSYNC_EMULATION calls delay(...), + // which is implemented via setTimeout on web targets. + // setTimeout implementation is simplified in D8, making + // the VSYNC_EMULATION mode meaningless when running in D8 + modes = setOf(Mode.SIMPLE), + // MultipleComponents is unsupported, because it uses vector icons. D8 doesn't provide XML parsing APIs. + // But there is an alternative workload called 'MultipleComponents-NoVectorGraphics' + disabledBenchmarks = setOf("MultipleComponents"), +) + +@JsExport +fun customLaunch(benchmarkName: String, frameCount: Int): Promise { + val config = basicConfigForD8.copy( + benchmarks = mapOf(benchmarkName.uppercase() to frameCount) + ) + Config.setGlobal(config) + + return MainScope().promise { + runBenchmarks(warmupCount = 0) + jsOne + } +} + +@JsExport +fun d8BenchmarksRunner(args: String): Promise { + val config = Args.parseArgs(args.split(" ").toTypedArray()) + .copy( + modes = basicConfigForD8.modes, + disabledBenchmarks = basicConfigForD8.disabledBenchmarks + ) + + Config.setGlobal(config) + + return MainScope().promise { + runBenchmarks() + jsOne + } +} + +private fun isD8env(): JsBoolean = + js("typeof isD8 !== 'undefined'") \ No newline at end of file diff --git a/compose/mpp/benchmarks/src/webMain/kotlin/BenchmarksSave.web.kt b/compose/mpp/benchmarks/src/webMain/kotlin/BenchmarksSave.web.kt new file mode 100644 index 0000000000000..34489ac9400f7 --- /dev/null +++ b/compose/mpp/benchmarks/src/webMain/kotlin/BenchmarksSave.web.kt @@ -0,0 +1,116 @@ +/* + * Copyright 2020-2025 JetBrains s.r.o. and respective authors and developers. + * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. + */ + +import io.ktor.client.HttpClient +import io.ktor.client.plugins.contentnegotiation.ContentNegotiation +import io.ktor.client.request.get +import io.ktor.client.request.post +import io.ktor.client.request.setBody +import io.ktor.http.ContentType +import io.ktor.http.HttpStatusCode +import io.ktor.http.contentType +import io.ktor.serialization.kotlinx.json.json +import kotlinx.browser.window +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlinx.coroutines.yield +import kotlinx.serialization.Serializable + +/** + * Browser implementation for saving benchmark results. + * Instead of trying to save directly to disk (which would fail with UnsupportedOperationException), + * this implementation sends the results to a server via HTTP. + */ +actual fun saveBenchmarkStats(name: String, stats: BenchmarkStats) { + GlobalScope.launch { + BenchmarksSaveServerClient.sendBenchmarkResult(name, stats) + } +} + +actual val printJsonToOutput: Boolean = false + +/** + * Client for sending benchmark results to the server + */ +object BenchmarksSaveServerClient { + private val client = HttpClient { + install(ContentNegotiation) { + json() + } + } + + private var resultsSendingInProgress = 0 + + private fun serverUrlRoot(): String { + val protocol = window.location.protocol + val hostname = window.location.hostname + return "$protocol//$hostname:$BENCHMARK_SERVER_PORT" + } + + private fun serverUrl(): String { + return "${serverUrlRoot()}/benchmark" + } + + /** + * Sends benchmark results to the server + */ + suspend fun sendBenchmarkResult(name: String, stats: BenchmarkStats) { + resultsSendingInProgress++ + println("Sending results: $name") + sendBenchmarkResult(name, stats.toJsonString()) + resultsSendingInProgress-- + println("Benchmark result sent to server: ${serverUrl()}") + } + + private suspend fun sendBenchmarkResult(name: String, stats: String) { + try { + + val result = BenchmarkResultToServer( + name = name, + stats = stats + ) + + client.post(serverUrl()) { + contentType(ContentType.Application.Json) + setBody(result) + } + } catch (e: Throwable) { + println("Error sending benchmark result to server: ${e.message}") + } + } + + suspend fun stopServer() { + while (resultsSendingInProgress > 0) { + yield() + } + sendBenchmarkResult("", "") + } + + suspend fun isServerAlive(): Boolean { + // waiting for the server to start for 2 seconds + val TIMEOUT = 2000 + val DELTA = 100L + var delayed = 0L + while (delayed < TIMEOUT) { + try { + return client.get(serverUrlRoot()).status == HttpStatusCode.OK + } catch (_: Throwable) { + delayed += DELTA + delay(DELTA) + } + } + return false + } +} + +/** + * Data class for sending benchmark results to the server + */ +@Serializable +data class BenchmarkResultToServer( + val name: String, + val stats: String // JSON string of BenchmarkStats +) \ No newline at end of file diff --git a/compose/mpp/benchmarks/src/webMain/kotlin/Main.web.kt b/compose/mpp/benchmarks/src/webMain/kotlin/Main.web.kt new file mode 100644 index 0000000000000..4d733e63434aa --- /dev/null +++ b/compose/mpp/benchmarks/src/webMain/kotlin/Main.web.kt @@ -0,0 +1,67 @@ +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.window.ComposeViewport +import kotlinx.browser.document +import kotlinx.browser.window +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() + +actual val isSvgSupported: Boolean = true + +@OptIn(ExperimentalComposeUiApi::class, ExperimentalWasmJsInterop::class) +fun mainBrowser() = MainScope().launch { + awaitSkikoWasm() + + val urlParams = URLSearchParams(window.location.search.toJsString()) + var i = 0 + val args = generateSequence { urlParams.get("arg${i++}") }.toList().toTypedArray() + + Config.setGlobalFromArgs(args) + + val composeRoot = document.getElementById("root") ?: error("No root element found") + 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() + GlobalScope.launch { + if (BenchmarksSaveServerClient.isServerAlive()) { + BenchmarksSaveServerClient.stopServer() + } + } + }) + } + } else { + composeRoot.remove() + println("Wait for the benchmarks to complete...\n") + MainScope().launch { + if (Config.saveStats() && !BenchmarksSaveServerClient.isServerAlive()) { + println("No benchmark server found.") + return@launch + } + + runBenchmarks() + println("Completed!") + if (BenchmarksSaveServerClient.isServerAlive()) { + GlobalScope.launch { + BenchmarksSaveServerClient.stopServer() + } + } + } + } +} + +private suspend fun awaitSkikoWasm() { + suspendCancellableCoroutine { c -> + @Suppress("INVISIBLE_REFERENCE") + org.jetbrains.skiko.wasm.onWasmReady { + c.resumeWith(Result.success(Unit)) + } + } +} diff --git a/compose/mpp/benchmarks/src/webMain/kotlin/runGC.web.kt b/compose/mpp/benchmarks/src/webMain/kotlin/runGC.web.kt new file mode 100644 index 0000000000000..461e709899404 --- /dev/null +++ b/compose/mpp/benchmarks/src/webMain/kotlin/runGC.web.kt @@ -0,0 +1,3 @@ +actual fun runGC() { + js("(typeof gc === 'function')? gc() : console.log('Manual GC is not available. Ensure that the browser was started with the appropriate flags.')") +} \ No newline at end of file diff --git a/compose/mpp/benchmarks/src/webMain/resources/index.html b/compose/mpp/benchmarks/src/webMain/resources/index.html new file mode 100644 index 0000000000000..196358702e835 --- /dev/null +++ b/compose/mpp/benchmarks/src/webMain/resources/index.html @@ -0,0 +1,22 @@ + + + + + Benchmarks Compose + K/Wasm + + + + + +

+ + \ No newline at end of file diff --git a/compose/mpp/benchmarks/src/webMain/resources/launcher.mjs b/compose/mpp/benchmarks/src/webMain/resources/launcher.mjs new file mode 100644 index 0000000000000..d6ec6a0fef898 --- /dev/null +++ b/compose/mpp/benchmarks/src/webMain/resources/launcher.mjs @@ -0,0 +1,5 @@ +import './polyfills.mjs'; +import { d8BenchmarksRunner } from './mpp-benchmarks.mjs'; + +await d8BenchmarksRunner(Array.from(arguments).join(' ')); +console.log('Finished'); diff --git a/compose/mpp/benchmarks/src/webMain/resources/launcher_jetstream3.mjs b/compose/mpp/benchmarks/src/webMain/resources/launcher_jetstream3.mjs new file mode 100644 index 0000000000000..bfbdb1a69ff98 --- /dev/null +++ b/compose/mpp/benchmarks/src/webMain/resources/launcher_jetstream3.mjs @@ -0,0 +1,16 @@ +import './polyfills.mjs'; +import { customLaunch } from './mpp-benchmarks.mjs'; + +/* + AnimatedVisibility, + LazyGrid, + LazyGrid-ItemLaunchedEffect, + LazyGrid-SmoothScroll, + LazyGrid-SmoothScroll-ItemLaunchedEffect, + VisualEffects, + MultipleComponents-NoVectorGraphics + */ +let name = arguments[0] ? arguments[0] : 'AnimatedVisibility'; +let frameCount = arguments[1] ? parseInt(arguments[1]) : 1000; +await customLaunch(name, frameCount); +console.log('Finished'); diff --git a/compose/mpp/benchmarks/src/webMain/resources/polyfills.mjs b/compose/mpp/benchmarks/src/webMain/resources/polyfills.mjs new file mode 100644 index 0000000000000..eb73783e4fa7b --- /dev/null +++ b/compose/mpp/benchmarks/src/webMain/resources/polyfills.mjs @@ -0,0 +1,83 @@ +// polyfills.mjs +globalThis.isD8 = true; + +if (typeof globalThis.window === 'undefined') { + globalThis.window = globalThis; +} +if (typeof globalThis.navigator === 'undefined') { + globalThis.navigator = {}; +} +if (!globalThis.navigator.languages) { + globalThis.navigator.languages = ['en-US', 'en']; + globalThis.navigator.userAgent = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36'; + globalThis.navigator.platform = "MacIntel"; +} + +// Compose reads `window.isSecureContext` in its Clipboard feature: +globalThis.isSecureContext = false; + +if (!globalThis.gc) { + // No GC control in D8 + globalThis.gc = () => { + // console.log('gc called'); + }; +} + +if (typeof globalThis.AbortController === 'undefined') { + globalThis.AbortController = class { + constructor() { + this.signal = {}; + } + abort() {} + }; +} + +// Minimal Blob polyfill +class BlobPolyfill { + constructor(uint8, type = '') { + this._uint8 = uint8; + this._type = type; + } + get size() { + return this._uint8.byteLength; + } + get type() { return this._type; } + async arrayBuffer() { + console.log('arrayBuffer called'); + return this._uint8.buffer; + } + slice(start = 0, end = this.size, type = this.type) { + return new BlobPolyfill(this._uint8.slice(start, end), type); + } +} + +const runtimeDirectory = String(import.meta.url) + .replace(/^file:\/\//, '') + .replace(/\/[^/]+$/, ''); + +globalThis.fetch = async (request) => { + const resourcePath = String(request) + .replace(/^file:\/\//, '') + .replace(/^\.\//, ''); + const filePath = resourcePath.startsWith('/') + ? resourcePath + : `${runtimeDirectory}/${resourcePath}`; + console.log('fetch', resourcePath); + let data; + try { + data = read(filePath, 'binary'); + } catch (error) { + console.error(`Failed to read D8 resource ${filePath}:`, error); + throw error; + } + + const uint8 = new Uint8Array(data); + + return { + ok: true, + status: 200, + async blob() { + return new BlobPolyfill(uint8, 'application/xml'); + }, + }; +}; diff --git a/compose/mpp/benchmarks/src/webMain/resources/styles.css b/compose/mpp/benchmarks/src/webMain/resources/styles.css new file mode 100644 index 0000000000000..85a9c38ec952b --- /dev/null +++ b/compose/mpp/benchmarks/src/webMain/resources/styles.css @@ -0,0 +1,28 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +html, body { + width: 100%; + height: 100%; + margin: 0; + padding: 0; +} + +#root { + width: 100%; + height: 100%; +} diff --git a/settings-fork.gradle b/settings-fork.gradle index 06f192a2c2101..a345df671220d 100644 --- a/settings-fork.gradle +++ b/settings-fork.gradle @@ -337,6 +337,7 @@ includeProject(":compose:animation:animation-core-lint") includeProject(":compose:animation:animation-graphics") includeProject(":compose:desktop:desktop") includeProject(":compose:desktop:desktop:desktop-samples", "compose/desktop/desktop/samples") +includeProject(":compose:mpp:benchmarks") includeProject(":compose:mpp") includeProject(":compose:mpp:demo") includeProject(":compose:mpp:demo-swiftui") From 755c5a3dcaa5a97c3d57e0224d0cc5bc324d7be2 Mon Sep 17 00:00:00 2001 From: Konstantin Date: Mon, 9 Nov 2026 16:40:51 +0100 Subject: [PATCH 02/61] Extract the multiplatform benchmark runtime --- .../benchmarks-runtime/build.gradle.kts | 44 +++++ .../benchmarks-runtime/gradle.properties | 1 + .../compose/benchmarks/Platform.apple.kt | 89 +++++++++++ .../benchmarks/BenchmarkComposeScene.kt | 54 +++++++ .../compose/benchmarks/GraphicsContext.kt | 29 ++++ .../compose/benchmarks/Measurements.kt | 150 ++++++++++++++++++ .../jetbrains/compose/benchmarks/Platform.kt | 22 +++ .../compose/benchmarks/Platform.jvm.kt | 68 ++++++++ .../compose/benchmarks/Platform.web.kt | 26 +++ settings-fork.gradle | 1 + 10 files changed, 484 insertions(+) create mode 100644 compose/mpp/newbenchmarks/benchmarks-runtime/build.gradle.kts create mode 100644 compose/mpp/newbenchmarks/benchmarks-runtime/gradle.properties create mode 100644 compose/mpp/newbenchmarks/benchmarks-runtime/src/appleMain/kotlin/org/jetbrains/compose/benchmarks/Platform.apple.kt create mode 100644 compose/mpp/newbenchmarks/benchmarks-runtime/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/BenchmarkComposeScene.kt create mode 100644 compose/mpp/newbenchmarks/benchmarks-runtime/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/GraphicsContext.kt create mode 100644 compose/mpp/newbenchmarks/benchmarks-runtime/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/Measurements.kt create mode 100644 compose/mpp/newbenchmarks/benchmarks-runtime/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/Platform.kt create mode 100644 compose/mpp/newbenchmarks/benchmarks-runtime/src/jvmMain/kotlin/org/jetbrains/compose/benchmarks/Platform.jvm.kt create mode 100644 compose/mpp/newbenchmarks/benchmarks-runtime/src/webMain/kotlin/org/jetbrains/compose/benchmarks/Platform.web.kt diff --git a/compose/mpp/newbenchmarks/benchmarks-runtime/build.gradle.kts b/compose/mpp/newbenchmarks/benchmarks-runtime/build.gradle.kts new file mode 100644 index 0000000000000..76b66b37c9c5b --- /dev/null +++ b/compose/mpp/newbenchmarks/benchmarks-runtime/build.gradle.kts @@ -0,0 +1,44 @@ +/* + * Copyright 2026 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:OptIn(ExperimentalWasmDsl::class) + +import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl + +plugins { + id("org.jetbrains.kotlin.multiplatform") + id("org.jetbrains.kotlin.plugin.compose") version libs.versions.kotlin.get() + id("org.jetbrains.kotlin.plugin.serialization") version libs.versions.kotlin.get() +} + +kotlin { + jvm() + + listOf( + iosArm64(), + iosSimulatorArm64() + ) + macosArm64() + + wasmJs { browser() } + js { browser() } + + sourceSets { + commonMain.dependencies { + implementation(project(":compose:ui:ui")) + } + } +} diff --git a/compose/mpp/newbenchmarks/benchmarks-runtime/gradle.properties b/compose/mpp/newbenchmarks/benchmarks-runtime/gradle.properties new file mode 100644 index 0000000000000..d15154e785f1e --- /dev/null +++ b/compose/mpp/newbenchmarks/benchmarks-runtime/gradle.properties @@ -0,0 +1 @@ +org.jetbrains.compose.experimental.macos.enabled=true diff --git a/compose/mpp/newbenchmarks/benchmarks-runtime/src/appleMain/kotlin/org/jetbrains/compose/benchmarks/Platform.apple.kt b/compose/mpp/newbenchmarks/benchmarks-runtime/src/appleMain/kotlin/org/jetbrains/compose/benchmarks/Platform.apple.kt new file mode 100644 index 0000000000000..6a3b06afd732e --- /dev/null +++ b/compose/mpp/newbenchmarks/benchmarks-runtime/src/appleMain/kotlin/org/jetbrains/compose/benchmarks/Platform.apple.kt @@ -0,0 +1,89 @@ +/* + * Copyright 2026 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jetbrains.compose.benchmarks + +import kotlin.coroutines.resume +import kotlin.coroutines.suspendCoroutine +import kotlin.native.runtime.GC +import kotlin.native.runtime.NativeRuntimeApi +import kotlinx.cinterop.ExperimentalForeignApi +import kotlinx.cinterop.objcPtr +import org.jetbrains.skia.BackendRenderTarget +import org.jetbrains.skia.ColorSpace +import org.jetbrains.skia.DirectContext +import org.jetbrains.skia.PixelGeometry +import org.jetbrains.skia.Surface +import org.jetbrains.skia.SurfaceColorFormat +import org.jetbrains.skia.SurfaceOrigin +import org.jetbrains.skia.SurfaceProps +import platform.Metal.MTLCreateSystemDefaultDevice +import platform.Metal.MTLPixelFormatBGRA8Unorm +import platform.Metal.MTLTextureDescriptor +import platform.Metal.MTLTextureType2D +import platform.Metal.MTLTextureUsageRenderTarget +import platform.Metal.MTLTextureUsageShaderRead +import platform.Metal.MTLTextureUsageShaderWrite + +@OptIn(NativeRuntimeApi::class) +internal actual object Platform { + actual fun getGraphicsContext(): GraphicsContext = AppleGraphicsContext() + + actual fun runGC() { + GC.collect() + } +} + +@OptIn(ExperimentalForeignApi::class) +private class AppleGraphicsContext : GraphicsContext { + private val device = MTLCreateSystemDefaultDevice() ?: throw IllegalStateException("Can't create MTLDevice") + private val commandQueue = device.newCommandQueue() ?: throw IllegalStateException("Can't create MTLCommandQueue") + private val directContext = DirectContext.makeMetal(device.objcPtr(), commandQueue.objcPtr()) + + override fun createSurface(width: Int, height: Int): Surface { + val descriptor = MTLTextureDescriptor() + descriptor.width = width.toULong() + descriptor.height = height.toULong() + descriptor.usage = MTLTextureUsageShaderRead or MTLTextureUsageShaderWrite or MTLTextureUsageRenderTarget + descriptor.textureType = MTLTextureType2D + descriptor.pixelFormat = MTLPixelFormatBGRA8Unorm + descriptor.mipmapLevelCount = 1UL + + val texture = device.newTextureWithDescriptor(descriptor) ?: throw IllegalStateException("Can't create MTLTexture") + val renderTarget = BackendRenderTarget.makeMetal(width, height, texture.objcPtr()) + + return Surface.makeFromBackendRenderTarget( + directContext, + renderTarget, + SurfaceOrigin.TOP_LEFT, + SurfaceColorFormat.BGRA_8888, + ColorSpace.sRGB, + SurfaceProps(pixelGeometry = PixelGeometry.UNKNOWN) + ) ?: throw IllegalStateException("Can't create Surface") + } + + override suspend fun awaitGPUCompletion() { + val commandBuffer = commandQueue.commandBuffer() ?: return + suspendCoroutine { continuation -> + commandBuffer.addCompletedHandler { + continuation.resume(Unit) + } + + commandBuffer.commit() + } + } + +} \ No newline at end of file diff --git a/compose/mpp/newbenchmarks/benchmarks-runtime/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/BenchmarkComposeScene.kt b/compose/mpp/newbenchmarks/benchmarks-runtime/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/BenchmarkComposeScene.kt new file mode 100644 index 0000000000000..e05369380791d --- /dev/null +++ b/compose/mpp/newbenchmarks/benchmarks-runtime/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/BenchmarkComposeScene.kt @@ -0,0 +1,54 @@ +/* + * Copyright 2026 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jetbrains.compose.benchmarks + +import androidx.compose.runtime.Composable +import androidx.compose.ui.InternalComposeUiApi +import androidx.compose.ui.graphics.Canvas +import androidx.compose.ui.platform.FrameRecomposer +import androidx.compose.ui.platform.registerSkikoComposeImplementation +import androidx.compose.ui.scene.CanvasLayersComposeScene +import androidx.compose.ui.unit.IntSize +import kotlinx.coroutines.Dispatchers + +@OptIn(InternalComposeUiApi::class) +internal class BenchmarkComposeScene(width: Int, height: Int) { + init { + registerSkikoComposeImplementation() + } + + private val frameRecomposer = FrameRecomposer(Dispatchers.Unconfined) + private val scene = CanvasLayersComposeScene( + frameRecomposer = frameRecomposer, + size = IntSize(width, height) + ) + + fun setContent(content: @Composable () -> Unit) { + scene.setContent(content = content) + } + + fun render(canvas: Canvas, nanoTime: Long) { + frameRecomposer.performFrame(nanoTime) + scene.measureAndLayout() + scene.draw(canvas) + } + + fun close() { + scene.close() + frameRecomposer.close() + } +} diff --git a/compose/mpp/newbenchmarks/benchmarks-runtime/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/GraphicsContext.kt b/compose/mpp/newbenchmarks/benchmarks-runtime/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/GraphicsContext.kt new file mode 100644 index 0000000000000..7ccd0ff224c44 --- /dev/null +++ b/compose/mpp/newbenchmarks/benchmarks-runtime/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/GraphicsContext.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2026 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jetbrains.compose.benchmarks + +import org.jetbrains.skia.Surface + +interface GraphicsContext { + fun createSurface(width: Int, height: Int): Surface + suspend fun awaitGPUCompletion() +} + +internal object EmptyGraphicsContext : GraphicsContext { + override fun createSurface(width: Int, height: Int): Surface = Surface.makeNull(width, height) + override suspend fun awaitGPUCompletion() = Unit +} \ No newline at end of file diff --git a/compose/mpp/newbenchmarks/benchmarks-runtime/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/Measurements.kt b/compose/mpp/newbenchmarks/benchmarks-runtime/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/Measurements.kt new file mode 100644 index 0000000000000..b5842b42fe029 --- /dev/null +++ b/compose/mpp/newbenchmarks/benchmarks-runtime/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/Measurements.kt @@ -0,0 +1,150 @@ +/* + * Copyright 2026 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jetbrains.compose.benchmarks + +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.asComposeCanvas +import kotlin.time.Duration +import kotlin.time.Duration.Companion.nanoseconds +import kotlin.time.DurationUnit +import kotlin.time.TimeSource.Monotonic.markNow +import kotlin.time.measureTime +import kotlinx.coroutines.delay +import kotlinx.coroutines.yield +import org.jetbrains.skia.Color +import org.jetbrains.skia.PictureRecorder +import org.jetbrains.skia.Rect +import org.jetbrains.skia.Surface + +class ContentMeasurement( + private val width: Int, + private val height: Int, + private val targetFps: Int, + private val warmupCount: Int, + private val content: @Composable () -> Unit +) { + class FrameInfo(val cpu: Duration, val gpu: Duration, val fps: Double) + + private val graphicsContext = Platform.getGraphicsContext() + private val pictureRecorder = PictureRecorder() + private val nanosPerFrame = (1.0 / targetFps.toDouble() * 1E9.toLong()).toLong() + + suspend fun measureAverageFrame(frameCount: Int): FrameInfo = measure { scene, surface -> + var gpuTime = Duration.ZERO + val totalTime = measureTime { + eventLoop(frameCount) { frameIndex -> + gpuTime += scene.renderOnSurface(surface, frameIndex * nanosPerFrame) + } + } + val cpuTime = totalTime - gpuTime + FrameInfo( + cpu = cpuTime / frameCount, + gpu = gpuTime / frameCount, + fps = frameCount.toDouble() / totalTime.toDouble(DurationUnit.SECONDS) + ) + } + + suspend fun measureRenderFps(frameCount: Int) = measure { scene, surface -> + var nextVSync = Duration.ZERO + var missedFrames = 0 + val start = markNow() + + val frameInfos = arrayOfNulls(frameCount) + + eventLoop(frameCount) { frameIndex -> + val frameStart = start + nextVSync + val gpuTime = scene.renderOnSurface(surface, frameIndex * nextVSync.inWholeNanoseconds) + val frameTime = frameStart.elapsedNow() + val cpuTime = frameTime - gpuTime + frameInfos[frameIndex] = FrameInfo(cpuTime, gpuTime, 1 / frameTime.toDouble(DurationUnit.SECONDS)) + + missedFrames += (frameTime.inWholeNanoseconds / nanosPerFrame).toInt() + nextVSync = ((frameIndex + 1 + missedFrames) * nanosPerFrame).nanoseconds + val timeUntilNextVSync = nextVSync - start.elapsedNow() + if (timeUntilNextVSync > Duration.ZERO) { + // Emulate waiting for next vsync + preciseDelay(timeUntilNextVSync) + } + } + + frameInfos.filterNotNull() + } + + private suspend fun measure(action: suspend (BenchmarkComposeScene, Surface) -> T): T { + val scene = BenchmarkComposeScene(width, height).apply { setContent(content) } + val surface = graphicsContext.createSurface(width, height) + + try { + warmup(scene, surface) + return action(scene, surface) + } finally { + scene.close() + surface.close() + Platform.runGC() + } + } + + private suspend fun warmup(scene: BenchmarkComposeScene, surface: Surface) { + eventLoop(warmupCount) { frameIndex -> + scene.renderOnSurface(surface, frameIndex * nanosPerFrame) + } + Platform.runGC() + } + + /** + * Renders the scene on the given surface at the specified time. + * @return The duration of the GPU rendering operation. + */ + private suspend fun BenchmarkComposeScene.renderOnSurface( + surface: Surface, + time: Long + ): Duration { + val bounds = Rect(0f, 0f, width.toFloat(), height.toFloat()) + val canvas = pictureRecorder.beginRecording(bounds).asComposeCanvas() + render(canvas, time) + val picture = pictureRecorder.finishRecordingAsPicture() + surface.canvas.clear(Color.TRANSPARENT) + surface.canvas.drawPicture(picture) + picture.close() + surface.flushAndSubmit() + return measureTime { graphicsContext.awaitGPUCompletion() } + } + + private suspend inline fun eventLoop(count: Int, action: (frameIndex: Int) -> Unit) { + repeat(count) { + action(it) + yield() + } + } + + private suspend inline fun preciseDelay(duration: Duration) { + val liveDelay: Duration + if (duration.inWholeMilliseconds > 2) { + //experiments have shown that for precise delay we should do live delay at least 2 ms + val delayMillis = duration.inWholeMilliseconds - 2 + val delayStart = markNow() + delay(delayMillis) + liveDelay = duration - delayStart.elapsedNow() + } else { + liveDelay = duration + } + val liveDelayStart = markNow() + while (liveDelayStart.elapsedNow() < liveDelay) { + // block a current thread + } + } +} \ No newline at end of file diff --git a/compose/mpp/newbenchmarks/benchmarks-runtime/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/Platform.kt b/compose/mpp/newbenchmarks/benchmarks-runtime/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/Platform.kt new file mode 100644 index 0000000000000..be473312b01d4 --- /dev/null +++ b/compose/mpp/newbenchmarks/benchmarks-runtime/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/Platform.kt @@ -0,0 +1,22 @@ +/* + * Copyright 2026 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jetbrains.compose.benchmarks + +internal expect object Platform { + fun getGraphicsContext(): GraphicsContext + fun runGC() +} \ No newline at end of file diff --git a/compose/mpp/newbenchmarks/benchmarks-runtime/src/jvmMain/kotlin/org/jetbrains/compose/benchmarks/Platform.jvm.kt b/compose/mpp/newbenchmarks/benchmarks-runtime/src/jvmMain/kotlin/org/jetbrains/compose/benchmarks/Platform.jvm.kt new file mode 100644 index 0000000000000..d0820064bb634 --- /dev/null +++ b/compose/mpp/newbenchmarks/benchmarks-runtime/src/jvmMain/kotlin/org/jetbrains/compose/benchmarks/Platform.jvm.kt @@ -0,0 +1,68 @@ +/* + * Copyright 2026 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jetbrains.compose.benchmarks + +import org.jetbrains.skia.ColorSpace +import org.jetbrains.skia.PixelGeometry +import org.jetbrains.skia.Surface +import org.jetbrains.skia.SurfaceColorFormat +import org.jetbrains.skia.SurfaceOrigin +import org.jetbrains.skia.SurfaceProps +import org.jetbrains.skiko.ExperimentalSkikoApi +import org.jetbrains.skiko.graphicapi.DirectXOffscreenContext +import org.jetbrains.skiko.hostOs + +internal actual object Platform { + actual fun getGraphicsContext(): GraphicsContext = when { + hostOs.isWindows -> { + DirectXGraphicsContext() + } + else -> { + println("Unsupported desktop host OS: $hostOs. Using non-GPU graphic context") + EmptyGraphicsContext + } + } + + actual fun runGC() { + System.gc() + } +} + +@OptIn(ExperimentalSkikoApi::class) +private class DirectXGraphicsContext : GraphicsContext { + // Note: we don't close `context` and `texture` after using, + // because it is created once in the main function + private val context = DirectXOffscreenContext() + private var texture: DirectXOffscreenContext.Texture? = null + + override fun createSurface(width: Int, height: Int): Surface { + texture?.close() + texture = context.Texture(width, height) + return Surface.makeFromBackendRenderTarget( + context.directContext, + texture!!.backendRenderTarget, + SurfaceOrigin.TOP_LEFT, + SurfaceColorFormat.BGRA_8888, + ColorSpace.sRGB, + SurfaceProps(pixelGeometry = PixelGeometry.UNKNOWN) + ) ?: throw IllegalStateException("Can't create Surface") + } + + override suspend fun awaitGPUCompletion() { + texture?.waitForCompletion() + } +} \ No newline at end of file diff --git a/compose/mpp/newbenchmarks/benchmarks-runtime/src/webMain/kotlin/org/jetbrains/compose/benchmarks/Platform.web.kt b/compose/mpp/newbenchmarks/benchmarks-runtime/src/webMain/kotlin/org/jetbrains/compose/benchmarks/Platform.web.kt new file mode 100644 index 0000000000000..a4b676632057c --- /dev/null +++ b/compose/mpp/newbenchmarks/benchmarks-runtime/src/webMain/kotlin/org/jetbrains/compose/benchmarks/Platform.web.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2026 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jetbrains.compose.benchmarks + +@OptIn(ExperimentalWasmJsInterop::class) +internal actual object Platform { + actual fun getGraphicsContext(): GraphicsContext = EmptyGraphicsContext + + actual fun runGC() { + js("(typeof gc === 'function')? gc() : console.log('Manual GC is not available. Ensure that the browser was started with the appropriate flags.')") + } +} \ No newline at end of file diff --git a/settings-fork.gradle b/settings-fork.gradle index a345df671220d..7e1adf70c8191 100644 --- a/settings-fork.gradle +++ b/settings-fork.gradle @@ -339,6 +339,7 @@ includeProject(":compose:desktop:desktop") includeProject(":compose:desktop:desktop:desktop-samples", "compose/desktop/desktop/samples") includeProject(":compose:mpp:benchmarks") includeProject(":compose:mpp") +includeProject(":compose:mpp:newbenchmarks:benchmarks-runtime") includeProject(":compose:mpp:demo") includeProject(":compose:mpp:demo-swiftui") includeProject(":compose:foundation:foundation") From e79d5ed427b109f4df5ae5c151043e82e0d0ca5f Mon Sep 17 00:00:00 2001 From: Konstantin Date: Mon, 9 Nov 2026 17:09:59 +0100 Subject: [PATCH 03/61] Add startup and real-frame measurements --- .../compose/benchmarks/AppMeasurement.kt | 82 +++++++++++++++++++ ...{Measurements.kt => ContentMeasurement.kt} | 2 +- 2 files changed, 83 insertions(+), 1 deletion(-) create mode 100644 compose/mpp/newbenchmarks/benchmarks-runtime/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/AppMeasurement.kt rename compose/mpp/newbenchmarks/benchmarks-runtime/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/{Measurements.kt => ContentMeasurement.kt} (99%) diff --git a/compose/mpp/newbenchmarks/benchmarks-runtime/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/AppMeasurement.kt b/compose/mpp/newbenchmarks/benchmarks-runtime/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/AppMeasurement.kt new file mode 100644 index 0000000000000..105aee55e3033 --- /dev/null +++ b/compose/mpp/newbenchmarks/benchmarks-runtime/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/AppMeasurement.kt @@ -0,0 +1,82 @@ +/* + * Copyright 2026 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jetbrains.compose.benchmarks + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.runtime.withFrameNanos +import kotlin.time.Duration +import kotlin.time.TimeSource.Monotonic + +data class StartupResult( + val timeOfFirstFrame: Monotonic.ValueTimeMark, + val timeOfNthFrame: Monotonic.ValueTimeMark, +) + +@Composable +internal fun MeasureStartup( + frameCount: Int, + onComplete: (StartupResult) -> Unit, + content: @Composable () -> Unit, +) { + require(frameCount > 0) { "frameCount must be positive" } + val currentOnComplete by rememberUpdatedState(onComplete) + + content() + + LaunchedEffect(frameCount) { + withFrameNanos { } + val timeOfFirstFrame = Monotonic.markNow() + repeat(frameCount) { withFrameNanos { } } + val timeOfNthFrame = Monotonic.markNow() + currentOnComplete(StartupResult(timeOfFirstFrame, timeOfNthFrame)) + } +} + +data class RealFramesResult( + val frameTimes: List, + val totalDuration: Duration, +) + +@Composable +internal fun MeasureRealFrames( + frameCount: Int, + onComplete: (RealFramesResult) -> Unit, + content: @Composable () -> Unit, +) { + require(frameCount > 0) { "frameCount must be positive" } + val currentOnComplete by rememberUpdatedState(onComplete) + val frames = remember { arrayOfNulls(frameCount) } + + content() + + LaunchedEffect(frameCount) { + // Skip the initial partial frame interval. + withFrameNanos { } + val start = Monotonic.markNow() + repeat(frameCount) { index -> + val frameStart = Monotonic.markNow() + withFrameNanos { } + frames[index] = frameStart.elapsedNow() + } + val totalDuration = start.elapsedNow() + currentOnComplete(RealFramesResult(frames.filterNotNull(), totalDuration)) + } +} diff --git a/compose/mpp/newbenchmarks/benchmarks-runtime/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/Measurements.kt b/compose/mpp/newbenchmarks/benchmarks-runtime/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/ContentMeasurement.kt similarity index 99% rename from compose/mpp/newbenchmarks/benchmarks-runtime/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/Measurements.kt rename to compose/mpp/newbenchmarks/benchmarks-runtime/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/ContentMeasurement.kt index b5842b42fe029..7d7c95878fcf6 100644 --- a/compose/mpp/newbenchmarks/benchmarks-runtime/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/Measurements.kt +++ b/compose/mpp/newbenchmarks/benchmarks-runtime/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/ContentMeasurement.kt @@ -30,7 +30,7 @@ import org.jetbrains.skia.PictureRecorder import org.jetbrains.skia.Rect import org.jetbrains.skia.Surface -class ContentMeasurement( +internal class ContentMeasurement( private val width: Int, private val height: Int, private val targetFps: Int, From 073df6b8110aac40485fba7dc31c0b737dd722a1 Mon Sep 17 00:00:00 2001 From: Konstantin Date: Mon, 9 Nov 2026 18:01:41 +0100 Subject: [PATCH 04/61] Add startup reports and HTTP reporting --- .../benchmarks-report/build.gradle.kts | 43 ++++++++++++++ .../benchmarks-report/gradle.properties | 1 + .../benchmarks/report/BenchmarkReports.kt | 29 ++++++++++ .../benchmarks-runtime/build.gradle.kts | 6 +- .../compose/benchmarks/AppMeasurement.kt | 4 +- .../compose/benchmarks/BenchmarkRunner.kt | 57 +++++++++++++++++++ .../compose/benchmarks/HttpClient.kt | 31 ++++++++++ settings-fork.gradle | 1 + 8 files changed, 169 insertions(+), 3 deletions(-) create mode 100644 compose/mpp/newbenchmarks/benchmarks-report/build.gradle.kts create mode 100644 compose/mpp/newbenchmarks/benchmarks-report/gradle.properties create mode 100644 compose/mpp/newbenchmarks/benchmarks-report/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/report/BenchmarkReports.kt create mode 100644 compose/mpp/newbenchmarks/benchmarks-runtime/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/BenchmarkRunner.kt create mode 100644 compose/mpp/newbenchmarks/benchmarks-runtime/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/HttpClient.kt diff --git a/compose/mpp/newbenchmarks/benchmarks-report/build.gradle.kts b/compose/mpp/newbenchmarks/benchmarks-report/build.gradle.kts new file mode 100644 index 0000000000000..e261490b3de7f --- /dev/null +++ b/compose/mpp/newbenchmarks/benchmarks-report/build.gradle.kts @@ -0,0 +1,43 @@ +/* + * Copyright 2026 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:OptIn(ExperimentalWasmDsl::class) + +import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl + +plugins { + id("org.jetbrains.kotlin.multiplatform") + id("org.jetbrains.kotlin.plugin.serialization") version libs.versions.kotlin.get() +} + +kotlin { + jvm() + + listOf( + iosArm64(), + iosSimulatorArm64() + ) + macosArm64() + + wasmJs { browser() } + js { browser() } + + sourceSets { + commonMain.dependencies { + implementation(libs.kotlinSerializationCore) + } + } +} diff --git a/compose/mpp/newbenchmarks/benchmarks-report/gradle.properties b/compose/mpp/newbenchmarks/benchmarks-report/gradle.properties new file mode 100644 index 0000000000000..d15154e785f1e --- /dev/null +++ b/compose/mpp/newbenchmarks/benchmarks-report/gradle.properties @@ -0,0 +1 @@ +org.jetbrains.compose.experimental.macos.enabled=true diff --git a/compose/mpp/newbenchmarks/benchmarks-report/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/report/BenchmarkReports.kt b/compose/mpp/newbenchmarks/benchmarks-report/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/report/BenchmarkReports.kt new file mode 100644 index 0000000000000..f81e83a06f15d --- /dev/null +++ b/compose/mpp/newbenchmarks/benchmarks-report/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/report/BenchmarkReports.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2026 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jetbrains.compose.benchmarks.report + +import kotlin.time.Duration +import kotlinx.serialization.Serializable + +@Serializable +data class StartupReport( + val id: String, + val frameCount: Int, + val timeToMain: Duration, + val timeToFirstFrame: Duration, + val timeToNthFrame: Duration, +) diff --git a/compose/mpp/newbenchmarks/benchmarks-runtime/build.gradle.kts b/compose/mpp/newbenchmarks/benchmarks-runtime/build.gradle.kts index 76b66b37c9c5b..2f700027c8b80 100644 --- a/compose/mpp/newbenchmarks/benchmarks-runtime/build.gradle.kts +++ b/compose/mpp/newbenchmarks/benchmarks-runtime/build.gradle.kts @@ -38,7 +38,11 @@ kotlin { sourceSets { commonMain.dependencies { - implementation(project(":compose:ui:ui")) + compileOnly(project(":compose:ui:ui")) + implementation(project(":compose:mpp:newbenchmarks:benchmarks-report")) + implementation("io.ktor:ktor-client-core:3.6.1") + implementation("io.ktor:ktor-client-content-negotiation:3.6.1") + implementation("io.ktor:ktor-serialization-kotlinx-json:3.6.1") } } } diff --git a/compose/mpp/newbenchmarks/benchmarks-runtime/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/AppMeasurement.kt b/compose/mpp/newbenchmarks/benchmarks-runtime/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/AppMeasurement.kt index 105aee55e3033..88b031c24b73b 100644 --- a/compose/mpp/newbenchmarks/benchmarks-runtime/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/AppMeasurement.kt +++ b/compose/mpp/newbenchmarks/benchmarks-runtime/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/AppMeasurement.kt @@ -25,7 +25,7 @@ import androidx.compose.runtime.withFrameNanos import kotlin.time.Duration import kotlin.time.TimeSource.Monotonic -data class StartupResult( +internal data class StartupResult( val timeOfFirstFrame: Monotonic.ValueTimeMark, val timeOfNthFrame: Monotonic.ValueTimeMark, ) @@ -50,7 +50,7 @@ internal fun MeasureStartup( } } -data class RealFramesResult( +internal data class RealFramesResult( val frameTimes: List, val totalDuration: Duration, ) diff --git a/compose/mpp/newbenchmarks/benchmarks-runtime/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/BenchmarkRunner.kt b/compose/mpp/newbenchmarks/benchmarks-runtime/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/BenchmarkRunner.kt new file mode 100644 index 0000000000000..e8fad08af903a --- /dev/null +++ b/compose/mpp/newbenchmarks/benchmarks-runtime/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/BenchmarkRunner.kt @@ -0,0 +1,57 @@ +/* + * Copyright 2026 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jetbrains.compose.benchmarks + +import androidx.compose.runtime.Composable +import io.ktor.client.request.post +import io.ktor.client.request.setBody +import kotlin.time.Duration +import kotlin.time.TimeSource.Monotonic +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.launch +import org.jetbrains.compose.benchmarks.report.StartupReport + +private val ReportingScope = CoroutineScope(Dispatchers.Default + SupervisorJob()) + +@Composable +fun MeasureApplicationStartup( + id: String, + mainMark: Monotonic.ValueTimeMark, + timeToMain: Duration, + reportApiUrl: String, + frameCount: Int, + content: @Composable () -> Unit, +) { + val onComplete: (StartupResult) -> Unit = { result -> + ReportingScope.launch { + val report = StartupReport( + id = id, + frameCount = frameCount, + timeToMain = timeToMain, + timeToFirstFrame = result.timeOfFirstFrame - mainMark, + timeToNthFrame = result.timeOfNthFrame - mainMark, + ) + val client = createHttpClient() + client.post(reportApiUrl) { setBody(report) } + client.close() + } + } + + MeasureStartup(frameCount, onComplete, content) +} \ No newline at end of file diff --git a/compose/mpp/newbenchmarks/benchmarks-runtime/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/HttpClient.kt b/compose/mpp/newbenchmarks/benchmarks-runtime/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/HttpClient.kt new file mode 100644 index 0000000000000..5c18a7266edfa --- /dev/null +++ b/compose/mpp/newbenchmarks/benchmarks-runtime/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/HttpClient.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2026 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jetbrains.compose.benchmarks + +import io.ktor.client.HttpClient +import io.ktor.client.plugins.contentnegotiation.ContentNegotiation +import io.ktor.serialization.kotlinx.json.json +import kotlinx.serialization.json.Json + +internal fun createHttpClient(): HttpClient = HttpClient { + val json = Json { + explicitNulls = true + } + install(ContentNegotiation) { + json(json) + } +} \ No newline at end of file diff --git a/settings-fork.gradle b/settings-fork.gradle index 7e1adf70c8191..104228fa96ddb 100644 --- a/settings-fork.gradle +++ b/settings-fork.gradle @@ -340,6 +340,7 @@ includeProject(":compose:desktop:desktop:desktop-samples", "compose/desktop/desk includeProject(":compose:mpp:benchmarks") includeProject(":compose:mpp") includeProject(":compose:mpp:newbenchmarks:benchmarks-runtime") +includeProject(":compose:mpp:newbenchmarks:benchmarks-report") includeProject(":compose:mpp:demo") includeProject(":compose:mpp:demo-swiftui") includeProject(":compose:foundation:foundation") From da5fbd645eb62b9593dd7f224408a42cc359f508 Mon Sep 17 00:00:00 2001 From: Konstantin Date: Mon, 9 Nov 2026 18:04:40 +0100 Subject: [PATCH 05/61] Add real-frame reports and HTTP reporting --- .../benchmarks/report/BenchmarkReports.kt | 8 ++++ .../compose/benchmarks/AppMeasurement.kt | 2 +- .../compose/benchmarks/BenchmarkRunner.kt | 43 ++++++++++++++++++- 3 files changed, 50 insertions(+), 3 deletions(-) diff --git a/compose/mpp/newbenchmarks/benchmarks-report/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/report/BenchmarkReports.kt b/compose/mpp/newbenchmarks/benchmarks-report/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/report/BenchmarkReports.kt index f81e83a06f15d..32c5448c3eeb4 100644 --- a/compose/mpp/newbenchmarks/benchmarks-report/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/report/BenchmarkReports.kt +++ b/compose/mpp/newbenchmarks/benchmarks-report/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/report/BenchmarkReports.kt @@ -27,3 +27,11 @@ data class StartupReport( val timeToFirstFrame: Duration, val timeToNthFrame: Duration, ) + +@Serializable +data class RealFramesReport( + val id: String, + val frameCount: Int, + val frameTimes: List, + val totalDuration: Duration, +) diff --git a/compose/mpp/newbenchmarks/benchmarks-runtime/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/AppMeasurement.kt b/compose/mpp/newbenchmarks/benchmarks-runtime/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/AppMeasurement.kt index 88b031c24b73b..a2e500a5876d6 100644 --- a/compose/mpp/newbenchmarks/benchmarks-runtime/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/AppMeasurement.kt +++ b/compose/mpp/newbenchmarks/benchmarks-runtime/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/AppMeasurement.kt @@ -63,7 +63,7 @@ internal fun MeasureRealFrames( ) { require(frameCount > 0) { "frameCount must be positive" } val currentOnComplete by rememberUpdatedState(onComplete) - val frames = remember { arrayOfNulls(frameCount) } + val frames = remember(frameCount) { arrayOfNulls(frameCount) } content() diff --git a/compose/mpp/newbenchmarks/benchmarks-runtime/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/BenchmarkRunner.kt b/compose/mpp/newbenchmarks/benchmarks-runtime/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/BenchmarkRunner.kt index e8fad08af903a..7a416850c0e5f 100644 --- a/compose/mpp/newbenchmarks/benchmarks-runtime/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/BenchmarkRunner.kt +++ b/compose/mpp/newbenchmarks/benchmarks-runtime/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/BenchmarkRunner.kt @@ -19,12 +19,15 @@ package org.jetbrains.compose.benchmarks import androidx.compose.runtime.Composable import io.ktor.client.request.post import io.ktor.client.request.setBody +import io.ktor.http.ContentType +import io.ktor.http.contentType import kotlin.time.Duration import kotlin.time.TimeSource.Monotonic import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.launch +import org.jetbrains.compose.benchmarks.report.RealFramesReport import org.jetbrains.compose.benchmarks.report.StartupReport private val ReportingScope = CoroutineScope(Dispatchers.Default + SupervisorJob()) @@ -48,10 +51,46 @@ fun MeasureApplicationStartup( timeToNthFrame = result.timeOfNthFrame - mainMark, ) val client = createHttpClient() - client.post(reportApiUrl) { setBody(report) } - client.close() + try { + client.post(reportApiUrl) { + contentType(ContentType.Application.Json) + setBody(report) + } + } finally { + client.close() + } } } MeasureStartup(frameCount, onComplete, content) +} + +@Composable +fun MeasureApplicationRealFrames( + id: String, + reportApiUrl: String, + frameCount: Int, + content: @Composable () -> Unit, +) { + val onComplete: (RealFramesResult) -> Unit = { result -> + ReportingScope.launch { + val report = RealFramesReport( + id = id, + frameCount = frameCount, + frameTimes = result.frameTimes, + totalDuration = result.totalDuration, + ) + val client = createHttpClient() + try { + client.post(reportApiUrl) { + contentType(ContentType.Application.Json) + setBody(report) + } + } finally { + client.close() + } + } + } + + MeasureRealFrames(frameCount, onComplete, content) } \ No newline at end of file From e6bd9f79dec5a76467dbb9fbf606d975681c3ba8 Mon Sep 17 00:00:00 2001 From: Konstantin Date: Mon, 9 Nov 2026 18:07:22 +0100 Subject: [PATCH 06/61] Add configurable real-frame warmup --- .../compose/benchmarks/report/BenchmarkReports.kt | 1 + .../org/jetbrains/compose/benchmarks/AppMeasurement.kt | 7 ++++++- .../org/jetbrains/compose/benchmarks/BenchmarkRunner.kt | 9 ++++++++- 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/compose/mpp/newbenchmarks/benchmarks-report/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/report/BenchmarkReports.kt b/compose/mpp/newbenchmarks/benchmarks-report/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/report/BenchmarkReports.kt index 32c5448c3eeb4..69ec9fc7a3caf 100644 --- a/compose/mpp/newbenchmarks/benchmarks-report/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/report/BenchmarkReports.kt +++ b/compose/mpp/newbenchmarks/benchmarks-report/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/report/BenchmarkReports.kt @@ -31,6 +31,7 @@ data class StartupReport( @Serializable data class RealFramesReport( val id: String, + val warmupCount: Int, val frameCount: Int, val frameTimes: List, val totalDuration: Duration, diff --git a/compose/mpp/newbenchmarks/benchmarks-runtime/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/AppMeasurement.kt b/compose/mpp/newbenchmarks/benchmarks-runtime/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/AppMeasurement.kt index a2e500a5876d6..d88ba90e7ddae 100644 --- a/compose/mpp/newbenchmarks/benchmarks-runtime/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/AppMeasurement.kt +++ b/compose/mpp/newbenchmarks/benchmarks-runtime/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/AppMeasurement.kt @@ -57,19 +57,24 @@ internal data class RealFramesResult( @Composable internal fun MeasureRealFrames( + warmupCount: Int, frameCount: Int, onComplete: (RealFramesResult) -> Unit, content: @Composable () -> Unit, ) { require(frameCount > 0) { "frameCount must be positive" } + require(warmupCount >= 0) { "warmupCount must not be negative" } val currentOnComplete by rememberUpdatedState(onComplete) val frames = remember(frameCount) { arrayOfNulls(frameCount) } content() - LaunchedEffect(frameCount) { + LaunchedEffect(frameCount, warmupCount) { // Skip the initial partial frame interval. withFrameNanos { } + + repeat(warmupCount) { withFrameNanos { } } + val start = Monotonic.markNow() repeat(frameCount) { index -> val frameStart = Monotonic.markNow() diff --git a/compose/mpp/newbenchmarks/benchmarks-runtime/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/BenchmarkRunner.kt b/compose/mpp/newbenchmarks/benchmarks-runtime/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/BenchmarkRunner.kt index 7a416850c0e5f..49129ac1c461b 100644 --- a/compose/mpp/newbenchmarks/benchmarks-runtime/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/BenchmarkRunner.kt +++ b/compose/mpp/newbenchmarks/benchmarks-runtime/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/BenchmarkRunner.kt @@ -69,6 +69,7 @@ fun MeasureApplicationStartup( fun MeasureApplicationRealFrames( id: String, reportApiUrl: String, + warmupCount: Int, frameCount: Int, content: @Composable () -> Unit, ) { @@ -76,6 +77,7 @@ fun MeasureApplicationRealFrames( ReportingScope.launch { val report = RealFramesReport( id = id, + warmupCount = warmupCount, frameCount = frameCount, frameTimes = result.frameTimes, totalDuration = result.totalDuration, @@ -92,5 +94,10 @@ fun MeasureApplicationRealFrames( } } - MeasureRealFrames(frameCount, onComplete, content) + MeasureRealFrames( + warmupCount = warmupCount, + frameCount = frameCount, + onComplete = onComplete, + content = content, + ) } \ No newline at end of file From bc32a2115023981dc1404d2148ce311ce415ecd9 Mon Sep 17 00:00:00 2001 From: Konstantin Date: Mon, 9 Nov 2026 18:21:46 +0100 Subject: [PATCH 07/61] Add average-frame and FPS reporting --- .../benchmarks/report/BenchmarkReports.kt | 29 +++++++ .../compose/benchmarks/BenchmarkRunner.kt | 75 ++++++++++++++++++- .../compose/benchmarks/ContentMeasurement.kt | 2 +- 3 files changed, 104 insertions(+), 2 deletions(-) diff --git a/compose/mpp/newbenchmarks/benchmarks-report/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/report/BenchmarkReports.kt b/compose/mpp/newbenchmarks/benchmarks-report/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/report/BenchmarkReports.kt index 69ec9fc7a3caf..6d66ffcac53d6 100644 --- a/compose/mpp/newbenchmarks/benchmarks-report/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/report/BenchmarkReports.kt +++ b/compose/mpp/newbenchmarks/benchmarks-report/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/report/BenchmarkReports.kt @@ -36,3 +36,32 @@ data class RealFramesReport( val frameTimes: List, val totalDuration: Duration, ) + +@Serializable +data class FrameInfo( + val cpu: Duration, + val gpu: Duration, + val fps: Double, +) + +@Serializable +data class AverageFrameReport( + val id: String, + val width: Int, + val height: Int, + val targetFps: Int, + val warmupCount: Int, + val frameCount: Int, + val averageFrameInfo: FrameInfo, +) + +@Serializable +data class FpsReport( + val id: String, + val width: Int, + val height: Int, + val targetFps: Int, + val warmupCount: Int, + val frameCount: Int, + val frames: List, +) diff --git a/compose/mpp/newbenchmarks/benchmarks-runtime/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/BenchmarkRunner.kt b/compose/mpp/newbenchmarks/benchmarks-runtime/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/BenchmarkRunner.kt index 49129ac1c461b..7b03d42170333 100644 --- a/compose/mpp/newbenchmarks/benchmarks-runtime/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/BenchmarkRunner.kt +++ b/compose/mpp/newbenchmarks/benchmarks-runtime/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/BenchmarkRunner.kt @@ -27,6 +27,9 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.launch +import org.jetbrains.compose.benchmarks.report.AverageFrameReport +import org.jetbrains.compose.benchmarks.report.FpsReport +import org.jetbrains.compose.benchmarks.report.FrameInfo import org.jetbrains.compose.benchmarks.report.RealFramesReport import org.jetbrains.compose.benchmarks.report.StartupReport @@ -100,4 +103,74 @@ fun MeasureApplicationRealFrames( onComplete = onComplete, content = content, ) -} \ No newline at end of file +} + +fun MeasureRenderAverageFrame( + id: String, + reportApiUrl: String, + width: Int, + height: Int, + targetFps: Int, + warmupCount: Int, + frameCount: Int, + content: @Composable () -> Unit +) { + val measurement = ContentMeasurement(width, height, targetFps, warmupCount, content) + + ReportingScope.launch { + val result = measurement.measureAverageFrame(frameCount) + val report = AverageFrameReport( + id = id, + width = width, + height = height, + targetFps = targetFps, + warmupCount = warmupCount, + frameCount = frameCount, + averageFrameInfo = result, + ) + val client = createHttpClient() + try { + client.post(reportApiUrl) { + contentType(ContentType.Application.Json) + setBody(report) + } + } finally { + client.close() + } + } +} + +fun MeasureRenderFps( + id: String, + reportApiUrl: String, + width: Int, + height: Int, + targetFps: Int, + warmupCount: Int, + frameCount: Int, + content: @Composable () -> Unit +) { + val measurement = ContentMeasurement(width, height, targetFps, warmupCount, content) + + ReportingScope.launch { + val result = measurement.measureRenderFps(frameCount) + val report = FpsReport( + id = id, + width = width, + height = height, + targetFps = targetFps, + warmupCount = warmupCount, + frameCount = frameCount, + frames = result, + ) + val client = createHttpClient() + try { + client.post(reportApiUrl) { + contentType(ContentType.Application.Json) + setBody(report) + } + } finally { + client.close() + } + } +} diff --git a/compose/mpp/newbenchmarks/benchmarks-runtime/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/ContentMeasurement.kt b/compose/mpp/newbenchmarks/benchmarks-runtime/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/ContentMeasurement.kt index 7d7c95878fcf6..e77b74a25d38b 100644 --- a/compose/mpp/newbenchmarks/benchmarks-runtime/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/ContentMeasurement.kt +++ b/compose/mpp/newbenchmarks/benchmarks-runtime/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/ContentMeasurement.kt @@ -25,6 +25,7 @@ import kotlin.time.TimeSource.Monotonic.markNow import kotlin.time.measureTime import kotlinx.coroutines.delay import kotlinx.coroutines.yield +import org.jetbrains.compose.benchmarks.report.FrameInfo import org.jetbrains.skia.Color import org.jetbrains.skia.PictureRecorder import org.jetbrains.skia.Rect @@ -37,7 +38,6 @@ internal class ContentMeasurement( private val warmupCount: Int, private val content: @Composable () -> Unit ) { - class FrameInfo(val cpu: Duration, val gpu: Duration, val fps: Double) private val graphicsContext = Platform.getGraphicsContext() private val pictureRecorder = PictureRecorder() From 5ccdede8b92e25b330c0cc4cd3f178d88a5b9763 Mon Sep 17 00:00:00 2001 From: Konstantin Date: Tue, 10 Nov 2026 11:27:26 +0100 Subject: [PATCH 08/61] Add a typed JSON report collector --- .../benchmarks-collector/build.gradle.kts | 31 +++++ .../compose/benchmarks/collector/Main.kt | 107 ++++++++++++++++++ .../benchmarks/report/BenchmarkReports.kt | 41 +++++-- .../compose/benchmarks/HttpClient.kt | 1 + settings-fork.gradle | 1 + 5 files changed, 173 insertions(+), 8 deletions(-) create mode 100644 compose/mpp/newbenchmarks/benchmarks-collector/build.gradle.kts create mode 100644 compose/mpp/newbenchmarks/benchmarks-collector/src/main/kotlin/org/jetbrains/compose/benchmarks/collector/Main.kt diff --git a/compose/mpp/newbenchmarks/benchmarks-collector/build.gradle.kts b/compose/mpp/newbenchmarks/benchmarks-collector/build.gradle.kts new file mode 100644 index 0000000000000..98a70c534dc2f --- /dev/null +++ b/compose/mpp/newbenchmarks/benchmarks-collector/build.gradle.kts @@ -0,0 +1,31 @@ +/* + * Copyright 2026 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +plugins { + id("org.jetbrains.kotlin.jvm") + application +} + +dependencies { + implementation(project(":compose:mpp:newbenchmarks:benchmarks-report")) + implementation("io.ktor:ktor-server-core:3.6.1") + implementation("io.ktor:ktor-server-cio:3.6.1") + implementation(libs.kotlinSerializationJson) +} + +application { + mainClass.set("org.jetbrains.compose.benchmarks.collector.MainKt") +} diff --git a/compose/mpp/newbenchmarks/benchmarks-collector/src/main/kotlin/org/jetbrains/compose/benchmarks/collector/Main.kt b/compose/mpp/newbenchmarks/benchmarks-collector/src/main/kotlin/org/jetbrains/compose/benchmarks/collector/Main.kt new file mode 100644 index 0000000000000..60d9e1fd03a44 --- /dev/null +++ b/compose/mpp/newbenchmarks/benchmarks-collector/src/main/kotlin/org/jetbrains/compose/benchmarks/collector/Main.kt @@ -0,0 +1,107 @@ +/* + * Copyright 2026 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jetbrains.compose.benchmarks.collector + +import io.ktor.http.HttpStatusCode +import io.ktor.server.application.Application +import io.ktor.server.cio.CIO +import io.ktor.server.engine.embeddedServer +import io.ktor.server.request.receiveText +import io.ktor.server.response.respondText +import io.ktor.server.routing.post +import io.ktor.server.routing.routing +import java.nio.file.FileAlreadyExistsException +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.StandardOpenOption.CREATE_NEW +import java.time.Instant +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import kotlinx.serialization.DeserializationStrategy +import kotlinx.serialization.SerializationException +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonContentPolymorphicSerializer +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.jsonPrimitive +import org.jetbrains.compose.benchmarks.report.AverageFrameReport +import org.jetbrains.compose.benchmarks.report.BenchmarkReport +import org.jetbrains.compose.benchmarks.report.BenchmarkReportType +import org.jetbrains.compose.benchmarks.report.FpsReport +import org.jetbrains.compose.benchmarks.report.RealFramesReport +import org.jetbrains.compose.benchmarks.report.StartupReport + +fun main(args: Array) { + require(args.size == 2) { "Usage: benchmarks-collector " } + val port = requireNotNull(args[0].toIntOrNull()?.takeIf { it in 1..65535 }) { + "Port must be an integer between 1 and 65535" + } + val outputDirectory = Path.of(args[1]).toAbsolutePath() + Files.createDirectories(outputDirectory) + + embeddedServer(CIO, host = "127.0.0.1", port = port) { + collectReports(outputDirectory) + }.start(wait = true) +} + +internal fun Application.collectReports(outputDirectory: Path) { + routing { + post("/reports") { + val body = call.receiveText() + val report = try { + Json.decodeFromString(BenchmarkReportSerializer, body) + } catch (_: IllegalArgumentException) { + call.respondText("Invalid benchmark report", status = HttpStatusCode.BadRequest) + return@post + } + + val fileName = withContext(Dispatchers.IO) { + saveReport(outputDirectory, report.type, body) + } + call.respondText(fileName, status = HttpStatusCode.Created) + } + } +} + +private object BenchmarkReportSerializer : + JsonContentPolymorphicSerializer(BenchmarkReport::class) { + override fun selectDeserializer(element: JsonElement): DeserializationStrategy { + val typeElement = (element as? JsonObject)?.get("type") + ?: throw SerializationException("Missing benchmark report type") + val type = Json.decodeFromJsonElement(BenchmarkReportType.serializer(), typeElement) + return when (type) { + BenchmarkReportType.STARTUP -> StartupReport.serializer() + BenchmarkReportType.REAL_FRAMES -> RealFramesReport.serializer() + BenchmarkReportType.AVERAGE_FRAME -> AverageFrameReport.serializer() + BenchmarkReportType.FPS -> FpsReport.serializer() + } + } +} + +private fun saveReport(outputDirectory: Path, type: BenchmarkReportType, body: String): String { + val typeName = Json.encodeToJsonElement( + BenchmarkReportType.serializer(), + type + ).jsonPrimitive.content + val fileName = "$typeName-${Instant.now()}.json" + Files.writeString( + outputDirectory.resolve(fileName), + body, + CREATE_NEW + ) + return fileName +} diff --git a/compose/mpp/newbenchmarks/benchmarks-report/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/report/BenchmarkReports.kt b/compose/mpp/newbenchmarks/benchmarks-report/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/report/BenchmarkReports.kt index 6d66ffcac53d6..eac3be117e353 100644 --- a/compose/mpp/newbenchmarks/benchmarks-report/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/report/BenchmarkReports.kt +++ b/compose/mpp/newbenchmarks/benchmarks-report/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/report/BenchmarkReports.kt @@ -17,25 +17,48 @@ package org.jetbrains.compose.benchmarks.report import kotlin.time.Duration +import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable +@Serializable +enum class BenchmarkReportType { + @SerialName("startup") + STARTUP, + + @SerialName("realFrames") + REAL_FRAMES, + + @SerialName("averageFrame") + AVERAGE_FRAME, + + @SerialName("fps") + FPS, +} + +interface BenchmarkReport { + val id: String + val type: BenchmarkReportType +} + @Serializable data class StartupReport( - val id: String, + override val id: String, + override val type: BenchmarkReportType = BenchmarkReportType.STARTUP, val frameCount: Int, val timeToMain: Duration, val timeToFirstFrame: Duration, val timeToNthFrame: Duration, -) +) : BenchmarkReport @Serializable data class RealFramesReport( - val id: String, + override val id: String, + override val type: BenchmarkReportType = BenchmarkReportType.REAL_FRAMES, val warmupCount: Int, val frameCount: Int, val frameTimes: List, val totalDuration: Duration, -) +) : BenchmarkReport @Serializable data class FrameInfo( @@ -46,22 +69,24 @@ data class FrameInfo( @Serializable data class AverageFrameReport( - val id: String, + override val id: String, + override val type: BenchmarkReportType = BenchmarkReportType.AVERAGE_FRAME, val width: Int, val height: Int, val targetFps: Int, val warmupCount: Int, val frameCount: Int, val averageFrameInfo: FrameInfo, -) +) : BenchmarkReport @Serializable data class FpsReport( - val id: String, + override val id: String, + override val type: BenchmarkReportType = BenchmarkReportType.FPS, val width: Int, val height: Int, val targetFps: Int, val warmupCount: Int, val frameCount: Int, val frames: List, -) +) : BenchmarkReport diff --git a/compose/mpp/newbenchmarks/benchmarks-runtime/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/HttpClient.kt b/compose/mpp/newbenchmarks/benchmarks-runtime/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/HttpClient.kt index 5c18a7266edfa..ced8a709cdf10 100644 --- a/compose/mpp/newbenchmarks/benchmarks-runtime/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/HttpClient.kt +++ b/compose/mpp/newbenchmarks/benchmarks-runtime/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/HttpClient.kt @@ -24,6 +24,7 @@ import kotlinx.serialization.json.Json internal fun createHttpClient(): HttpClient = HttpClient { val json = Json { explicitNulls = true + encodeDefaults = true } install(ContentNegotiation) { json(json) diff --git a/settings-fork.gradle b/settings-fork.gradle index 104228fa96ddb..f2da152eefa0c 100644 --- a/settings-fork.gradle +++ b/settings-fork.gradle @@ -341,6 +341,7 @@ includeProject(":compose:mpp:benchmarks") includeProject(":compose:mpp") includeProject(":compose:mpp:newbenchmarks:benchmarks-runtime") includeProject(":compose:mpp:newbenchmarks:benchmarks-report") +includeProject(":compose:mpp:newbenchmarks:benchmarks-collector") includeProject(":compose:mpp:demo") includeProject(":compose:mpp:demo-swiftui") includeProject(":compose:foundation:foundation") From a56a1f63a2b82591519ee3fb74749ee126344b25 Mon Sep 17 00:00:00 2001 From: Konstantin Date: Tue, 10 Nov 2026 11:29:01 +0100 Subject: [PATCH 09/61] Move web GC into a JavaScript interop function --- .../org/jetbrains/compose/benchmarks/Platform.web.kt | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/compose/mpp/newbenchmarks/benchmarks-runtime/src/webMain/kotlin/org/jetbrains/compose/benchmarks/Platform.web.kt b/compose/mpp/newbenchmarks/benchmarks-runtime/src/webMain/kotlin/org/jetbrains/compose/benchmarks/Platform.web.kt index a4b676632057c..154d4c1237a9c 100644 --- a/compose/mpp/newbenchmarks/benchmarks-runtime/src/webMain/kotlin/org/jetbrains/compose/benchmarks/Platform.web.kt +++ b/compose/mpp/newbenchmarks/benchmarks-runtime/src/webMain/kotlin/org/jetbrains/compose/benchmarks/Platform.web.kt @@ -16,11 +16,15 @@ package org.jetbrains.compose.benchmarks -@OptIn(ExperimentalWasmJsInterop::class) internal actual object Platform { actual fun getGraphicsContext(): GraphicsContext = EmptyGraphicsContext actual fun runGC() { - js("(typeof gc === 'function')? gc() : console.log('Manual GC is not available. Ensure that the browser was started with the appropriate flags.')") + webGC() } +} + +@OptIn(ExperimentalWasmJsInterop::class) +private fun webGC() { + js("(typeof gc === 'function')? gc() : console.log('Manual GC is not available. Ensure that the browser was started with the appropriate flags.')") } \ No newline at end of file From 53c720ccec518a8fd5c1da4943bcd8461ad39932 Mon Sep 17 00:00:00 2001 From: Konstantin Date: Tue, 10 Nov 2026 11:54:29 +0100 Subject: [PATCH 10/61] Add benchmark apps for desktop, web, and iOS --- compose/mpp/newbenchmarks/README.md | 97 +++++ .../benchmarks-app/build.gradle.kts | 80 ++++ .../benchmarks-app/gradle.properties | 1 + .../jetbrains/compose/benchmarks/app/App.kt | 36 ++ .../compose/benchmarks/app/Main.ios.kt | 24 ++ .../compose/benchmarks/app/Main.jvm.kt | 29 ++ .../compose/benchmarks/app/Main.macos.kt | 38 ++ .../compose/benchmarks/app/Main.web.kt | 34 ++ .../src/webMain/resources/index.html | 15 + .../project.pbxproj | 383 ++++++++++++++++++ .../contents.xcworkspacedata | 4 + .../xcschemes/BenchmarksIosApp.xcscheme | 26 ++ .../AccentColor.colorset/Contents.json | 11 + .../AppIcon.appiconset/Contents.json | 98 +++++ .../Assets.xcassets/Contents.json | 6 + .../BenchmarksIosApp/BenchmarksIosApp.swift | 29 ++ .../BenchmarksIosApp/Info.plist | 8 + .../Preview Assets.xcassets/Contents.json | 6 + .../benchmarks-ios-app/README.md | 27 ++ settings-fork.gradle | 1 + 20 files changed, 953 insertions(+) create mode 100644 compose/mpp/newbenchmarks/README.md create mode 100644 compose/mpp/newbenchmarks/benchmarks-app/build.gradle.kts create mode 100644 compose/mpp/newbenchmarks/benchmarks-app/gradle.properties create mode 100644 compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/App.kt create mode 100644 compose/mpp/newbenchmarks/benchmarks-app/src/iosMain/kotlin/org/jetbrains/compose/benchmarks/app/Main.ios.kt create mode 100644 compose/mpp/newbenchmarks/benchmarks-app/src/jvmMain/kotlin/org/jetbrains/compose/benchmarks/app/Main.jvm.kt create mode 100644 compose/mpp/newbenchmarks/benchmarks-app/src/macosMain/kotlin/org/jetbrains/compose/benchmarks/app/Main.macos.kt create mode 100644 compose/mpp/newbenchmarks/benchmarks-app/src/webMain/kotlin/org/jetbrains/compose/benchmarks/app/Main.web.kt create mode 100644 compose/mpp/newbenchmarks/benchmarks-app/src/webMain/resources/index.html create mode 100644 compose/mpp/newbenchmarks/benchmarks-ios-app/BenchmarksIosApp.xcodeproj/project.pbxproj create mode 100644 compose/mpp/newbenchmarks/benchmarks-ios-app/BenchmarksIosApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 compose/mpp/newbenchmarks/benchmarks-ios-app/BenchmarksIosApp.xcodeproj/xcshareddata/xcschemes/BenchmarksIosApp.xcscheme create mode 100644 compose/mpp/newbenchmarks/benchmarks-ios-app/BenchmarksIosApp/Assets.xcassets/AccentColor.colorset/Contents.json create mode 100644 compose/mpp/newbenchmarks/benchmarks-ios-app/BenchmarksIosApp/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 compose/mpp/newbenchmarks/benchmarks-ios-app/BenchmarksIosApp/Assets.xcassets/Contents.json create mode 100644 compose/mpp/newbenchmarks/benchmarks-ios-app/BenchmarksIosApp/BenchmarksIosApp.swift create mode 100644 compose/mpp/newbenchmarks/benchmarks-ios-app/BenchmarksIosApp/Info.plist create mode 100644 compose/mpp/newbenchmarks/benchmarks-ios-app/BenchmarksIosApp/Preview Content/Preview Assets.xcassets/Contents.json create mode 100644 compose/mpp/newbenchmarks/benchmarks-ios-app/README.md diff --git a/compose/mpp/newbenchmarks/README.md b/compose/mpp/newbenchmarks/README.md new file mode 100644 index 0000000000000..13121058f2a9b --- /dev/null +++ b/compose/mpp/newbenchmarks/README.md @@ -0,0 +1,97 @@ +# Benchmarks + +Run all commands from the repository root. Start the collector in one terminal, then start an app +in another terminal. + +The apps currently display the collector URL and test name only. They do not run benchmarks or send reports yet. + +## Collector + +Pass the listening port and output directory: + +```bash +./gradlew :compose:mpp:newbenchmarks:benchmarks-collector:run \ + --args="8090 $(pwd)/out/benchmark-reports" --console=plain +``` + +The endpoint is `POST http://127.0.0.1:8090/reports`. It accepts a benchmark report as JSON and saves it +as `[type]-[UTC ISO time].json` in the output directory. Keep this process running. + +## Desktop (JVM) + +Pass `bechmarkCollectorUrl` and the test name as two positional arguments: + +```bash +./gradlew :compose:mpp:newbenchmarks:benchmarks-app:run \ + --args="http://127.0.0.1:8090/reports LazyGrid" --console=plain +``` + +## macOS (native ARM64) + +On an Apple Silicon Mac, build the executable: + +```bash +./gradlew :compose:mpp:newbenchmarks:benchmarks-app:linkDebugExecutableMacosArm64 --console=plain +``` + +Then run it with the same two arguments: + +```bash +./out/compose-multiplatform-core/compose/mpp/newbenchmarks/benchmarks-app/build/bin/macosArm64/debugExecutable/benchmarks-app.kexe \ + http://127.0.0.1:8090/reports LazyGrid +``` + +This path assumes the default repository `out` directory. Adjust it if using a custom `OUT_DIR`. + +## Browser (JS or WasmJS) + +Start one development server at a time. + +JS: + +```bash +./gradlew :compose:mpp:newbenchmarks:benchmarks-app:jsBrowserDevelopmentRun --console=plain +``` + +WasmJS: + +```bash +./gradlew :compose:mpp:newbenchmarks:benchmarks-app:wasmJsBrowserDevelopmentRun --console=plain +``` + +Open the address printed by the development server and add the `bechmarkCollectorUrl` and `test` query parameters. +For example, if it uses port 8080: + +```text +http://localhost:8080/?bechmarkCollectorUrl=http%3A%2F%2F127.0.0.1%3A8090%2Freports&test=LazyGrid +``` + +URL-encode both parameter values. If the server opens a page without parameters, add them to the +address and reload the page. + +## iOS (ARM64 simulator or device) + +On macOS with Xcode installed: + +```bash +open compose/mpp/newbenchmarks/benchmarks-ios-app/BenchmarksIosApp.xcodeproj +``` + +1. Select the **BenchmarksIosApp** scheme. +2. Select an ARM64 iOS simulator or a connected iOS device. +3. Open **Product → Scheme → Edit Scheme → Run → Arguments**. +4. Add two enabled launch arguments, in this order: + - `http://127.0.0.1:8090/reports` (`bechmarkCollectorUrl`) + - `LazyGrid` +5. Run the app. Xcode builds the `BenchmarksApp` Kotlin framework automatically. + +The shared scheme already contains these example arguments. For a physical device, select your +team under **Signing & Capabilities**. See [iOS app setup](benchmarks-ios-app/README.md) for more details. + +## Current network limits + +- The collector listens on `127.0.0.1` only. It is accessible from the same Mac and its iOS simulator, + but not from a physical iOS device. Future device reporting will need a reachable host address + and a collector configured to listen on a network interface. +- Browser reporting will need CORS support in the collector. It is not configured yet. +- These limits do not affect the current apps, which only display the input parameters. diff --git a/compose/mpp/newbenchmarks/benchmarks-app/build.gradle.kts b/compose/mpp/newbenchmarks/benchmarks-app/build.gradle.kts new file mode 100644 index 0000000000000..cad613f3fdec1 --- /dev/null +++ b/compose/mpp/newbenchmarks/benchmarks-app/build.gradle.kts @@ -0,0 +1,80 @@ +/* + * Copyright 2026 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:OptIn(ExperimentalWasmDsl::class) + +import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl + +plugins { + id("org.jetbrains.kotlin.multiplatform") + id("org.jetbrains.kotlin.plugin.compose") version libs.versions.kotlin.get() + id("org.jetbrains.compose") version "1.13.0-beta02" +} + +kotlin { + jvm() + + listOf(iosArm64(), iosSimulatorArm64()).forEach { target -> + target.binaries.framework { + baseName = "BenchmarksApp" + isStatic = true + } + } + + macosArm64 { + binaries.executable { + entryPoint = "org.jetbrains.compose.benchmarks.app.main" + } + } + + wasmJs { + browser { + commonWebpackConfig { + outputFileName = "benchmarks-app.js" + } + } + binaries.executable() + } + js { + browser { + commonWebpackConfig { + outputFileName = "benchmarks-app.js" + } + } + binaries.executable() + } + + sourceSets { + commonMain.dependencies { + implementation(project(":compose:runtime:runtime")) + implementation(project(":compose:ui:ui")) + implementation(project(":compose:foundation:foundation")) + } + jvmMain.dependencies { + implementation(libs.skikoAwtRuntime) + implementation(libs.kotlinCoroutinesSwing) + } + webMain.dependencies { + implementation(libs.kotlinXw3c) + } + } +} + +compose.desktop { + application { + mainClass = "org.jetbrains.compose.benchmarks.app.Main_jvmKt" + } +} diff --git a/compose/mpp/newbenchmarks/benchmarks-app/gradle.properties b/compose/mpp/newbenchmarks/benchmarks-app/gradle.properties new file mode 100644 index 0000000000000..d15154e785f1e --- /dev/null +++ b/compose/mpp/newbenchmarks/benchmarks-app/gradle.properties @@ -0,0 +1 @@ +org.jetbrains.compose.experimental.macos.enabled=true diff --git a/compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/App.kt b/compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/App.kt new file mode 100644 index 0000000000000..f1c0887225a7a --- /dev/null +++ b/compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/App.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2026 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jetbrains.compose.benchmarks.app + +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.text.BasicText +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp + +@Composable +internal fun App(bechmarkCollectorUrl: String, testName: String) { + BasicText( + text = "bechmarkCollectorUrl: $bechmarkCollectorUrl\nTest: $testName", + modifier = Modifier.padding(16.dp), + ) +} + +internal fun parseArguments(args: Array): Pair { + require(args.size == 2) { "Usage: benchmarks-app " } + return args[0] to args[1] +} diff --git a/compose/mpp/newbenchmarks/benchmarks-app/src/iosMain/kotlin/org/jetbrains/compose/benchmarks/app/Main.ios.kt b/compose/mpp/newbenchmarks/benchmarks-app/src/iosMain/kotlin/org/jetbrains/compose/benchmarks/app/Main.ios.kt new file mode 100644 index 0000000000000..177f52b49cad2 --- /dev/null +++ b/compose/mpp/newbenchmarks/benchmarks-app/src/iosMain/kotlin/org/jetbrains/compose/benchmarks/app/Main.ios.kt @@ -0,0 +1,24 @@ +/* + * Copyright 2026 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jetbrains.compose.benchmarks.app + +import androidx.compose.ui.window.ComposeUIViewController +import platform.UIKit.UIViewController + +fun MainViewController(bechmarkCollectorUrl: String, testName: String): UIViewController = ComposeUIViewController { + App(bechmarkCollectorUrl, testName) +} diff --git a/compose/mpp/newbenchmarks/benchmarks-app/src/jvmMain/kotlin/org/jetbrains/compose/benchmarks/app/Main.jvm.kt b/compose/mpp/newbenchmarks/benchmarks-app/src/jvmMain/kotlin/org/jetbrains/compose/benchmarks/app/Main.jvm.kt new file mode 100644 index 0000000000000..f0004bc9b8e82 --- /dev/null +++ b/compose/mpp/newbenchmarks/benchmarks-app/src/jvmMain/kotlin/org/jetbrains/compose/benchmarks/app/Main.jvm.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2026 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jetbrains.compose.benchmarks.app + +import androidx.compose.ui.window.Window +import androidx.compose.ui.window.application + +fun main(args: Array) { + val (bechmarkCollectorUrl, testName) = parseArguments(args) + application { + Window(onCloseRequest = ::exitApplication, title = "Benchmarks") { + App(bechmarkCollectorUrl, testName) + } + } +} diff --git a/compose/mpp/newbenchmarks/benchmarks-app/src/macosMain/kotlin/org/jetbrains/compose/benchmarks/app/Main.macos.kt b/compose/mpp/newbenchmarks/benchmarks-app/src/macosMain/kotlin/org/jetbrains/compose/benchmarks/app/Main.macos.kt new file mode 100644 index 0000000000000..c2aafa743aef0 --- /dev/null +++ b/compose/mpp/newbenchmarks/benchmarks-app/src/macosMain/kotlin/org/jetbrains/compose/benchmarks/app/Main.macos.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2026 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jetbrains.compose.benchmarks.app + +import androidx.compose.ui.window.Window +import platform.AppKit.NSApplication +import platform.AppKit.NSApplicationActivationPolicy +import platform.AppKit.NSApplicationDelegateProtocol +import platform.darwin.NSObject + +fun main(args: Array) { + val (bechmarkCollectorUrl, testName) = parseArguments(args) + val application = NSApplication.sharedApplication() + application.setActivationPolicy(NSApplicationActivationPolicy.NSApplicationActivationPolicyRegular) + application.delegate = object : NSObject(), NSApplicationDelegateProtocol { + override fun applicationShouldTerminateAfterLastWindowClosed(sender: NSApplication): Boolean { + return true + } + } + Window("Benchmarks") { + App(bechmarkCollectorUrl, testName) + } + application.run() +} diff --git a/compose/mpp/newbenchmarks/benchmarks-app/src/webMain/kotlin/org/jetbrains/compose/benchmarks/app/Main.web.kt b/compose/mpp/newbenchmarks/benchmarks-app/src/webMain/kotlin/org/jetbrains/compose/benchmarks/app/Main.web.kt new file mode 100644 index 0000000000000..37097b2ac651e --- /dev/null +++ b/compose/mpp/newbenchmarks/benchmarks-app/src/webMain/kotlin/org/jetbrains/compose/benchmarks/app/Main.web.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2026 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jetbrains.compose.benchmarks.app + +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.window.ComposeViewport +import kotlin.js.ExperimentalWasmJsInterop +import kotlin.js.toJsString +import kotlinx.browser.window +import org.w3c.dom.url.URLSearchParams + +@OptIn(ExperimentalComposeUiApi::class, ExperimentalWasmJsInterop::class) +fun main() { + val parameters = URLSearchParams(window.location.search.toJsString()) + val bechmarkCollectorUrl = requireNotNull(parameters.get("bechmarkCollectorUrl")) { "Missing query parameter: bechmarkCollectorUrl" } + val testName = requireNotNull(parameters.get("test")) { "Missing query parameter: test" } + ComposeViewport("root") { + App(bechmarkCollectorUrl, testName) + } +} diff --git a/compose/mpp/newbenchmarks/benchmarks-app/src/webMain/resources/index.html b/compose/mpp/newbenchmarks/benchmarks-app/src/webMain/resources/index.html new file mode 100644 index 0000000000000..4345faa2ab5dd --- /dev/null +++ b/compose/mpp/newbenchmarks/benchmarks-app/src/webMain/resources/index.html @@ -0,0 +1,15 @@ + + + + + + Benchmarks + + + +
+ + + diff --git a/compose/mpp/newbenchmarks/benchmarks-ios-app/BenchmarksIosApp.xcodeproj/project.pbxproj b/compose/mpp/newbenchmarks/benchmarks-ios-app/BenchmarksIosApp.xcodeproj/project.pbxproj new file mode 100644 index 0000000000000..defe74cce0369 --- /dev/null +++ b/compose/mpp/newbenchmarks/benchmarks-ios-app/BenchmarksIosApp.xcodeproj/project.pbxproj @@ -0,0 +1,383 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 56; + objects = { + +/* Begin PBXBuildFile section */ + A93A953B29CC810C00F8E227 /* BenchmarksIosApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = A93A953A29CC810C00F8E227 /* BenchmarksIosApp.swift */; }; + A93A953F29CC810D00F8E227 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A93A953E29CC810D00F8E227 /* Assets.xcassets */; }; + A93A954229CC810D00F8E227 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A93A954129CC810D00F8E227 /* Preview Assets.xcassets */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + A93A953729CC810C00F8E227 /* BenchmarksIosApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "BenchmarksIosApp.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + A93A953A29CC810C00F8E227 /* BenchmarksIosApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BenchmarksIosApp.swift; sourceTree = ""; }; + A93A953E29CC810D00F8E227 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + A93A954129CC810D00F8E227 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + A93A953429CC810C00F8E227 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + A93A952E29CC810C00F8E227 = { + isa = PBXGroup; + children = ( + A93A953929CC810C00F8E227 /* BenchmarksIosApp */, + A93A953829CC810C00F8E227 /* Products */, + C4127409AE3703430489E7BC /* Frameworks */, + ); + sourceTree = ""; + }; + A93A953829CC810C00F8E227 /* Products */ = { + isa = PBXGroup; + children = ( + A93A953729CC810C00F8E227 /* BenchmarksIosApp.app */, + ); + name = Products; + sourceTree = ""; + }; + A93A953929CC810C00F8E227 /* BenchmarksIosApp */ = { + isa = PBXGroup; + children = ( + A93A953A29CC810C00F8E227 /* BenchmarksIosApp.swift */, + A93A953E29CC810D00F8E227 /* Assets.xcassets */, + A93A954029CC810D00F8E227 /* Preview Content */, + ); + path = BenchmarksIosApp; + sourceTree = ""; + }; + A93A954029CC810D00F8E227 /* Preview Content */ = { + isa = PBXGroup; + children = ( + A93A954129CC810D00F8E227 /* Preview Assets.xcassets */, + ); + path = "Preview Content"; + sourceTree = ""; + }; + C4127409AE3703430489E7BC /* Frameworks */ = { + isa = PBXGroup; + children = ( + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + A93A953629CC810C00F8E227 /* BenchmarksIosApp */ = { + isa = PBXNativeTarget; + buildConfigurationList = A93A954529CC810D00F8E227 /* Build configuration list for PBXNativeTarget "BenchmarksIosApp" */; + buildPhases = ( + A9D80A052AAB5CDE006C8738 /* ShellScript */, + A93A953329CC810C00F8E227 /* Sources */, + A93A953429CC810C00F8E227 /* Frameworks */, + A93A953529CC810C00F8E227 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = BenchmarksIosApp; + productName = BenchmarksIosApp; + productReference = A93A953729CC810C00F8E227 /* BenchmarksIosApp.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + A93A952F29CC810C00F8E227 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 1420; + LastUpgradeCheck = 1420; + TargetAttributes = { + A93A953629CC810C00F8E227 = { + CreatedOnToolsVersion = 14.2; + }; + }; + }; + buildConfigurationList = A93A953229CC810C00F8E227 /* Build configuration list for PBXProject "BenchmarksIosApp" */; + compatibilityVersion = "Xcode 14.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = A93A952E29CC810C00F8E227; + productRefGroup = A93A953829CC810C00F8E227 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + A93A953629CC810C00F8E227 /* BenchmarksIosApp */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + A93A953529CC810C00F8E227 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + A93A954229CC810D00F8E227 /* Preview Assets.xcassets in Resources */, + A93A953F29CC810D00F8E227 /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + A9D80A052AAB5CDE006C8738 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "cd \"$SRCROOT/../../../..\"\n./gradlew :compose:mpp:newbenchmarks:benchmarks-app:embedAndSignAppleFrameworkForXcode --console=plain\n"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + A93A953329CC810C00F8E227 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + A93A953B29CC810C00F8E227 /* BenchmarksIosApp.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + A93A954329CC810D00F8E227 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 16.2; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + A93A954429CC810D00F8E227 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 16.2; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + A93A954629CC810D00F8E227 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CMP_OUT_DIR = "$(SRCROOT)/../../../../out"; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(CMP_OUT_DIR)/compose-multiplatform-core/compose/mpp/newbenchmarks/benchmarks-app/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)", + ); + OTHER_LDFLAGS = ( + "$(inherited)", + "-framework", + BenchmarksApp, + ); + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = "\"BenchmarksIosApp/Preview Content\""; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = BenchmarksIosApp/Info.plist; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = org.jetbrains.compose.benchmarks.app.BenchmarksIosApp; + PRODUCT_NAME = "BenchmarksIosApp"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + A93A954729CC810D00F8E227 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CMP_OUT_DIR = "$(SRCROOT)/../../../../out"; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(CMP_OUT_DIR)/compose-multiplatform-core/compose/mpp/newbenchmarks/benchmarks-app/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)", + ); + OTHER_LDFLAGS = ( + "$(inherited)", + "-framework", + BenchmarksApp, + ); + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = "\"BenchmarksIosApp/Preview Content\""; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = BenchmarksIosApp/Info.plist; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = org.jetbrains.compose.benchmarks.app.BenchmarksIosApp; + PRODUCT_NAME = "BenchmarksIosApp"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + A93A953229CC810C00F8E227 /* Build configuration list for PBXProject "BenchmarksIosApp" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + A93A954329CC810D00F8E227 /* Debug */, + A93A954429CC810D00F8E227 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + A93A954529CC810D00F8E227 /* Build configuration list for PBXNativeTarget "BenchmarksIosApp" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + A93A954629CC810D00F8E227 /* Debug */, + A93A954729CC810D00F8E227 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = A93A952F29CC810C00F8E227 /* Project object */; +} diff --git a/compose/mpp/newbenchmarks/benchmarks-ios-app/BenchmarksIosApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/compose/mpp/newbenchmarks/benchmarks-ios-app/BenchmarksIosApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000000..69a6d136e1414 --- /dev/null +++ b/compose/mpp/newbenchmarks/benchmarks-ios-app/BenchmarksIosApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,4 @@ + + + + diff --git a/compose/mpp/newbenchmarks/benchmarks-ios-app/BenchmarksIosApp.xcodeproj/xcshareddata/xcschemes/BenchmarksIosApp.xcscheme b/compose/mpp/newbenchmarks/benchmarks-ios-app/BenchmarksIosApp.xcodeproj/xcshareddata/xcschemes/BenchmarksIosApp.xcscheme new file mode 100644 index 0000000000000..0f2e53beb218a --- /dev/null +++ b/compose/mpp/newbenchmarks/benchmarks-ios-app/BenchmarksIosApp.xcodeproj/xcshareddata/xcschemes/BenchmarksIosApp.xcscheme @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/compose/mpp/newbenchmarks/benchmarks-ios-app/BenchmarksIosApp/Assets.xcassets/AccentColor.colorset/Contents.json b/compose/mpp/newbenchmarks/benchmarks-ios-app/BenchmarksIosApp/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000000000..ee7e3ca03f870 --- /dev/null +++ b/compose/mpp/newbenchmarks/benchmarks-ios-app/BenchmarksIosApp/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} \ No newline at end of file diff --git a/compose/mpp/newbenchmarks/benchmarks-ios-app/BenchmarksIosApp/Assets.xcassets/AppIcon.appiconset/Contents.json b/compose/mpp/newbenchmarks/benchmarks-ios-app/BenchmarksIosApp/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000000000..fb88a396bf389 --- /dev/null +++ b/compose/mpp/newbenchmarks/benchmarks-ios-app/BenchmarksIosApp/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,98 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "60x60" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "60x60" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "20x20" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "29x29" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "40x40" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "40x40" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "76x76" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "76x76" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "83.5x83.5" + }, + { + "idiom" : "ios-marketing", + "scale" : "1x", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} \ No newline at end of file diff --git a/compose/mpp/newbenchmarks/benchmarks-ios-app/BenchmarksIosApp/Assets.xcassets/Contents.json b/compose/mpp/newbenchmarks/benchmarks-ios-app/BenchmarksIosApp/Assets.xcassets/Contents.json new file mode 100644 index 0000000000000..4aa7c5350bfce --- /dev/null +++ b/compose/mpp/newbenchmarks/benchmarks-ios-app/BenchmarksIosApp/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} \ No newline at end of file diff --git a/compose/mpp/newbenchmarks/benchmarks-ios-app/BenchmarksIosApp/BenchmarksIosApp.swift b/compose/mpp/newbenchmarks/benchmarks-ios-app/BenchmarksIosApp/BenchmarksIosApp.swift new file mode 100644 index 0000000000000..72feed73165cf --- /dev/null +++ b/compose/mpp/newbenchmarks/benchmarks-ios-app/BenchmarksIosApp/BenchmarksIosApp.swift @@ -0,0 +1,29 @@ +import SwiftUI +import BenchmarksApp + +@main +struct ComposeApp: App { + private let arguments = Array(ProcessInfo.processInfo.arguments.dropFirst()) + + var body: some Scene { + WindowGroup { + ContentView( + bechmarkCollectorUrl: arguments.first ?? "", + testName: arguments.dropFirst().first ?? "" + ).ignoresSafeArea(.all) + } + } +} + +struct ContentView: UIViewControllerRepresentable { + let bechmarkCollectorUrl: String + let testName: String + + func makeUIViewController(context: Context) -> UIViewController { + Main_iosKt.MainViewController(bechmarkCollectorUrl: bechmarkCollectorUrl, testName: testName) + } + + func updateUIViewController(_ uiViewController: UIViewController, context: Context) { + // Updates will be handled by Compose. + } +} diff --git a/compose/mpp/newbenchmarks/benchmarks-ios-app/BenchmarksIosApp/Info.plist b/compose/mpp/newbenchmarks/benchmarks-ios-app/BenchmarksIosApp/Info.plist new file mode 100644 index 0000000000000..903a507566d94 --- /dev/null +++ b/compose/mpp/newbenchmarks/benchmarks-ios-app/BenchmarksIosApp/Info.plist @@ -0,0 +1,8 @@ + + + + + CADisableMinimumFrameDurationOnPhone + + + diff --git a/compose/mpp/newbenchmarks/benchmarks-ios-app/BenchmarksIosApp/Preview Content/Preview Assets.xcassets/Contents.json b/compose/mpp/newbenchmarks/benchmarks-ios-app/BenchmarksIosApp/Preview Content/Preview Assets.xcassets/Contents.json new file mode 100644 index 0000000000000..4aa7c5350bfce --- /dev/null +++ b/compose/mpp/newbenchmarks/benchmarks-ios-app/BenchmarksIosApp/Preview Content/Preview Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} \ No newline at end of file diff --git a/compose/mpp/newbenchmarks/benchmarks-ios-app/README.md b/compose/mpp/newbenchmarks/benchmarks-ios-app/README.md new file mode 100644 index 0000000000000..ba5c39931da7e --- /dev/null +++ b/compose/mpp/newbenchmarks/benchmarks-ios-app/README.md @@ -0,0 +1,27 @@ +# Benchmarks iOS app + +Open `BenchmarksIosApp.xcodeproj` in Xcode and select the shared `BenchmarksIosApp` scheme. +The project follows the DemoApp setup: SwiftUI hosts the Compose view controller, and a Run Script +builds the static `BenchmarksApp` framework with `embedAndSignAppleFrameworkForXcode` before Swift compilation. + +## Parameters + +In **Product → Scheme → Edit Scheme → Run → Arguments**, set two launch arguments: + +1. `bechmarkCollectorUrl`, for example `http://127.0.0.1:8090/reports`. +2. Test name, for example `LazyGrid`. + +The shared scheme includes these example values. Both are displayed by the Compose application; +no benchmarks or network requests are performed. When launched without arguments, the values are empty. + +## Build settings + +- iOS deployment target: 16.2, matching DemoApp. +- Supported architectures: ARM64 device and ARM64 simulator, matching `benchmarks-app`. +- For a physical device, select your development team under **Signing & Capabilities**. +- User Script Sandboxing is disabled so Gradle can build the framework. +- Framework search paths use the repository's default `out` layout. If using a custom Gradle `OUT_DIR`, + override the Xcode `CMP_OUT_DIR` build setting to the same directory. + +There is no separate Gradle module for this Xcode application; its Kotlin framework is built by +`:compose:mpp:newbenchmarks:benchmarks-app`. diff --git a/settings-fork.gradle b/settings-fork.gradle index f2da152eefa0c..cc34b77bece3b 100644 --- a/settings-fork.gradle +++ b/settings-fork.gradle @@ -342,6 +342,7 @@ includeProject(":compose:mpp") includeProject(":compose:mpp:newbenchmarks:benchmarks-runtime") includeProject(":compose:mpp:newbenchmarks:benchmarks-report") includeProject(":compose:mpp:newbenchmarks:benchmarks-collector") +includeProject(":compose:mpp:newbenchmarks:benchmarks-app") includeProject(":compose:mpp:demo") includeProject(":compose:mpp:demo-swiftui") includeProject(":compose:foundation:foundation") From 68095acf334f6934aadbb3c24255ccd1b94e7e07 Mon Sep 17 00:00:00 2001 From: Konstantin Date: Tue, 10 Nov 2026 12:12:14 +0100 Subject: [PATCH 11/61] Add benchmark cases and resources to the app --- .../benchmarks-app/build.gradle.kts | 16 +- .../drawable/compose-multiplatform.png | Bin 0 -> 34968 bytes .../drawable/example1_cat.jpg | Bin 0 -> 37524 bytes .../drawable/example1_ic_call_answer.xml | 36 + .../drawable/example1_sailing.svg | 1 + .../example1_compose-community-primary.png | Bin 0 -> 17662 bytes .../font/jetbrainsmono_italic.ttf | Bin 0 -> 141572 bytes .../font/jetbrainsmono_regular.ttf | Bin 0 -> 136708 bytes .../animation/AnimatedVisibility.kt | 47 ++ .../benchmarks/canvasdrawing/CanvasDrawing.kt | 159 +++++ .../complexlazylist/components/MainUI.kt | 115 +++ .../components/MultiLevelCell.kt | 371 ++++++++++ .../components/refresh/LoadingIndicator.kt | 159 +++++ .../components/refresh/SwipeProgress.kt | 17 + .../components/refresh/SwipeRefreshLayout.kt | 121 ++++ .../SwipeRefreshNestedScrollConnection.kt | 134 ++++ .../components/refresh/SwipeRefreshState.kt | 79 +++ .../complexlazylist/models/FollowItem.kt | 16 + .../complexlazylist/models/HotItem.kt | 16 + .../complexlazylist/models/IBaseViewModel.kt | 10 + .../models/ICompositionModel.kt | 21 + .../models/ViewModelFactory.kt | 56 ++ .../app/benchmarks/heavyshader/HeavyShader.kt | 135 ++++ .../app/benchmarks/lazygrid/LazyGrid.kt | 97 +++ .../multipleComponents/Clickable.kt | 362 ++++++++++ .../benchmarks/multipleComponents/MenuIcon.kt | 49 ++ .../multipleComponents/MultipleComponents.kt | 662 ++++++++++++++++++ .../app/benchmarks/textlayout/TextLayout.kt | 117 ++++ .../app/benchmarks/visualeffects/HappyNY.kt | 321 +++++++++ settings-fork.gradle | 1 - 30 files changed, 3114 insertions(+), 4 deletions(-) create mode 100644 compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/composeResources/drawable/compose-multiplatform.png create mode 100644 compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/composeResources/drawable/example1_cat.jpg create mode 100644 compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/composeResources/drawable/example1_ic_call_answer.xml create mode 100644 compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/composeResources/drawable/example1_sailing.svg create mode 100644 compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/composeResources/files/example1_compose-community-primary.png create mode 100644 compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/composeResources/font/jetbrainsmono_italic.ttf create mode 100644 compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/composeResources/font/jetbrainsmono_regular.ttf create mode 100644 compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/animation/AnimatedVisibility.kt create mode 100644 compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/canvasdrawing/CanvasDrawing.kt create mode 100644 compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/complexlazylist/components/MainUI.kt create mode 100644 compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/complexlazylist/components/MultiLevelCell.kt create mode 100644 compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/complexlazylist/components/refresh/LoadingIndicator.kt create mode 100644 compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/complexlazylist/components/refresh/SwipeProgress.kt create mode 100644 compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/complexlazylist/components/refresh/SwipeRefreshLayout.kt create mode 100644 compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/complexlazylist/components/refresh/SwipeRefreshNestedScrollConnection.kt create mode 100644 compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/complexlazylist/components/refresh/SwipeRefreshState.kt create mode 100644 compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/complexlazylist/models/FollowItem.kt create mode 100644 compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/complexlazylist/models/HotItem.kt create mode 100644 compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/complexlazylist/models/IBaseViewModel.kt create mode 100644 compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/complexlazylist/models/ICompositionModel.kt create mode 100644 compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/complexlazylist/models/ViewModelFactory.kt create mode 100644 compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/heavyshader/HeavyShader.kt create mode 100644 compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/lazygrid/LazyGrid.kt create mode 100644 compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/multipleComponents/Clickable.kt create mode 100644 compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/multipleComponents/MenuIcon.kt create mode 100644 compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/multipleComponents/MultipleComponents.kt create mode 100644 compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/textlayout/TextLayout.kt create mode 100644 compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/visualeffects/HappyNY.kt diff --git a/compose/mpp/newbenchmarks/benchmarks-app/build.gradle.kts b/compose/mpp/newbenchmarks/benchmarks-app/build.gradle.kts index cad613f3fdec1..1c8451267d10e 100644 --- a/compose/mpp/newbenchmarks/benchmarks-app/build.gradle.kts +++ b/compose/mpp/newbenchmarks/benchmarks-app/build.gradle.kts @@ -62,6 +62,11 @@ kotlin { implementation(project(":compose:runtime:runtime")) implementation(project(":compose:ui:ui")) implementation(project(":compose:foundation:foundation")) + implementation(project(":compose:material:material")) + implementation("org.jetbrains.compose.components:components-resources:1.13.0-beta02") { + exclude(group = "org.jetbrains.compose.runtime") + exclude(group = "org.jetbrains.compose.ui") + } } jvmMain.dependencies { implementation(libs.skikoAwtRuntime) @@ -73,8 +78,13 @@ kotlin { } } -compose.desktop { - application { - mainClass = "org.jetbrains.compose.benchmarks.app.Main_jvmKt" +compose { + resources { + packageOfResClass = "compose_benchmarks.benchmarks.generated.resources" + } + desktop { + application { + mainClass = "org.jetbrains.compose.benchmarks.app.Main_jvmKt" + } } } diff --git a/compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/composeResources/drawable/compose-multiplatform.png b/compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/composeResources/drawable/compose-multiplatform.png new file mode 100644 index 0000000000000000000000000000000000000000..8b74accd0beb7831c819df311cd38e47cb57f601 GIT binary patch literal 34968 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 literal 0 HcmV?d00001 diff --git a/compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/composeResources/drawable/example1_cat.jpg b/compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/composeResources/drawable/example1_cat.jpg new file mode 100644 index 0000000000000000000000000000000000000000..62f987d6dcf1aeff1e227fdcea04c6bed7c26875 GIT binary patch literal 37524 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 Hf4=?~qAD`< literal 0 HcmV?d00001 diff --git a/compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/composeResources/drawable/example1_ic_call_answer.xml b/compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/composeResources/drawable/example1_ic_call_answer.xml new file mode 100644 index 0000000000000..da8a42f54a3ec --- /dev/null +++ b/compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/composeResources/drawable/example1_ic_call_answer.xml @@ -0,0 +1,36 @@ + + + + diff --git a/compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/composeResources/drawable/example1_sailing.svg b/compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/composeResources/drawable/example1_sailing.svg new file mode 100644 index 0000000000000..ba23d0fe090c4 --- /dev/null +++ b/compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/composeResources/drawable/example1_sailing.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/composeResources/files/example1_compose-community-primary.png b/compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/composeResources/files/example1_compose-community-primary.png new file mode 100644 index 0000000000000000000000000000000000000000..a73ddd79899482e2f1252de2a3e9d4b8225f140b GIT binary patch literal 17662 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 literal 0 HcmV?d00001 diff --git a/compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/composeResources/font/jetbrainsmono_italic.ttf b/compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/composeResources/font/jetbrainsmono_italic.ttf new file mode 100644 index 0000000000000000000000000000000000000000..44e1f4a7f3acd89e15ab5050607ee7bb497f5d1a GIT binary patch literal 141572 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

-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 literal 0 HcmV?d00001 diff --git a/compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/animation/AnimatedVisibility.kt b/compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/animation/AnimatedVisibility.kt new file mode 100644 index 0000000000000..51d86109cbaac --- /dev/null +++ b/compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/animation/AnimatedVisibility.kt @@ -0,0 +1,47 @@ +package org.jetbrains.compose.benchmarks.app.benchmarks.animation + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.EnterExitState +import androidx.compose.animation.core.Transition +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.runtime.withFrameNanos +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import compose_benchmarks.benchmarks.generated.resources.Res +import compose_benchmarks.benchmarks.generated.resources.compose_multiplatform +import org.jetbrains.compose.resources.ExperimentalResourceApi +import org.jetbrains.compose.resources.painterResource + +@OptIn(ExperimentalResourceApi::class) +@Composable +fun AnimatedVisibility() { + MaterialTheme { + var showImage by remember { mutableStateOf(false) } + var transition: Transition? = null + LaunchedEffect(showImage) { + do { + withFrameNanos {} + } while (transition?.isRunning == true) + + showImage = !showImage + } + Column(Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) { + AnimatedVisibility(showImage) { + transition = this.transition + Image( + painterResource(Res.drawable.compose_multiplatform), + null + ) + } + } + } +} \ No newline at end of file diff --git a/compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/canvasdrawing/CanvasDrawing.kt b/compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/canvasdrawing/CanvasDrawing.kt new file mode 100644 index 0000000000000..be0427649f9df --- /dev/null +++ b/compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/canvasdrawing/CanvasDrawing.kt @@ -0,0 +1,159 @@ +/* + * Copyright 2020-2026 JetBrains s.r.o. and respective authors and developers. + * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. + */ + +package org.jetbrains.compose.benchmarks.app.benchmarks.canvasdrawing + +import androidx.compose.animation.core.LinearEasing +import androidx.compose.animation.core.RepeatMode +import androidx.compose.animation.core.animateFloat +import androidx.compose.animation.core.infiniteRepeatable +import androidx.compose.animation.core.rememberInfiniteTransition +import androidx.compose.animation.core.tween +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.background +import androidx.compose.foundation.gestures.scrollBy +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.runtime.withFrameMillis +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Path +import androidx.compose.ui.unit.dp +import kotlin.math.PI +import kotlin.math.cos +import kotlin.math.sin +import kotlin.random.Random +import kotlinx.coroutines.isActive + +private const val ITEM_COUNT = 1200 + +@Composable +fun CanvasDrawing() { + val listState = rememberLazyListState() + var scrollForward by remember { mutableStateOf(true) } + + LaunchedEffect(Unit) { + while (isActive) { + withFrameMillis { } + val currentItem = listState.firstVisibleItemIndex + if (currentItem == 0) scrollForward = true + if (currentItem > ITEM_COUNT - 100) scrollForward = false + listState.scrollBy(if (scrollForward) 33f else -33f) + } + } + + LazyColumn( + state = listState, + modifier = Modifier.fillMaxSize().background(Color.Black) + ) { + items(ITEM_COUNT) { index -> + CanvasDrawingItem(index) + } + } +} + +@Composable +private fun CanvasDrawingItem(index: Int) { + val animatedValue by rememberInfiniteTransition().animateFloat( + initialValue = 0f, + targetValue = 360f, + animationSpec = infiniteRepeatable( + animation = tween(3000, easing = LinearEasing), + repeatMode = RepeatMode.Restart + ) + ) + + Canvas( + modifier = Modifier + .fillMaxWidth() + .height(300.dp) + .padding(8.dp) + ) { + val seed = index * 12345L + val random = Random(seed) + + drawRect( + brush = Brush.verticalGradient( + colors = listOf( + Color(random.nextInt(0xFFFFFF) or 0xFF000000.toInt()), + Color(random.nextInt(0xFFFFFF) or 0xFF000000.toInt()) + ) + ) + ) + + repeat(200) { i -> + val angle = animatedValue + i * 7.2f + val radius = size.minDimension / 4f + val x = size.width / 2 + cos(angle * PI / 180.0).toFloat() * radius * random.nextFloat() + val y = size.height / 2 + sin(angle * PI / 180.0).toFloat() * radius * random.nextFloat() + + val path = Path().apply { + moveTo(x, y) + repeat(40) { j -> + val pointAngle = angle + j * 45f + animatedValue + val pointRadius = 20f + random.nextFloat() * 30f + val px = x + cos(pointAngle * PI / 180.0).toFloat() * pointRadius + val py = y + sin(pointAngle * PI / 180.0).toFloat() * pointRadius + if (j % 2 == 0) { + lineTo(px, py) + } else { + quadraticBezierTo( + x + random.nextFloat() * 50f - 25f, + y + random.nextFloat() * 50f - 25f, + px, py + ) + } + } + close() + } + + drawPath( + path = path, + brush = Brush.radialGradient( + colors = listOf( + Color(random.nextInt(0xFFFFFF) or 0x80000000.toInt()), + Color(random.nextInt(0xFFFFFF) or 0x40000000.toInt()) + ), + center = Offset(x, y) + ) + ) + } + + repeat(70) { i -> + val lineY = i * (size.height / 30) + drawLine( + color = Color(random.nextInt(0xFFFFFF) or 0xFF000000.toInt()), + start = Offset(0f, lineY), + end = Offset(size.width, lineY + sin(animatedValue * i).toFloat() * 20f), + strokeWidth = 2f + ) + } + + repeat(70) { i -> + val circleX = (i % 8) * (size.width / 8) + (size.width / 16) + val circleY = (i / 8) * (size.height / 5) + (size.height / 10) + val circleRadius = 15f + sin(animatedValue + i).toFloat() * 10f + + drawCircle( + color = Color(random.nextInt(0xFFFFFF) or 0xFF000000.toInt()), + radius = circleRadius, + center = Offset(circleX, circleY), + alpha = 0.7f + ) + } + } +} diff --git a/compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/complexlazylist/components/MainUI.kt b/compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/complexlazylist/components/MainUI.kt new file mode 100644 index 0000000000000..b2eb0422cb1c2 --- /dev/null +++ b/compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/complexlazylist/components/MainUI.kt @@ -0,0 +1,115 @@ +package org.jetbrains.compose.benchmarks.app.benchmarks.complexlazylist.components + +import androidx.compose.foundation.gestures.scrollBy +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.withFrameMillis +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import org.jetbrains.compose.benchmarks.app.benchmarks.complexlazylist.components.refresh.LOADING_MORE +import org.jetbrains.compose.benchmarks.app.benchmarks.complexlazylist.components.refresh.LoadingIndicatorDefault +import org.jetbrains.compose.benchmarks.app.benchmarks.complexlazylist.components.refresh.NORMAL +import org.jetbrains.compose.benchmarks.app.benchmarks.complexlazylist.components.refresh.REFRESHING +import org.jetbrains.compose.benchmarks.app.benchmarks.complexlazylist.components.refresh.SwipeRefreshLayout +import org.jetbrains.compose.benchmarks.app.benchmarks.complexlazylist.components.refresh.rememberSwipeRefreshState +import org.jetbrains.compose.benchmarks.app.benchmarks.complexlazylist.models.IBaseViewModel +import org.jetbrains.compose.benchmarks.app.benchmarks.complexlazylist.models.ICompositionModel +import org.jetbrains.compose.benchmarks.app.benchmarks.complexlazylist.models.fetchCompositionModels + +// just for demo +internal var models: MutableList = mutableStateListOf() + +@Composable +fun MainUiNoImageUseModel() { + MaterialTheme { + MainLazyColumnItemsList(noImage = true, useJson = false) + DisposableEffect(Unit) { + onDispose { + models.clear() + } + } + } +} + +@Composable +fun MainLazyColumnItemsList(noImage: Boolean, useJson: Boolean) { + val scope = rememberCoroutineScope() + val state = rememberSwipeRefreshState(NORMAL) + + LaunchedEffect(scope) { + scope.launch(Dispatchers.Default) { + fetchCompositionModels(false) { list -> + for (item in list) + models.add(item) + } + } + } + + SwipeRefreshLayout( + state = state, + indicator = { modifier, s, indicatorHeight -> + LoadingIndicatorDefault(modifier, s, indicatorHeight) + }, + onRefresh = { + scope.launch { + state.loadState = REFRESHING + delay(2000) + fetchCompositionModels(useJson) { + models.clear() + for (item in it) + models.add(item) + state.loadState = NORMAL + } + } + + }, + onLoadMore = { + scope.launch { + state.loadState = LOADING_MORE + delay(2000L) + fetchCompositionModels(useJson) { + for (item in it) + models.add(item) + state.loadState = NORMAL + } + } + } + ) { modifier -> + run { + val state = rememberLazyListState() + LaunchedEffect(Unit) { + while (true) { + withFrameMillis { } + state.scrollBy(20f) + } + } + LazyColumn(modifier, state = state) { + itemsIndexed( + items = models, + key = { index, _ -> + index + } + ) { _, item -> + when (item) { + is ICompositionModel -> MultiCellUI(item) + // .. todo need more types + else -> throw RuntimeException("Unexpected") + } + } + } + } + } +} + +@Composable +fun MultiCellUI(item: IBaseViewModel) { + DecoratedCell(item) +} \ No newline at end of file diff --git a/compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/complexlazylist/components/MultiLevelCell.kt b/compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/complexlazylist/components/MultiLevelCell.kt new file mode 100644 index 0000000000000..beb3940c0d6f8 --- /dev/null +++ b/compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/complexlazylist/components/MultiLevelCell.kt @@ -0,0 +1,371 @@ +package org.jetbrains.compose.benchmarks.app.benchmarks.complexlazylist.components + +import androidx.compose.animation.core.animate +import androidx.compose.animation.core.animateDpAsState +import androidx.compose.animation.core.tween +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.MaterialTheme.typography +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.scale +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.graphics.painter.ColorPainter +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.launch +import org.jetbrains.compose.benchmarks.app.benchmarks.complexlazylist.models.IBaseViewModel +import org.jetbrains.compose.benchmarks.app.benchmarks.complexlazylist.models.ICompositionItem +import org.jetbrains.compose.benchmarks.app.benchmarks.complexlazylist.models.ICompositionModel + +@Composable +internal fun DecoratedCell(model: IBaseViewModel) { + Column( + modifier = Modifier + .fillMaxWidth() + .border(1.dp, Color.LightGray, RoundedCornerShape(4.dp)) + .background( + brush = Brush.linearGradient( + colors = listOf( + Color.LightGray.copy(1f), + Color.LightGray.copy(0.5f), + Color.LightGray.copy(0f) + ), + start = Offset(320f, 0f), + end = Offset(320f, 164f) + ) + ), + verticalArrangement = Arrangement.spacedBy((-60).dp) + ) { + CompositionView(model) + Box( + modifier = Modifier.fillMaxWidth() + .heightIn(164.dp), + contentAlignment = Alignment.BottomCenter, + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .heightIn(64.dp) + .padding(8.dp) + .border(1.dp, Color.Cyan.copy(0.3f), RoundedCornerShape(4.dp)) + .padding(8.dp) + .background(color = Color.Cyan.copy(alpha = 0.1f)) + ) { + + Avatar( + modifier = Modifier + .padding(4.dp) + .align(Alignment.CenterVertically) + ) + + TextInfo( + Modifier + .weight(1f) + .align(Alignment.CenterVertically) + ) + + FollowBtn( + Modifier.align(Alignment.CenterVertically) + ) + } + } + } +} + +@Composable +internal fun CompositionView(model: IBaseViewModel) { + if (model !is ICompositionModel) + return + Box( + modifier = Modifier.clip(RoundedCornerShape(10.dp)).background(Color.White).padding(10.dp) + ) { + model.overlyTopLeft?.let { overlyTopLeft(it) } + model.overlyView1?.let { overlyView1(it) } + model.overlyView2?.let { overlyView2(it) } + model.overlyView3?.let { overlyView3(it) } + model.overlyTopRight?.let { overlyTopRight(it) } + model.label?.let { overlyLabel(it) } + } +} + +@Composable +internal fun overlyTopLeft(compositionItem: ICompositionItem) { + compositionItem.bgColor?.let { Color.DarkGray }?.let { ColorPainter(it) }?.let { + compositionItem.alpha?.toFloat()?.let { it1 -> + compositionItem.radius?.toFloat()?.let { RoundedCornerShape(it.dp) }?.let { it2 -> + Modifier + .padding(start = 0.dp, top = 0.dp) + .width(50.dp) + .height(50.dp) + .alpha(it1) + .clip(shape = it2) + .shadow(3.dp) + } + }?.let { it2 -> + Image( + painter = it, + modifier = it2, + contentDescription = null + ) + } + } +} + +@Composable +internal fun overlyView1(compositionItem: ICompositionItem) { + compositionItem.bgColor?.let { Color.Red }?.let { ColorPainter(it) }?.let { + compositionItem.alpha?.toFloat()?.let { it1 -> + compositionItem.radius?.toFloat()?.let { RoundedCornerShape(it.dp) }?.let { it2 -> + Modifier + .padding(start = 20.dp, top = 20.dp) + .width(150.dp) + .height(100.dp) + .alpha(it1) + .clip(shape = it2) + .shadow(3.dp) + } + }?.let { it2 -> + Image( + painter = it, + modifier = it2, + contentDescription = null + ) + } + } +} + +@Composable +internal fun overlyView2(compositionItem: ICompositionItem) { + compositionItem.bgColor?.let { Color.Yellow }?.let { ColorPainter(it) }?.let { + compositionItem.alpha?.toFloat()?.let { it1 -> + compositionItem.radius?.toFloat()?.let { RoundedCornerShape(it.dp) }?.let { it2 -> + Modifier + .padding(start = 100.dp, top = 50.dp) + .width(150.dp) + .height(100.dp) + .alpha(it1) + .clip(shape = it2) + .shadow(3.dp) + } + }?.let { it2 -> + Image( + painter = it, + modifier = it2, + contentDescription = null + ) + } + } +} + +@Composable +internal fun overlyView3(compositionItem: ICompositionItem) { + compositionItem.bgColor?.let { Color.Green }?.let { ColorPainter(it) }?.let { + compositionItem.alpha?.toFloat()?.let { it1 -> + compositionItem.radius?.toFloat()?.let { RoundedCornerShape(it.dp) }?.let { it2 -> + Modifier + .padding(start = 110.dp, top = 60.dp) + .width(130.dp) + .height(80.dp) + .alpha(it1) + .clip(shape = it2) + .shadow(3.dp) + } + }?.let { it2 -> + Image( + painter = it, + modifier = it2, + contentDescription = null + ) + } + } +} + +@Composable +internal fun overlyTopRight(compositionItem: ICompositionItem) { + compositionItem.bgColor?.let { Color.LightGray }?.let { ColorPainter(it) }?.let { + compositionItem.alpha?.toFloat()?.let { it1 -> + compositionItem.radius?.toFloat()?.let { RoundedCornerShape(it.dp) }?.let { it2 -> + Modifier + .padding(start = 160.dp, top = 15.dp) + .width(90.dp) + .height(60.dp) + .alpha(it1) + .clip(shape = it2) + .shadow(3.dp) + } + }?.let { it2 -> + Image( + painter = it, + modifier = it2, + contentDescription = null + ) + } + } +} + +@Composable +internal fun overlyLabel(compositionItem: ICompositionItem) { + Box(modifier = Modifier.fillMaxSize().padding(), contentAlignment = Alignment.TopCenter) { + compositionItem.text?.let { + Text( + text = it, + modifier = Modifier.background(Color.Black.copy(0.5f)).alpha(0.5f) + .padding(start = 10.dp, end = 10.dp).width(200.dp).height(100.dp), + color = Color.Cyan + ) + } + } +} + +@Composable +internal fun Avatar(modifier: Modifier) { + Image( + modifier = modifier + .size(50.dp) + .clip(CircleShape) + .border( + shape = CircleShape, + border = BorderStroke( + width = 2.dp, + brush = Brush.linearGradient( + colors = listOf(Color.Blue, Color.Yellow, Color.Green, Color.Red), + start = Offset(0f, 0f), + end = Offset(100f, 100f) + ) + ) + ) + .border( + shape = CircleShape, + border = BorderStroke(4.dp, SolidColor(Color.White)) + ), + painter = ColorPainter(color = Color.Gray), + contentDescription = null, // decorative + ) +} + +@Composable +internal fun TextInfo(modifier: Modifier) { + Column(modifier = modifier) { + Text( + text = "name", + color = Color.Black, + maxLines = 1, + style = TextStyle( + fontWeight = FontWeight.Bold, + fontSize = 16.sp, + letterSpacing = 0.15.sp + ) + ) + Text( + text = "desc", + color = Color.Black.copy(alpha = 0.75f), + maxLines = 1, + style = TextStyle( // here + fontWeight = FontWeight.Normal, + fontSize = 14.sp, + letterSpacing = 0.25.sp + ) + ) + } +} + +@Composable +internal fun FollowBtn(modifier: Modifier) { + var checked by remember { mutableStateOf(false) } + val backgroundShape: Shape = remember { RoundedCornerShape(4.dp) } + var clickCount by remember { mutableStateOf(1) } + val offsetX by animateDpAsState( + if (checked) (-80).dp else (-10).dp, + animationSpec = tween(durationMillis = 3000) + ) + + var alpha by remember { mutableStateOf(1f) } + var scale by remember { mutableStateOf(1f) } + val scope = rememberCoroutineScope() + Row( + horizontalArrangement = Arrangement.spacedBy(10.dp), + verticalAlignment = Alignment.CenterVertically, + modifier = modifier.offset(x = offsetX), + ) { + Text( + text = if (checked) "unfollow" else "follow", + style = typography.body1.copy(color = Color.White), + textAlign = TextAlign.Center, + modifier = modifier + .width(100.dp) + .clickable(onClick = { + clickCount++ + checked = !checked + scope.launch { + coroutineScope { + launch { + animate(1f, 0.5f) { value, _ -> alpha = value } + } + launch { + animate(1f, 0.5f) { value, _ -> scale = value } + } + } + coroutineScope { + launch { + animate(0.5f, 1f) { value, _ -> alpha = value } + } + launch { + animate(0.5f, 1f) { value, _ -> scale = value } + } + } + } + }) + .shadow(5.dp, shape = backgroundShape) + .clip(backgroundShape) + .background( + brush = Brush.verticalGradient( + colors = listOf( + Color.Red, + Color.Yellow, + ), + startY = 0f, + endY = 80f + ) + ) + .padding(6.dp) + .scale(scale) + .alpha(alpha) + ) + } +} diff --git a/compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/complexlazylist/components/refresh/LoadingIndicator.kt b/compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/complexlazylist/components/refresh/LoadingIndicator.kt new file mode 100644 index 0000000000000..58064e0dfc732 --- /dev/null +++ b/compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/complexlazylist/components/refresh/LoadingIndicator.kt @@ -0,0 +1,159 @@ +package org.jetbrains.compose.benchmarks.app.benchmarks.complexlazylist.components.refresh + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.LinearEasing +import androidx.compose.animation.core.RepeatMode +import androidx.compose.animation.core.animate +import androidx.compose.animation.core.infiniteRepeatable +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.rotate +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.StrokeCap +import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.max + +@Composable +internal fun BoxScope.LoadingIndicatorDefault( + modifier: Modifier, + state: SwipeRefreshState, + indicatorHeight: Dp +) { + val height = max(30.dp, with(LocalDensity.current) { + state.progress.offset.toDp() + }) + var prev by remember { mutableStateOf(Offset(0f, 0f)) } + var prevOffsetAngle by remember { mutableStateOf(0f) } + var offsetAngle by remember { mutableStateOf(0f) } + var startAngle by remember { mutableStateOf(prev.x) } + var endAngle by remember { mutableStateOf(prev.y) } + + LaunchedEffect(state.loadState == REFRESHING || state.loadState == LOADING_MORE) { + if (!(state.loadState == REFRESHING || state.loadState == LOADING_MORE)) { + prevOffsetAngle = offsetAngle + return@LaunchedEffect + } + animate( + initialValue = prevOffsetAngle, + targetValue = prevOffsetAngle + 360f, + animationSpec = infiniteRepeatable( + animation = tween(5000, easing = LinearEasing), + repeatMode = RepeatMode.Restart + ) + ) { value, _ -> + offsetAngle = value + } + } + + LaunchedEffect(state.loadState == REFRESHING || state.loadState == LOADING_MORE, endAngle) { + if (!(state.loadState == REFRESHING || state.loadState == LOADING_MORE)) return@LaunchedEffect + if ((endAngle - startAngle).toInt() == 270) { + animate( + initialValue = startAngle, + targetValue = endAngle - 10f, + animationSpec = tween(durationMillis = 700) + ) { value, _ -> + startAngle = value + } + } + } + + LaunchedEffect(state.loadState == REFRESHING || state.loadState == LOADING_MORE, startAngle) { + if (!(state.loadState == REFRESHING || state.loadState == LOADING_MORE)) if (!(state.loadState == REFRESHING || state.loadState == LOADING_MORE)) { + prev = Offset(startAngle, endAngle) + return@LaunchedEffect + } + if ((endAngle - startAngle).toInt() == 10 + || endAngle.toInt() == prev.y.toInt() + ) { + animate( + initialValue = endAngle, + targetValue = startAngle + 270f, + animationSpec = tween(durationMillis = 700), + ) { value, _ -> + endAngle = value + } + } + } + + Box( + modifier + .fillMaxWidth() + .height(height), contentAlignment = Alignment.Center + ) { + if (state.isSwipeInProgress) { + if (state.progress.offset <= with(LocalDensity.current) { indicatorHeight.toPx() }) { + Text(text = if (state.progress.location == TOP) "下拉刷新" else "上拉加载更多") + } else { + Text(text = if (state.progress.location == TOP) "松开刷新" else "松开加载更多") + } + } else { + AnimatedVisibility( + visible = state.loadState == REFRESHING || state.loadState == LOADING_MORE, + modifier = Modifier.align(Alignment.Center), + enter = fadeIn(), + exit = fadeOut() + ) { + Row( + modifier = Modifier + .align(alignment = Alignment.Center), + horizontalArrangement = Arrangement.SpaceEvenly + ) { + Box( + modifier = Modifier + .padding(10.dp) + .height(20.dp).width(20.dp) + .aspectRatio(1f) + .rotate(offsetAngle) + ) { + Canvas( + modifier = Modifier.height(20.dp).width(20.dp) + ) { + drawArc( + color = Color.Gray, + startAngle = startAngle, + sweepAngle = endAngle - startAngle, + useCenter = false, + style = Stroke(width = 5f, cap = StrokeCap.Round) + ) + } + } + + Text( + modifier = Modifier.padding(10.dp), + text = "Loading...", + color = Color.Gray, + fontWeight = FontWeight.W500, + textAlign = TextAlign.Center + ) + } + } + } + } +} \ No newline at end of file diff --git a/compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/complexlazylist/components/refresh/SwipeProgress.kt b/compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/complexlazylist/components/refresh/SwipeProgress.kt new file mode 100644 index 0000000000000..97aadb41cd71e --- /dev/null +++ b/compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/complexlazylist/components/refresh/SwipeProgress.kt @@ -0,0 +1,17 @@ +package org.jetbrains.compose.benchmarks.app.benchmarks.complexlazylist.components.refresh + +import androidx.compose.runtime.Immutable + +internal const val NONE = 0 + +internal const val TOP = 1 + +internal const val BOTTOM = 2 + +@Immutable +internal data class SwipeProgress( + val location: Int = NONE, + val offset: Float = 0f, + /*@FloatRange(from = 0.0, to = 1.0)*/ + val fraction: Float = 0f +) diff --git a/compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/complexlazylist/components/refresh/SwipeRefreshLayout.kt b/compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/complexlazylist/components/refresh/SwipeRefreshLayout.kt new file mode 100644 index 0000000000000..8150e9310d498 --- /dev/null +++ b/compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/complexlazylist/components/refresh/SwipeRefreshLayout.kt @@ -0,0 +1,121 @@ +package org.jetbrains.compose.benchmarks.app.benchmarks.complexlazylist.components.refresh + +import androidx.compose.animation.core.animate +import androidx.compose.animation.core.tween +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import kotlin.math.min + +@Composable +internal fun SwipeRefreshLayout( + state: SwipeRefreshState, + onRefresh: () -> Unit, + onLoadMore: () -> Unit, + modifier: Modifier = Modifier, + refreshTriggerDistance: Dp = 120.dp, + indicationHeight: Dp = 60.dp, + refreshEnabled: Boolean = true, + loadMoreEnabled: Boolean = true, + indicator: @Composable BoxScope.(modifier: Modifier, state: SwipeRefreshState, indicatorHeight: Dp) -> Unit = { m, s, height -> + LoadingIndicatorDefault(m, s, height) + }, + content: @Composable (modifier: Modifier) -> Unit, +) { + val refreshTriggerPx = with(LocalDensity.current) { refreshTriggerDistance.toPx() } + val indicationHeightPx = with(LocalDensity.current) { indicationHeight.toPx() } + + // Our LaunchedEffect, which animates the indicator to its resting position + LaunchedEffect(state.isSwipeInProgress) { + if (!state.isSwipeInProgress) { + // If there's not a swipe in progress, rest the indicator at 0f + state.animateOffsetTo(0f) + } + } + + val coroutineScope = rememberCoroutineScope() + val updatedOnRefresh = rememberUpdatedState(onRefresh) + val updatedOnLoadMore = rememberUpdatedState(onLoadMore) + + val nestedScrollConnection = remember(state, coroutineScope) { + SwipeRefreshNestedScrollConnection( + state, + coroutineScope, + onRefresh = { updatedOnRefresh.value.invoke() }, + onLoadMore = { updatedOnLoadMore.value.invoke() } + ) + }.apply { + this.refreshEnabled = refreshEnabled + this.loadMoreEnabled = loadMoreEnabled + this.refreshTrigger = refreshTriggerPx + this.indicatorHeight = indicationHeightPx + } + + BoxWithConstraints(modifier.nestedScroll(connection = nestedScrollConnection)) { + if (!state.isSwipeInProgress) + LaunchedEffect((state.loadState == REFRESHING || state.loadState == LOADING_MORE)) { + animate( + animationSpec = tween(durationMillis = 300), + initialValue = state.progress.offset, + targetValue = when (state.loadState) { + LOADING_MORE -> indicationHeightPx + REFRESHING -> indicationHeightPx + else -> 0f + } + ) { value, _ -> + if (!state.isSwipeInProgress) { + state.progress = state.progress.copy( + offset = value, + fraction = min(1f, value / refreshTriggerPx) + ) + } + } + } + + val offsetDp = with(LocalDensity.current) { + state.progress.offset.toDp() + } + content( + when (state.progress.location) { + TOP -> Modifier.padding(top = offsetDp) + BOTTOM -> Modifier.padding(bottom = offsetDp) + else -> Modifier + } + ) + if (state.progress.location != NONE) { + Box(modifier = Modifier + .fillMaxWidth() + .height(refreshTriggerDistance) + .graphicsLayer { + translationY = + if (state.progress.location == LOADING_MORE) constraints.maxHeight - state.progress.offset + else state.progress.offset - refreshTriggerPx + } + ) { + indicator( + Modifier.align(if (state.progress.location == TOP) Alignment.BottomStart else Alignment.TopStart), + state, + indicationHeight + ) + } + } + } +} + +@Composable +internal fun rememberSwipeRefreshState(state: Int) = remember { SwipeRefreshState(state) } \ No newline at end of file diff --git a/compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/complexlazylist/components/refresh/SwipeRefreshNestedScrollConnection.kt b/compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/complexlazylist/components/refresh/SwipeRefreshNestedScrollConnection.kt new file mode 100644 index 0000000000000..ac0f2eae4b1c5 --- /dev/null +++ b/compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/complexlazylist/components/refresh/SwipeRefreshNestedScrollConnection.kt @@ -0,0 +1,134 @@ +package org.jetbrains.compose.benchmarks.app.benchmarks.complexlazylist.components.refresh + +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.input.nestedscroll.NestedScrollConnection +import androidx.compose.ui.input.nestedscroll.NestedScrollSource +import androidx.compose.ui.unit.Velocity +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import kotlin.math.abs +import kotlin.math.absoluteValue +import kotlin.math.roundToInt + +internal class SwipeRefreshNestedScrollConnection( + private val state: SwipeRefreshState, + private val coroutineScope: CoroutineScope, + private val onRefresh: () -> Unit, + private val onLoadMore: () -> Unit +) : NestedScrollConnection { + var refreshEnabled: Boolean = false + var loadMoreEnabled: Boolean = false + var refreshTrigger: Float = 100f + var indicatorHeight: Float = 50f + + private var isTop = false + private var isBottom = false + + override fun onPreScroll( + available: Offset, + source: NestedScrollSource + ): Offset = when { + !refreshEnabled && !loadMoreEnabled -> Offset.Zero + state.loadState != NORMAL -> Offset.Zero + source == NestedScrollSource.Drag -> { + if (available.y > 0 && isBottom) { + onScroll(available) + } else if (available.y < 0 && isTop) { + onScroll(available) + } else { + Offset.Zero + } + } + else -> Offset.Zero + } + + override fun onPostScroll( + consumed: Offset, + available: Offset, + source: NestedScrollSource + ): Offset { + + if (!refreshEnabled && !loadMoreEnabled) { + return Offset.Zero + } + + else if (state.loadState != NORMAL) { + return Offset.Zero + } else if (source == NestedScrollSource.Drag) { + if (available.y < 0) { + if (!isBottom) { + isBottom = true + } + if (isBottom) { + return onScroll(available) + } + + } else if (available.y > 0) { + if (!isTop) { + isTop = true + } + if (isTop) { + return onScroll(available) + } + } + } + return Offset.Zero + } + + private fun onScroll(available: Offset): Offset { + if (!isBottom && !isTop) { + return Offset.Zero + } + if (available.y > 0 && isTop) { + state.isSwipeInProgress = true + } else if (available.y < 0 && isBottom) { + state.isSwipeInProgress = true + } else if (state.indicatorOffset.roundToInt() == 0) { + state.isSwipeInProgress = false + } + + val newOffset = (available.y + state.indicatorOffset).let { + if (isTop) it.coerceAtLeast(0.0F) else it.coerceAtMost(0.0F) + } + val dragConsumed = newOffset - state.indicatorOffset + + return if (dragConsumed.absoluteValue >= 0.5f) { + coroutineScope.launch { + state.dispatchScrollDelta( + dragConsumed, + if (isTop) TOP else BOTTOM, + refreshTrigger, + ) + } + // Return the consumed Y + Offset(x = 0f, y = dragConsumed) + } else { + Offset.Zero + } + } + + override suspend fun onPreFling(available: Velocity): Velocity { + // If we're dragging, not currently refreshing and scrolled + // past the trigger point, refresh! + if (state.loadState == NORMAL && abs(state.indicatorOffset) >= indicatorHeight) { + if (isTop) { + onRefresh() + } else if (isBottom) { + onLoadMore() + } + } + + // Reset the drag in progress state + state.isSwipeInProgress = false + + // Don't consume any velocity, to allow the scrolling layout to fling + return Velocity.Zero + } + + override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity { + return Velocity.Zero.also { + isTop = false + isBottom = false + } + } +} \ No newline at end of file diff --git a/compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/complexlazylist/components/refresh/SwipeRefreshState.kt b/compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/complexlazylist/components/refresh/SwipeRefreshState.kt new file mode 100644 index 0000000000000..dc4d3333c26a0 --- /dev/null +++ b/compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/complexlazylist/components/refresh/SwipeRefreshState.kt @@ -0,0 +1,79 @@ +package org.jetbrains.compose.benchmarks.app.benchmarks.complexlazylist.components.refresh + +import androidx.compose.animation.core.Animatable +import androidx.compose.foundation.MutatePriority +import androidx.compose.foundation.MutatorMutex +import androidx.compose.runtime.Stable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import kotlin.math.abs +import kotlin.math.min + +internal const val NORMAL = 0 + + +internal const val REFRESHING = 1 + + +internal const val LOADING_MORE = 2 + +@Stable +internal class SwipeRefreshState( + loadState: Int, +) { + private val _indicatorOffset = Animatable(0f) + private val mutatorMutex = MutatorMutex() + + var loadState: Int by mutableStateOf(loadState) + + /** + * Whether a swipe/drag is currently in progress. + */ + var isSwipeInProgress: Boolean by mutableStateOf(false) + internal set + + var progress: SwipeProgress by mutableStateOf(SwipeProgress()) + internal set + + /** + * The current offset for the indicator, in pixels. + */ + internal val indicatorOffset: Float get() = _indicatorOffset.value + + internal suspend fun animateOffsetTo( + offset: Float, + ) { + mutatorMutex.mutate { + _indicatorOffset.animateTo(offset) + } + } + + /** + * Dispatch scroll delta in pixels from touch events. + */ + internal suspend fun dispatchScrollDelta( + delta: Float, + location: Int, + maxOffsetY: Float, + ) { + mutatorMutex.mutate(MutatePriority.UserInput) { + _indicatorOffset.snapTo((_indicatorOffset.value + delta).toFloat()) + updateProgress( + location = location, + maxOffsetY = maxOffsetY, + ) + } + } + + private fun updateProgress( + offsetY: Float = abs(indicatorOffset), + location: Int, + maxOffsetY: Float, + ) { + val offsetPercent = min(1f, offsetY / maxOffsetY) + + val offset = min(maxOffsetY, offsetY) + progress = SwipeProgress(location, offset, offsetPercent) + } +} diff --git a/compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/complexlazylist/models/FollowItem.kt b/compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/complexlazylist/models/FollowItem.kt new file mode 100644 index 0000000000000..940b449337179 --- /dev/null +++ b/compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/complexlazylist/models/FollowItem.kt @@ -0,0 +1,16 @@ +package org.jetbrains.compose.benchmarks.app.benchmarks.complexlazylist.models + +class FollowItem() : IBaseViewModel { + override val blockId: String? + get() = TODO("Not yet implemented") + override val reportInfo: Map? + get() = TODO("Not yet implemented") + override val operations: Map? + get() = TODO("Not yet implemented") + override val flipInfos: Map? + get() = TODO("Not yet implemented") + override val extraData: Map? + get() = TODO("Not yet implemented") + override val data: Map? + get() = TODO("Not yet implemented") +} \ No newline at end of file diff --git a/compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/complexlazylist/models/HotItem.kt b/compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/complexlazylist/models/HotItem.kt new file mode 100644 index 0000000000000..48a3f81cdc11d --- /dev/null +++ b/compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/complexlazylist/models/HotItem.kt @@ -0,0 +1,16 @@ +package org.jetbrains.compose.benchmarks.app.benchmarks.complexlazylist.models + +class HotItem() : IBaseViewModel { + override val blockId: String? + get() = TODO("Not yet implemented") + override val reportInfo: Map? + get() = TODO("Not yet implemented") + override val operations: Map? + get() = TODO("Not yet implemented") + override val flipInfos: Map? + get() = TODO("Not yet implemented") + override val extraData: Map? + get() = TODO("Not yet implemented") + override val data: Map? + get() = TODO("Not yet implemented") +} diff --git a/compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/complexlazylist/models/IBaseViewModel.kt b/compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/complexlazylist/models/IBaseViewModel.kt new file mode 100644 index 0000000000000..dceb3c780fcdc --- /dev/null +++ b/compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/complexlazylist/models/IBaseViewModel.kt @@ -0,0 +1,10 @@ +package org.jetbrains.compose.benchmarks.app.benchmarks.complexlazylist.models + +interface IBaseViewModel { + val blockId: String? + val reportInfo: Map? + val operations: Map? + val flipInfos: Map? + val extraData: Map? + val data: Map? +} \ No newline at end of file diff --git a/compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/complexlazylist/models/ICompositionModel.kt b/compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/complexlazylist/models/ICompositionModel.kt new file mode 100644 index 0000000000000..f474c2b5e9def --- /dev/null +++ b/compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/complexlazylist/models/ICompositionModel.kt @@ -0,0 +1,21 @@ +package org.jetbrains.compose.benchmarks.app.benchmarks.complexlazylist.models + +interface ICompositionItem { + val bgColor: String? + val radius: String? + val alpha: String? + val shadowColor: String? + val textColor: String? + val text: String? +} + +interface ICompositionModel : IBaseViewModel { + val title: String? + val subtitle: String? + val overlyTopLeft: ICompositionItem? + val overlyView1: ICompositionItem? + val overlyView2: ICompositionItem? + val overlyView3: ICompositionItem? + val overlyTopRight: ICompositionItem? + val label: ICompositionItem? +} \ No newline at end of file diff --git a/compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/complexlazylist/models/ViewModelFactory.kt b/compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/complexlazylist/models/ViewModelFactory.kt new file mode 100644 index 0000000000000..a741349328cb9 --- /dev/null +++ b/compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/complexlazylist/models/ViewModelFactory.kt @@ -0,0 +1,56 @@ +package org.jetbrains.compose.benchmarks.app.benchmarks.complexlazylist.models + + +val fakeItem2 = object : ICompositionItem { + override val bgColor: String? + get() = "" + override val radius: String? + get() = "0.5" + override val alpha: String? + get() = "0.5" + override val shadowColor: String? + get() = "" + override val textColor: String? + get() = "" + override val text: String? + get() = "yqhwjmjdsk" + + "fksghksjhgksjh" + + "gsgjlksgjlk" +} + +open class FakeItem : ICompositionModel { + override val title: String? + get() = "aghjagj" + override val subtitle: String? + get() = "ghjghjghj" + override val overlyTopLeft: ICompositionItem? + get() = fakeItem2 + override val overlyView1: ICompositionItem? + get() = fakeItem2 + override val overlyView2: ICompositionItem? + get() = fakeItem2 + override val overlyView3: ICompositionItem? + get() = fakeItem2 + override val overlyTopRight: ICompositionItem? + get() = fakeItem2 + override val label: ICompositionItem? + get() = fakeItem2 + override val blockId: String? + get() = "" + override val reportInfo: Map? + get() = TODO("Not yet implemented") + override val operations: Map? + get() = TODO("Not yet implemented") + override val flipInfos: Map? + get() = TODO("Not yet implemented") + override val extraData: Map? + get() = TODO("Not yet implemented") + override val data: Map? + get() = TODO("Not yet implemented") +} + +fun createFakeItem(): ICompositionModel = FakeItem() + +internal fun fetchCompositionModels(useJSON: Boolean, callback: (List) -> Unit) { + callback(List(250, { createFakeItem() })) +} diff --git a/compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/heavyshader/HeavyShader.kt b/compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/heavyshader/HeavyShader.kt new file mode 100644 index 0000000000000..2e7fef1f1c9fc --- /dev/null +++ b/compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/heavyshader/HeavyShader.kt @@ -0,0 +1,135 @@ +/* + * Copyright 2020-2026 JetBrains s.r.o. and respective authors and developers. + * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. + */ + +package org.jetbrains.compose.benchmarks.app.benchmarks.heavyshader + +import androidx.compose.animation.core.LinearEasing +import androidx.compose.animation.core.RepeatMode +import androidx.compose.animation.core.animateFloat +import androidx.compose.animation.core.infiniteRepeatable +import androidx.compose.animation.core.rememberInfiniteTransition +import androidx.compose.animation.core.tween +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.background +import androidx.compose.foundation.gestures.scrollBy +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.runtime.withFrameMillis +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.BlendMode +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import kotlin.math.PI +import kotlin.math.cos +import kotlin.math.sin +import kotlinx.coroutines.isActive + +private const val ITEM_COUNT = 800 + +@Composable +fun HeavyShader() { + val listState = rememberLazyListState() + var scrollForward by remember { mutableStateOf(true) } + + LaunchedEffect(Unit) { + while (isActive) { + withFrameMillis { } + val currentItem = listState.firstVisibleItemIndex + if (currentItem == 0) scrollForward = true + if (currentItem > ITEM_COUNT - 100) scrollForward = false + listState.scrollBy(if (scrollForward) 33f else -33f) + } + } + + LazyColumn( + state = listState, + modifier = Modifier.fillMaxSize().background(Color.Black) + ) { + items(ITEM_COUNT) { + HeavyShaderItem() + } + } +} + +@Composable +private fun HeavyShaderItem() { + val time by rememberInfiniteTransition().animateFloat( + initialValue = 0f, + targetValue = 100f, + animationSpec = infiniteRepeatable( + animation = tween(5000, easing = LinearEasing), + repeatMode = RepeatMode.Restart + ) + ) + + Canvas( + modifier = Modifier + .fillMaxWidth() + .height(120.dp) + .padding(8.dp) + ) { + val layers = 60 + + repeat(layers) { layer -> + val offset = layer * 10f + val alpha = 1f - (layer.toFloat() / layers) + + drawRect( + brush = Brush.radialGradient( + colors = listOf( + Color(0xFF00FF00).copy(alpha = alpha), + Color(0xFF0000FF).copy(alpha = alpha * 0.7f), + Color(0xFFFF0000).copy(alpha = alpha * 0.5f), + Color(0xFFFFFF00).copy(alpha = alpha * 0.3f), + Color(0xFF00FFFF).copy(alpha = alpha * 0.1f) + ), + center = Offset( + size.width / 2 + cos(time + layer).toFloat() * 150f, + size.height / 2 + sin(time + layer).toFloat() * 150f + ), + radius = size.minDimension / 2 + offset + ), + blendMode = when (layer % 3) { + 0 -> BlendMode.Screen + 1 -> BlendMode.Overlay + else -> BlendMode.Multiply + } + ) + } + + repeat(60) { i -> + val angle = i * 7.2f + time * 10 + val distance = size.minDimension / 2 + val x = size.width / 2 + cos(angle * PI / 180.0).toFloat() * distance + val y = size.height / 2 + sin(angle * PI / 180.0).toFloat() * distance + + drawCircle( + brush = Brush.radialGradient( + colors = listOf( + Color.White.copy(alpha = 0.8f), + Color.Transparent + ), + center = Offset(x, y), + radius = 30f + ), + radius = 30f, + center = Offset(x, y), + blendMode = BlendMode.Plus + ) + } + } +} diff --git a/compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/lazygrid/LazyGrid.kt b/compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/lazygrid/LazyGrid.kt new file mode 100644 index 0000000000000..525be663b3622 --- /dev/null +++ b/compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/lazygrid/LazyGrid.kt @@ -0,0 +1,97 @@ +package org.jetbrains.compose.benchmarks.app.benchmarks.lazygrid + +import androidx.compose.foundation.gestures.scrollBy +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.lazy.grid.items +import androidx.compose.foundation.lazy.grid.rememberLazyGridState +import androidx.compose.material.Card +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import kotlinx.coroutines.isActive +import kotlin.coroutines.suspendCoroutine + +@Composable +fun LazyGrid(smoothScroll: Boolean = false, withLaunchedEffectInItem: Boolean = false) { + val itemCount = 12000 + val entries = remember {List(itemCount) { Entry("$it") }} + val state = rememberLazyGridState() + + MaterialTheme { + Column { + LazyVerticalGrid( + columns = GridCells.Fixed(4), + modifier = Modifier.fillMaxWidth().semantics { contentDescription = "IamLazy" }, + state = state + ) { + items(entries) { + ListCell(it, withLaunchedEffectInItem) + } + } + } + + } + + var curItem by remember { mutableStateOf(0) } + var direct by remember { mutableStateOf(true) } + if (smoothScroll) { + LaunchedEffect(Unit) { + while (isActive) { + withFrameMillis { } + curItem = state.firstVisibleItemIndex + if (curItem == 0) direct = true + if (curItem > itemCount - 100) direct = false + state.scrollBy(if (direct) 55f else -55f) + } + } + } else { + LaunchedEffect(Unit) { + while(isActive) { + withFrameMillis {} + curItem += if (direct) 50 else -50 + if (curItem >= itemCount) { + direct = false + curItem = itemCount - 1 + } else if (curItem <= 0) { + direct = true + curItem = 0 + } + state.scrollToItem(curItem) + } + } + } +} + +data class Entry(val contents: String) + +@Composable +private fun ListCell(entry: Entry, withLaunchedEffect: Boolean = false) { + Card( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp) + ) { + Text( + text = entry.contents, + textAlign = TextAlign.Center, + style = MaterialTheme.typography.h5, + modifier = Modifier.padding(16.dp) + ) + + if (withLaunchedEffect) { + LaunchedEffect(Unit) { + // Never resumed to imitate some async task running in an item's scope + suspendCoroutine { } + } + } + } +} \ No newline at end of file diff --git a/compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/multipleComponents/Clickable.kt b/compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/multipleComponents/Clickable.kt new file mode 100644 index 0000000000000..e1f9538aa7180 --- /dev/null +++ b/compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/multipleComponents/Clickable.kt @@ -0,0 +1,362 @@ +/* + * Copyright 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// copy of https://github.com/JetBrains/compose-multiplatform-core/blob/d9e875b62e7bb4dd47b6b155d3a787251ff5bd38/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/Clickable.desktop.kt#L78 +// inefficient implementation of clickable for benchmarking purposes (Modifier.Node variant in AOSP is more optimized) + +package org.jetbrains.compose.benchmarks.app.benchmarks.multipleComponents + +import androidx.compose.foundation.Indication +import androidx.compose.foundation.focusable +import androidx.compose.foundation.gestures.awaitEachGesture +import androidx.compose.foundation.hoverable +import androidx.compose.foundation.indication +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.PressInteraction +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.State +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.ui.Modifier +import androidx.compose.ui.composed +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.input.key.Key +import androidx.compose.ui.input.key.KeyEvent +import androidx.compose.ui.input.key.KeyEventType.Companion.KeyDown +import androidx.compose.ui.input.key.KeyEventType.Companion.KeyUp +import androidx.compose.ui.input.key.key +import androidx.compose.ui.input.key.onKeyEvent +import androidx.compose.ui.input.key.type +import androidx.compose.ui.input.pointer.AwaitPointerEventScope +import androidx.compose.ui.input.pointer.PointerButtons +import androidx.compose.ui.input.pointer.PointerEvent +import androidx.compose.ui.input.pointer.PointerEventPass +import androidx.compose.ui.input.pointer.PointerInputScope +import androidx.compose.ui.input.pointer.PointerKeyboardModifiers +import androidx.compose.ui.input.pointer.changedToDown +import androidx.compose.ui.input.pointer.changedToUp +import androidx.compose.ui.input.pointer.isOutOfBounds +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.input.pointer.positionChangeConsumed +import androidx.compose.ui.node.ModifierNodeElement +import androidx.compose.ui.node.SemanticsModifierNode +import androidx.compose.ui.platform.InspectorInfo +import androidx.compose.ui.platform.debugInspectorInfo +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.semantics.SemanticsPropertyReceiver +import androidx.compose.ui.semantics.disabled +import androidx.compose.ui.semantics.onClick +import androidx.compose.ui.semantics.onLongClick +import androidx.compose.ui.unit.center +import androidx.compose.ui.unit.toOffset +import androidx.compose.ui.util.fastAll +import androidx.compose.ui.util.fastAny +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch + +@Immutable +class MouseClickScope( + val buttons: PointerButtons, + val keyboardModifiers: PointerKeyboardModifiers +) + +@Suppress("DEPRECATION") +internal val EmptyClickContext = MouseClickScope( + PointerButtons(0), PointerKeyboardModifiers(0) +) + +fun Modifier.mouseClickable( + enabled: Boolean = true, + onClickLabel: String? = null, + role: Role? = null, + @Suppress("DEPRECATION") onClick: MouseClickScope.() -> Unit +) = composed( + factory = { + val onClickState = rememberUpdatedState(onClick) + val centreOffset = remember { mutableStateOf(Offset.Zero) } + val currentKeyPressInteractions = remember { mutableMapOf() } + val focusRequester = remember { FocusRequester() } + val gesture = if (enabled) { + Modifier.pointerInput(Unit) { + centreOffset.value = size.center.toOffset() + detectTapWithContext( + onTap = { down, _ -> + focusRequester.requestFocus() + onClickState.value.invoke( + MouseClickScope( + down.buttons, + down.keyboardModifiers + ) + ) + } + ) + } + } else { + Modifier + } + Modifier + .focusRequester(focusRequester) + .genericClickableWithoutGesture( + interactionSource = remember { MutableInteractionSource() }, + indication = null, + indicationScope = rememberCoroutineScope(), + keyClickOffset = centreOffset, + enabled = enabled, + onClickLabel = onClickLabel, + currentKeyPressInteractions = currentKeyPressInteractions, + role = role, + onLongClickLabel = null, + onLongClick = null, + onClick = { onClick(EmptyClickContext) } + ) + .then(gesture) + }, + inspectorInfo = debugInspectorInfo { + name = "clickable" + properties["enabled"] = enabled + properties["onClickLabel"] = onClickLabel + properties["role"] = role + properties["onClick"] = onClick + } +) + +internal suspend fun PointerInputScope.detectTapWithContext( + onTap: ((PointerEvent, PointerEvent) -> Unit)? = null +) { + awaitEachGesture { + val down = awaitEventFirstDown().also { + it.changes.forEach { it.consume() } + } + + val up = waitForFirstInboundUpOrCancellation() + if (up != null) { + up.changes.forEach { it.consume() } + onTap?.invoke(down, up) + } + } +} + +private suspend fun AwaitPointerEventScope.awaitEventFirstDown(): PointerEvent { + var event: PointerEvent + do { + event = awaitPointerEvent() + } while ( + !event.changes.fastAll { it.changedToDown() } + ) + return event +} + +private suspend fun AwaitPointerEventScope.waitForFirstInboundUpOrCancellation(): PointerEvent? { + while (true) { + val event = awaitPointerEvent(PointerEventPass.Main) + if (event.changes.fastAll { it.changedToUp() }) { + // All pointers are up + return event + } + + if (event.changes.fastAny { + it.consumed.downChange || it.isOutOfBounds(size, extendedTouchPadding) + } + ) { + return null // Canceled + } + + // Check for cancel by position consumption. We can look on the Final pass of the + // existing pointer event because it comes after the Main pass we checked above. + val consumeCheck = awaitPointerEvent(PointerEventPass.Final) + if (consumeCheck.changes.fastAny { it.positionChangeConsumed() }) { + return null + } + } +} + +internal fun Modifier.genericClickableWithoutGesture( + interactionSource: MutableInteractionSource, + indication: Indication?, + indicationScope: CoroutineScope, + currentKeyPressInteractions: MutableMap, + keyClickOffset: State, + enabled: Boolean = true, + onClickLabel: String? = null, + role: Role? = null, + onLongClickLabel: String? = null, + onLongClick: (() -> Unit)? = null, + onClick: () -> Unit +): Modifier { + fun Modifier.detectPressAndClickFromKey() = this.onKeyEvent { keyEvent -> + when { + enabled && keyEvent.isPress -> { + // If the key already exists in the map, keyEvent is a repeat event. + // We ignore it as we only want to emit an interaction for the initial key press. + if (!currentKeyPressInteractions.containsKey(keyEvent.key)) { + val press = PressInteraction.Press(keyClickOffset.value) + currentKeyPressInteractions[keyEvent.key] = press + indicationScope.launch { interactionSource.emit(press) } + true + } else { + false + } + } + enabled && keyEvent.isClick -> { + currentKeyPressInteractions.remove(keyEvent.key)?.let { + indicationScope.launch { + interactionSource.emit(PressInteraction.Release(it)) + } + } + onClick() + true + } + else -> false + } + } + return this then + ClickableSemanticsElement( + enabled = enabled, + role = role, + onLongClickLabel = onLongClickLabel, + onLongClick = onLongClick, + onClickLabel = onClickLabel, + onClick = onClick + ) + .detectPressAndClickFromKey() + .indication(interactionSource, indication) + .hoverable(enabled = enabled, interactionSource = interactionSource) + .focusable(enabled = enabled, interactionSource = interactionSource) +} + +private class ClickableSemanticsElement( + private val enabled: Boolean, + private val role: Role?, + private val onLongClickLabel: String?, + private val onLongClick: (() -> Unit)?, + private val onClickLabel: String?, + private val onClick: () -> Unit +) : ModifierNodeElement() { + override fun create() = ClickableSemanticsNode( + enabled = enabled, + role = role, + onLongClickLabel = onLongClickLabel, + onLongClick = onLongClick, + onClickLabel = onClickLabel, + onClick = onClick + ) + + override fun update(node: ClickableSemanticsNode) { + node.update(enabled, onClickLabel, role, onClick, onLongClickLabel, onLongClick) + } + + override fun InspectorInfo.inspectableProperties() = Unit + + override fun hashCode(): Int { + var result = enabled.hashCode() + result = 31 * result + role.hashCode() + result = 31 * result + onLongClickLabel.hashCode() + result = 31 * result + onLongClick.hashCode() + result = 31 * result + onClickLabel.hashCode() + result = 31 * result + onClick.hashCode() + return result + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is ClickableSemanticsElement) return false + + if (enabled != other.enabled) return false + if (role != other.role) return false + if (onLongClickLabel != other.onLongClickLabel) return false + if (onLongClick !== other.onLongClick) return false + if (onClickLabel != other.onClickLabel) return false + if (onClick !== other.onClick) return false + + return true + } +} + +internal class ClickableSemanticsNode( + private var enabled: Boolean, + private var onClickLabel: String?, + private var role: Role?, + private var onClick: () -> Unit, + private var onLongClickLabel: String?, + private var onLongClick: (() -> Unit)?, +) : SemanticsModifierNode, Modifier.Node() { + fun update( + enabled: Boolean, + onClickLabel: String?, + role: Role?, + onClick: () -> Unit, + onLongClickLabel: String?, + onLongClick: (() -> Unit)?, + ) { + this.enabled = enabled + this.onClickLabel = onClickLabel + this.role = role + this.onClick = onClick + this.onLongClickLabel = onLongClickLabel + this.onLongClick = onLongClick + } + + override val shouldMergeDescendantSemantics: Boolean + get() = true + + override fun SemanticsPropertyReceiver.applySemantics() { + if (this@ClickableSemanticsNode.role != null) { + role = this@ClickableSemanticsNode.role!! + } + onClick( + action = { onClick(); true }, + label = onClickLabel + ) + if (onLongClick != null) { + onLongClick( + action = { onLongClick?.invoke(); true }, + label = onLongClickLabel + ) + } + if (!enabled) { + disabled() + } + } +} + + +/** + * Whether the specified [KeyEvent] should trigger a press for a clickable component, i.e. whether + * it is associated with a press of an enter key or dpad centre. + */ +private val KeyEvent.isPress: Boolean + get() = type == KeyDown && isEnter + +/** + * Whether the specified [KeyEvent] should trigger a click for a clickable component, i.e. whether + * it is associated with a release of an enter key or dpad centre. + */ +private val KeyEvent.isClick: Boolean + get() = type == KeyUp && isEnter + +private val KeyEvent.isEnter: Boolean + get() = + when (key) { + Key.DirectionCenter, + Key.Enter, + Key.NumPadEnter, + Key.Spacebar -> true + else -> false + } diff --git a/compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/multipleComponents/MenuIcon.kt b/compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/multipleComponents/MenuIcon.kt new file mode 100644 index 0000000000000..c3a730d1c9c10 --- /dev/null +++ b/compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/multipleComponents/MenuIcon.kt @@ -0,0 +1,49 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jetbrains.compose.benchmarks.app.benchmarks.multipleComponents + +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.graphics.vector.path +import androidx.compose.ui.unit.dp + +internal val MenuIcon: ImageVector = ImageVector.Builder( + name = "Menu", + defaultWidth = 24.dp, + defaultHeight = 24.dp, + viewportWidth = 24f, + viewportHeight = 24f +).apply { + path(fill = SolidColor(Color.Black)) { + moveTo(3f, 18f) + horizontalLineTo(21f) + verticalLineTo(16f) + horizontalLineTo(3f) + close() + moveTo(3f, 13f) + horizontalLineTo(21f) + verticalLineTo(11f) + horizontalLineTo(3f) + close() + moveTo(3f, 6f) + verticalLineTo(8f) + horizontalLineTo(21f) + verticalLineTo(6f) + close() + } +}.build() diff --git a/compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/multipleComponents/MultipleComponents.kt b/compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/multipleComponents/MultipleComponents.kt new file mode 100644 index 0000000000000..c45ac108d168d --- /dev/null +++ b/compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/multipleComponents/MultipleComponents.kt @@ -0,0 +1,662 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// copy of https://github.com/JetBrains/compose-multiplatform-core/blob/d9e875b62e7bb4dd47b6b155d3a787251ff5bd38/compose/desktop/desktop/samples/src/jvmMain/kotlin/androidx/compose/desktop/examples/example1/Main.jvm.kt#L17 + +package org.jetbrains.compose.benchmarks.app.benchmarks.multipleComponents + +import androidx.compose.animation.animateColorAsState +import androidx.compose.animation.core.TweenSpec +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.Image +import androidx.compose.foundation.ScrollState +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.text.InlineTextContent +import androidx.compose.foundation.text.appendInlineContent +import androidx.compose.foundation.text.selection.SelectionContainer +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.BottomAppBar +import androidx.compose.material.Button +import androidx.compose.material.ButtonDefaults +import androidx.compose.material.Checkbox +import androidx.compose.material.CircularProgressIndicator +import androidx.compose.material.DropdownMenu +import androidx.compose.material.DropdownMenuItem +import androidx.compose.material.ExtendedFloatingActionButton +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Scaffold +import androidx.compose.material.Slider +import androidx.compose.material.Switch +import androidx.compose.material.Text +import androidx.compose.material.TextField +import androidx.compose.material.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.produceState +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusProperties +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Color.Companion.Black +import androidx.compose.ui.graphics.Color.Companion.Blue +import androidx.compose.ui.graphics.Color.Companion.Green +import androidx.compose.ui.graphics.Color.Companion.Red +import androidx.compose.ui.graphics.Color.Companion.Yellow +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.graphics.ImageShader +import androidx.compose.ui.graphics.ShaderBrush +import androidx.compose.ui.graphics.Shadow +import androidx.compose.ui.graphics.TileMode +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.input.key.Key +import androidx.compose.ui.input.key.isMetaPressed +import androidx.compose.ui.input.key.isShiftPressed +import androidx.compose.ui.input.key.key +import androidx.compose.ui.input.key.onPreviewKeyEvent +import androidx.compose.ui.input.pointer.PointerEventType +import androidx.compose.ui.input.pointer.PointerIcon +import androidx.compose.ui.input.pointer.isAltPressed +import androidx.compose.ui.input.pointer.isBackPressed +import androidx.compose.ui.input.pointer.isCtrlPressed +import androidx.compose.ui.input.pointer.isForwardPressed +import androidx.compose.ui.input.pointer.isMetaPressed +import androidx.compose.ui.input.pointer.isPrimaryPressed +import androidx.compose.ui.input.pointer.isSecondaryPressed +import androidx.compose.ui.input.pointer.isShiftPressed +import androidx.compose.ui.input.pointer.isTertiaryPressed +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.input.pointer.pointerHoverIcon +import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.text.ExperimentalTextApi +import androidx.compose.ui.text.Placeholder +import androidx.compose.ui.text.PlaceholderVerticalAlign +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.text.style.TextDecoration.Companion.Underline +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.em +import androidx.compose.ui.unit.sp +import compose_benchmarks.benchmarks.generated.resources.Res +import compose_benchmarks.benchmarks.generated.resources.example1_cat +import compose_benchmarks.benchmarks.generated.resources.example1_ic_call_answer +import compose_benchmarks.benchmarks.generated.resources.example1_sailing +import compose_benchmarks.benchmarks.generated.resources.jetbrainsmono_italic +import compose_benchmarks.benchmarks.generated.resources.jetbrainsmono_regular +import org.jetbrains.compose.resources.ExperimentalResourceApi +import org.jetbrains.compose.resources.Font +import org.jetbrains.compose.resources.InternalResourceApi +import org.jetbrains.compose.resources.decodeToImageBitmap +import org.jetbrains.compose.resources.painterResource +import kotlin.random.Random + +@Composable +fun MultipleComponentsExample(isVectorGraphicsSupported: Boolean = true) { + val uriHandler = LocalUriHandler.current + MaterialTheme { + Scaffold( + topBar = { + TopAppBar( + title = { + Row(verticalAlignment = Alignment.CenterVertically) { + if (isVectorGraphicsSupported) { + Image( + painterResource(Res.drawable.example1_sailing), + contentDescription = "Star" + ) + } + Text("Desktop Compose Elements") + } + } + ) + }, + floatingActionButton = { + ExtendedFloatingActionButton( + text = { Text("Open URL") }, + onClick = { + uriHandler.openUri("https://google.com") + } + ) + }, + isFloatingActionButtonDocked = true, + bottomBar = { + BottomAppBar(cutoutShape = CircleShape) { + IconButton( + onClick = {} + ) { + Icon(MenuIcon, "Menu", Modifier.size(ButtonDefaults.IconSize)) + } + } + }, + content = { innerPadding -> + Row(Modifier.padding(innerPadding)) { + LeftColumn(Modifier.weight(1f)) + MiddleColumn(Modifier.width(500.dp), isVectorGraphicsSupported = isVectorGraphicsSupported) + RightColumn(Modifier.width(200.dp)) + } + } + ) + } +} + +@Composable +private fun LeftColumn(modifier: Modifier) = Box(modifier.fillMaxSize()) { + val state = rememberScrollState() + ScrollableContent(state) +} + +@OptIn(ExperimentalComposeUiApi::class) +@Composable +private fun ScrollableContent(scrollState: ScrollState) { + val italicFont = FontFamily( + Font(Res.font.jetbrainsmono_italic) + ) + + val dispatchedFonts = FontFamily( + Font(Res.font.jetbrainsmono_regular, style = FontStyle.Italic), + Font(Res.font.jetbrainsmono_italic, style = FontStyle.Normal) + ) + + val amount = remember { mutableStateOf(0f) } + val animation = remember { mutableStateOf(true) } + Column(Modifier.fillMaxSize().verticalScroll(scrollState)) { + Text( + text = "Привет! 你好! Desktop Compose use: ${amount.value}", + color = Color.Black, + modifier = Modifier + .background(Color.Blue) + .height(56.dp) + .wrapContentSize(Alignment.Center) + ) + + val inlineIndicatorId = "indicator" + + Text( + text = buildAnnotatedString { + append("The quick ") + if (animation.value) { + appendInlineContent(inlineIndicatorId) + } + pushStyle( + SpanStyle( + color = Color(0xff964B00), + shadow = Shadow(Color.Green, offset = Offset(1f, 1f)) + ) + ) + append("brown fox") + pop() + pushStyle(SpanStyle(background = Color.Yellow)) + append(" 🦊 ate a ") + pop() + pushStyle(SpanStyle(fontSize = 30.sp, textDecoration = Underline)) + append("zesty hamburgerfons") + pop() + append(" 🍔.\nThe 👩‍👩‍👧‍👧 laughed.") + addStyle(SpanStyle(color = Color.Green), 25, 35) + }, + color = Color.Black, + inlineContent = mapOf( + inlineIndicatorId to InlineTextContent( + Placeholder( + width = 1.em, + height = 1.em, + placeholderVerticalAlign = PlaceholderVerticalAlign.AboveBaseline + ) + ) { + CircularProgressIndicator(Modifier.padding(end = 3.dp)) + } + ) + ) + + val loremColors = listOf( + Color.Black, + Color.Yellow, + Color.Green, + Color.Blue + ) + var loremColor by remember { mutableStateOf(0) } + + val loremDecorations = listOf( + TextDecoration.None, + TextDecoration.Underline, + TextDecoration.LineThrough + ) + val lorem = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do" + + " eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad" + + " minim veniam, quis nostrud exercitation ullamco laboris nisi ut" + + " aliquipex ea commodo consequat. Duis aute irure dolor in reprehenderit" + + " in voluptate velit esse cillum dolore eu fugiat nulla pariatur." + + " Excepteur" + + " sint occaecat cupidatat non proident, sunt in culpa qui officia" + + " deserunt mollit anim id est laborum." + var loremDecoration by remember { mutableStateOf(0) } + Text( + text = lorem, + color = loremColors[loremColor], + textAlign = TextAlign.Center, + textDecoration = loremDecorations[loremDecoration], + modifier = Modifier.clickable { + if (loremColor < loremColors.size - 1) { + loremColor += 1 + } else { + loremColor = 0 + } + + if (loremDecoration < loremDecorations.size - 1) { + loremDecoration += 1 + } else { + loremDecoration = 0 + } + } + ) + + Text( + text = lorem, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + Row { + Box(modifier = Modifier.size(200.dp, 200.dp).border(1.dp, Black)) { + BrushTextGradient(lorem) + } + Box(modifier = Modifier.size(200.dp, 200.dp).border(1.dp, Black)) { + BrushTextImage(lorem) + } + } + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceEvenly + ) { + Text( + "Default", + ) + + Text( + "SansSerif", + fontFamily = FontFamily.SansSerif + ) + + Text( + "Serif", + fontFamily = FontFamily.Serif + ) + + Text( + "Monospace", + fontFamily = FontFamily.Monospace + ) + + Text( + "Cursive", + fontFamily = FontFamily.Cursive + ) + } + + var overText by remember { mutableStateOf("Move mouse over text:") } + Text(overText, style = TextStyle(letterSpacing = 10.sp)) + + SelectionContainer { + Text( + text = "fun > List.quickSort(): List = when {\n" + + " size < 2 -> this\n" + + " else -> {\n" + + " val pivot = first()\n" + + " val (smaller, greater) = drop(1).partition { it <= pivot }\n" + + " smaller.quickSort() + pivot + greater.quickSort()\n" + + " }\n" + + "}", + fontFamily = italicFont, + modifier = Modifier + .padding(10.dp) + .pointerInput(Unit) { + awaitPointerEventScope { + while (true) { + val event = awaitPointerEvent() + when (event.type) { + PointerEventType.Move -> { + val position = event.changes.first().position + overText = "Move position: $position" + } + PointerEventType.Enter -> overText = "Over enter" + PointerEventType.Exit -> overText = "Over exit" + } + } + } + } + ) + } + Text( + text = buildAnnotatedString { + append("resolved: NotoSans-Regular.ttf ") + pushStyle( + SpanStyle( + fontStyle = FontStyle.Italic + ) + ) + append("NotoSans-italic.ttf.") + }, + fontFamily = dispatchedFonts, + ) + + Row(verticalAlignment = Alignment.CenterVertically) { + Button( + modifier = Modifier.padding(4.dp).pointerHoverIcon(PointerIcon.Hand), + onClick = { + amount.value++ + } + ) { + Text("Base") + } + + var clickableText by remember { mutableStateOf("Click me!") } + Text( + modifier = Modifier.mouseClickable( + onClick = { + clickableText = buildString { + append("Buttons pressed:\n") + append("primary: ${buttons.isPrimaryPressed}\t") + append("secondary: ${buttons.isSecondaryPressed}\t") + append("tertiary: ${buttons.isTertiaryPressed}\t") + append("primary: ${buttons.isPrimaryPressed}\t") + append("back: ${buttons.isBackPressed}\t") + append("forward: ${buttons.isForwardPressed}\t") + + append("\n\nKeyboard modifiers pressed:\n") + + append("alt: ${keyboardModifiers.isAltPressed}\t") + append("ctrl: ${keyboardModifiers.isCtrlPressed}\t") + append("meta: ${keyboardModifiers.isMetaPressed}\t") + append("shift: ${keyboardModifiers.isShiftPressed}\t") + } + } + ), + text = clickableText + ) + } + + Row( + modifier = Modifier.padding(vertical = 10.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Row { + Column { + Switch( + animation.value, + onCheckedChange = { + animation.value = it + } + ) + Row( + modifier = Modifier.padding(4.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Checkbox( + animation.value, + onCheckedChange = { + animation.value = it + } + ) + Text("Animation") + } + } + + Button( + modifier = Modifier.padding(4.dp), + onClick = { } + ) { + Text("Window") + } + } + + Animations(isCircularEnabled = animation.value) + } + + Slider( + value = amount.value / 100f, + onValueChange = { amount.value = (it * 100) } + ) + val dropDownMenuExpanded = remember { mutableStateOf(false) } + Button(onClick = { dropDownMenuExpanded.value = true }) { + Text("Expand Menu") + } + DropdownMenu( + expanded = dropDownMenuExpanded.value, + onDismissRequest = { + dropDownMenuExpanded.value = false + println("OnDismissRequest") + } + ) { + DropdownMenuItem(modifier = Modifier, onClick = { + println("Item 1 clicked") + }) { + Text("Item 1") + } + DropdownMenuItem(modifier = Modifier, onClick = { + println("Item 2 clicked") + }) { + Text("Item 2") + } + DropdownMenuItem(modifier = Modifier, onClick = { + println("Item 3 clicked") + }) { + Text("Item 3") + } + } + TextField( + value = amount.value.toString(), + onValueChange = { amount.value = it.toFloatOrNull() ?: 42f }, + label = { Text(text = "Input1") } + ) + + } +} + +@Composable +fun MiddleColumn(modifier: Modifier, isVectorGraphicsSupported: Boolean) = Column(modifier) { + val (focusItem1, focusItem2) = FocusRequester.createRefs() + val text = remember { + mutableStateOf("Hello \uD83E\uDDD1\uD83C\uDFFF\u200D\uD83E\uDDB0") + } + + TextField( + value = text.value, + onValueChange = { text.value = it }, + label = { Text(text = "Input2") }, + placeholder = { + Text(text = "Important input") + }, + maxLines = 1, + modifier = Modifier.onPreviewKeyEvent { + when { + (it.isMetaPressed && it.key == Key.Enter) -> { + if (it.isShiftPressed) { + text.value = "Cleared with shift!" + } else { + text.value = "Cleared!" + } + true + } + else -> false + } + }.focusRequester(focusItem1) + .focusProperties { + next = focusItem2 + } + ) + + var text2 by remember { + val initText = buildString { + (1..1000).forEach { + append("$it\n") + } + } + mutableStateOf(initText) + } + TextField( + text2, + modifier = Modifier + .height(200.dp) + .focusRequester(focusItem2) + .focusProperties { + previous = focusItem1 + }, + onValueChange = { text2 = it } + ) + + Row { + Image( + painterResource(Res.drawable.example1_cat), + "Localized description", + Modifier.size(200.dp) + ) + + if (isVectorGraphicsSupported) { + Icon( + painterResource(Res.drawable.example1_ic_call_answer), + "Localized description", + Modifier.size(100.dp).align(Alignment.CenterVertically), + tint = Color.Blue.copy(alpha = 0.5f) + ) + } + } + + Box( + modifier = Modifier.size(150.dp).background(Color.Gray).pointerHoverIcon( + PointerIcon.Hand + ) + ) { + Box( + modifier = Modifier.offset(20.dp, 20.dp).size(100.dp).background(Color.Blue).pointerHoverIcon( + PointerIcon.Crosshair + ) + ) { + Text("pointerHoverIcon test with Ctrl") + } + } +} + +@Composable +fun Animations(isCircularEnabled: Boolean) = Row { + if (isCircularEnabled) { + CircularProgressIndicator(Modifier.padding(10.dp)) + } + + val enabled = remember { mutableStateOf(true) } + val color by animateColorAsState( + if (enabled.value) Color.Green else Color.Red, + animationSpec = TweenSpec(durationMillis = 2000) + ) + + MaterialTheme { + Box( + Modifier + .size(70.dp) + .clickable { enabled.value = !enabled.value } + .background(color) + ) + } +} + +@OptIn(ExperimentalFoundationApi::class) +@Composable +private fun RightColumn(modifier: Modifier) = Box(modifier) { + val state = rememberLazyListState() + val itemCount = 100000 + val heights = remember { + val random = Random(24) + (0 until itemCount).map { random.nextFloat() } + } + + LazyColumn(Modifier.fillMaxSize().graphicsLayer(alpha = 0.5f), state = state) { + items((0 until itemCount).toList()) { i -> + val itemHeight = 20.dp + 20.dp * heights[i] + Text(i.toString(), Modifier.graphicsLayer(alpha = 0.5f).height(itemHeight)) + } + } + +} + +@OptIn(ExperimentalTextApi::class) +@Composable +fun BrushTextGradient(text: String) { + Text( + text = text, + style = TextStyle( + fontSize = 12.sp, + fontWeight = FontWeight.Bold, + brush = Brush.linearGradient( + colors = listOf(Red, Yellow, Green, Blue) + ) + ) + ) +} + +@OptIn(ExperimentalTextApi::class, ExperimentalResourceApi::class, InternalResourceApi::class) +@Composable +fun BrushTextImage(text: String) { + val image: ImageBitmap? by produceState(null) { + value = Res.readBytes("files/example1_compose-community-primary.png").decodeToImageBitmap() + } + if (image != null) { + Text( + text = text, + style = TextStyle( + fontSize = 12.sp, + fontWeight = FontWeight.Bold, + brush = ShaderBrush( + ImageShader(image!!, TileMode.Repeated, TileMode.Repeated) + ) + ) + ) + } +} \ No newline at end of file diff --git a/compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/textlayout/TextLayout.kt b/compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/textlayout/TextLayout.kt new file mode 100644 index 0000000000000..2afd2bcd0e856 --- /dev/null +++ b/compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/textlayout/TextLayout.kt @@ -0,0 +1,117 @@ +/* + * Copyright 2020-2026 JetBrains s.r.o. and respective authors and developers. + * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. + */ + +package org.jetbrains.compose.benchmarks.app.benchmarks.textlayout + +import androidx.compose.foundation.background +import androidx.compose.foundation.gestures.scrollBy +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.runtime.withFrameMillis +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import kotlin.random.Random +import kotlinx.coroutines.isActive + +private const val ITEM_COUNT = 2000 + +@Composable +fun TextLayout() { + val listState = rememberLazyListState() + var frame by remember { mutableStateOf(0) } + var scrollForward by remember { mutableStateOf(true) } + + LaunchedEffect(Unit) { + while (isActive) { + withFrameMillis { } + val currentItem = listState.firstVisibleItemIndex + if (currentItem == 0) scrollForward = true + if (currentItem > ITEM_COUNT - 100) scrollForward = false + listState.scrollBy(if (scrollForward) 67f else -67f) + frame++ + } + } + + LazyColumn( + state = listState, + modifier = Modifier.fillMaxSize() + ) { + items(ITEM_COUNT) { index -> + TextLayoutItem(index, frame) + } + } +} + +@Composable +private fun TextLayoutItem(index: Int, frame: Int) { + Column( + modifier = Modifier + .fillMaxWidth() + .background(Color(0xFF2A2A2A)) + ) { + repeat(12) { row -> + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween + ) { + repeat(10) { col -> + Column( + modifier = Modifier + .weight(1f) + .background(Color(0xFF3A3A3A)) + ) { + Text( + "$frame R$row:C$col", + color = Color.White, + fontSize = 6.sp + ) + Row { + Box( + modifier = Modifier + .size(10.dp) + .background(Color(Random.nextInt(0xFFFFFF) or 0xFF000000.toInt())) + ) + Column { + Text("$frame #$index", color = Color.Gray, fontSize = 6.sp) + Text("Item", color = Color.Gray, fontSize = 6.sp) + } + } + Spacer(modifier = Modifier.height(4.dp)) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween + ) { + repeat(10) { + Box( + modifier = Modifier + .size(10.dp) + .background(Color(0xFF4A4A4A)) + ) + } + } + } + } + } + } + } +} diff --git a/compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/visualeffects/HappyNY.kt b/compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/visualeffects/HappyNY.kt new file mode 100644 index 0000000000000..fc03eeffbf63e --- /dev/null +++ b/compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/visualeffects/HappyNY.kt @@ -0,0 +1,321 @@ +package org.jetbrains.compose.benchmarks.app.benchmarks.visualeffects + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Surface +import androidx.compose.runtime.* +import androidx.compose.runtime.snapshots.SnapshotStateList +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.* +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp + +import kotlin.math.* +import kotlin.random.Random + +const val snowCount = 80 +const val starCount = 60 +const val rocketPartsCount = 30 + +data class SnowFlake( + var x: Dp, + var y: Dp, + val scale: Float, + var v: Double, + var alpha: Float, + var angle: Float, + var rotate: Int, + var phase: Double +) + +data class Star(val x: Dp, val y: Dp, val color: Color, val size: Dp) + +val random = Random(123) + +fun random(): Float = random.nextFloat() + +class DoubleRocket(val particle: Particle) { + private val STATE_ROCKET = 0 + private val STATE_SMALL_ROCKETS = 1 + var state = STATE_ROCKET + var rockets: Array = emptyArray() + private fun checkState(time: Long) { + if (particle.vy > -3.0 && state == STATE_ROCKET) { + explode(time) + } + if (state == STATE_SMALL_ROCKETS) { + var done = true + rockets.forEach { + if (!it.exploded) { + it.checkExplode(time) + } + if (!it.checkDone()) { + done = false + } + } + if (done) { + reset() + } + } + } + + private fun reset() { + state = STATE_ROCKET + particle.x = 0.0 + particle.y = 1000.0 + particle.vx = 2.1 + particle.vy = -12.5 + } + + private fun explode(time: Long) { + val colors = arrayOf(Color(0xff, 0, 0), Color(192, 255, 192), Color(192, 212, 255)) + rockets = Array(7) { + val v = 1.2f + 1.0 * random() + val angle = 2 * PI * random() + Rocket( + Particle( + particle.x, + particle.y, + v * sin(angle) + particle.vx, + v * cos(angle) + particle.vy - 0.5f, + colors[it % colors.size] + ), colors[it % colors.size], time + ) + } + state = STATE_SMALL_ROCKETS + } + + fun move(time: Long, prevTime: Long) { + if (rocket.state == rocket.STATE_ROCKET) { + rocket.particle.move(time, prevTime) + rocket.particle.gravity(time, prevTime) + } else { + rocket.rockets.forEach { + it.move(time, prevTime) + } + } + rocket.checkState(time) + } + + @Composable + fun draw() { + if (state == rocket.STATE_ROCKET) { + particle.draw() + } else { + rockets.forEach { + it.draw() + } + } + } + +} + +class Rocket(val particle: Particle, val color: Color, val startTime: Long = 0) { + var exploded = false + var parts: Array = emptyArray() + + fun checkExplode(time: Long) { + if (time - startTime > 1200000000) { + explode() + } + } + + private fun explode() { + parts = Array(rocketPartsCount) { + val v = 0.5f + 1.5 * random() + val angle = 2 * PI * random() + Particle(particle.x, particle.y, v * sin(angle) + particle.vx, v * cos(angle) + particle.vy, color, 1) + } + exploded = true + } + + fun checkDone(): Boolean { + if (!exploded) return false + parts.forEach { + if (it.y < 800) return false + } + return true + } + + fun move(time: Long, prevTime: Long) { + if (!exploded) { + particle.move(time, prevTime) + particle.gravity(time, prevTime) + checkExplode(time) + } else { + parts.forEach { + it.move(time, prevTime) + it.gravity(time, prevTime) + } + } + } + + @Composable + fun draw() { + if (!exploded) { + particle.draw() + } else { + parts.forEach { + it.draw() + } + } + } +} + +class Particle(var x: Double, var y: Double, var vx: Double, var vy: Double, val color: Color, val type: Int = 0) { + fun move(time: Long, prevTime: Long) { + x = (x + vx * (time - prevTime) / 30000000) + y = (y + vy * (time - prevTime) / 30000000) + } + + fun gravity(time: Long, prevTime: Long) { + vy = vy + 1.0f * (time - prevTime) / 300000000 + } + + @Composable + fun draw() { + val alphaFactor = if (type == 0) 1.0f else 1 / (1 + abs(vy / 5)).toFloat() + Box(Modifier.size(5.dp).offset(x.dp, y.dp).alpha(alphaFactor).clip(CircleShape).background(color)) + for (i in 1..5) { + Box( + Modifier.size(4.dp).offset((x - vx / 2 * i).dp, (y - vy / 2 * i).dp) + .alpha(alphaFactor * (1 - 0.18f * i)).clip(CircleShape).background(color) + ) + } + } +} + +val rocket = DoubleRocket(Particle(0.0, 1000.0, 2.1, -12.5, Color.White)) + +fun prepareStarsAndSnowFlakes(stars: SnapshotStateList, snowFlakes: SnapshotStateList, width: Int, height: Int) { + for (i in 0..snowCount) { + snowFlakes.add( + SnowFlake( + (50 + (width - 50) * random()).dp, + (height * random()).dp, + 0.1f + 0.2f * random(), + 1.5 + 3 * random(), + (0.4f + 0.4 * random()).toFloat(), + 60 * random(), + Random.nextInt(1, 5) - 3, + random() * 2 * PI + ) + ) + } + val colors = arrayOf(Color.Red, Color.Yellow, Color.Green, Color.Yellow, Color.Cyan, Color.Magenta, Color.White) + for (i in 0..starCount) { + stars.add( + Star( + (width * random()).dp, + (height * random()).dp, + colors[Random.nextInt(colors.size)], + (3 + 5 * random()).dp + ) + ) + } +} + +@Composable +fun NYContent(width: Int, height: Int) { + var time by remember { mutableStateOf(0L) } + val startTime = remember { 0L } + var prevTime by remember { mutableStateOf(0L) } + val snowFlakes = remember { mutableStateListOf() } + val stars = remember { mutableStateListOf() } + var flickering2 by remember { mutableStateOf(true) } + remember { prepareStarsAndSnowFlakes(stars, snowFlakes, width, height) } + + Surface( + modifier = Modifier.fillMaxSize().padding(5.dp).shadow(3.dp, RoundedCornerShape(20.dp)), + color = Color.Black, + shape = RoundedCornerShape(20.dp) + ) { + + LaunchedEffect(Unit) { + while (true) { + withFrameNanos { + prevTime = time + time = it + } + } + } + + if (flickering2) { + if (time - startTime > 15500000000) { //note, that startTime has been updated above + flickering2 = false + } + } + + rocket.move(time, prevTime) + + Box(Modifier.fillMaxSize()) { + + snow(time, prevTime, snowFlakes, startTime, height) + + starrySky(stars) + + rocket.draw() + + } + } +} + + +@Composable +fun starrySky(stars: SnapshotStateList) { + stars.forEach { + star(it.x, it.y, it.color, size = it.size) + } +} + +@Composable +fun star(x: Dp, y: Dp, color: Color = Color.White, size: Dp) { + Box(Modifier.offset(x, y).scale(1.0f, 0.2f).rotate(45f).size(size).background(color)) + Box(Modifier.offset(x, y).scale(0.2f, 1.0f).rotate(45f).size(size).background(color)) +} + +@Composable +fun snow(time: Long, prevTime: Long, snowFlakes: SnapshotStateList, startTime: Long, height: Int) { + val deltaAngle = (time - startTime) / 100000000 + with(LocalDensity.current) { + snowFlakes.forEach { + var y = it.y + ((it.v * (time - prevTime)) / 300000000).dp + if (y > (height + 20).dp) { + y = -20.dp + } + it.y = y + val x = it.x + (15 * sin(time.toDouble() / 3000000000 + it.phase)).dp + snowFlake(Modifier.offset(x, y).scale(it.scale).rotate(it.angle + deltaAngle * it.rotate), it.alpha) + } + } +} + +@Composable +fun snowFlake(modifier: Modifier, alpha: Float = 0.8f) { + Box(modifier) { + snowFlakeInt(0, 0f, 30.dp, 0.dp, alpha) + snowFlakeInt(0, 60f, 15.dp, 25.dp, alpha) + snowFlakeInt(0, 120f, -15.dp, 25.dp, alpha) + snowFlakeInt(0, 180f, -30.dp, 0.dp, alpha) + snowFlakeInt(0, 240f, -15.dp, -25.dp, alpha) + snowFlakeInt(0, 300f, 15.dp, -25.dp, alpha) + } + +} + +@Composable +fun snowFlakeInt(level: Int, angle: Float, shiftX: Dp, shiftY: Dp, alpha: Float) { + if (level > 3) return + Box( + Modifier.offset(shiftX, shiftY).rotate(angle).width(100.dp).height(10.dp).scale(0.6f).alpha(1f) + .background(Color.White.copy(alpha = alpha)) + ) { + snowFlakeInt(level + 1, 30f, 12.dp, 20.dp, alpha * 0.8f) + snowFlakeInt(level + 1, -30f, 12.dp, -20.dp, alpha * 0.8f) + } +} + diff --git a/settings-fork.gradle b/settings-fork.gradle index cc34b77bece3b..35a200417624f 100644 --- a/settings-fork.gradle +++ b/settings-fork.gradle @@ -337,7 +337,6 @@ includeProject(":compose:animation:animation-core-lint") includeProject(":compose:animation:animation-graphics") includeProject(":compose:desktop:desktop") includeProject(":compose:desktop:desktop:desktop-samples", "compose/desktop/desktop/samples") -includeProject(":compose:mpp:benchmarks") includeProject(":compose:mpp") includeProject(":compose:mpp:newbenchmarks:benchmarks-runtime") includeProject(":compose:mpp:newbenchmarks:benchmarks-report") From c7e3254e0664643c9890e87b45ac9b533fc60b30 Mon Sep 17 00:00:00 2001 From: Konstantin Date: Tue, 10 Nov 2026 12:54:15 +0100 Subject: [PATCH 12/61] Separate offscreen runs from UI benchmarks --- compose/mpp/newbenchmarks/README.md | 34 +++++--- .../benchmarks-app/build.gradle.kts | 2 + .../jetbrains/compose/benchmarks/app/App.kt | 32 ++++---- .../compose/benchmarks/app/ArgumentsParser.kt | 64 +++++++++++++++ .../benchmarks/app/RenderBenchmarks.kt | 55 +++++++++++++ .../compose/benchmarks/app/Main.ios.kt | 29 ++++++- .../compose/benchmarks/app/Main.jvm.kt | 12 ++- .../compose/benchmarks/app/Main.macos.kt | 11 ++- .../compose/benchmarks/app/Main.web.kt | 37 +++++++-- .../compose/benchmarks/collector/Main.kt | 48 +++++++++++- .../xcschemes/BenchmarksIosApp.xcscheme | 1 + .../BenchmarksIosApp/BenchmarksIosApp.swift | 51 ++++++------ .../benchmarks-ios-app/README.md | 16 ++-- .../benchmarks-runtime/build.gradle.kts | 9 +++ .../compose/benchmarks/BenchmarkRunner.kt | 78 +++++++++---------- 15 files changed, 370 insertions(+), 109 deletions(-) create mode 100644 compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/ArgumentsParser.kt create mode 100644 compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/RenderBenchmarks.kt diff --git a/compose/mpp/newbenchmarks/README.md b/compose/mpp/newbenchmarks/README.md index 13121058f2a9b..b4ac2cf7e1ca4 100644 --- a/compose/mpp/newbenchmarks/README.md +++ b/compose/mpp/newbenchmarks/README.md @@ -3,7 +3,18 @@ Run all commands from the repository root. Start the collector in one terminal, then start an app in another terminal. -The apps currently display the collector URL and test name only. They do not run benchmarks or send reports yet. +`averageFrame` and `fps` run offscreen without creating an application window or Compose UI host. +The benchmark case is composed only inside the offscreen scene. Both modes use a 1920×1080 scene, +120 target FPS, 100 warmup frames, and 1000 measured frames, then send a report to the collector. +JVM and native macOS runs finish after the report is sent. Browser and iOS runs keep their platform +application alive without creating a UI host. + +Use `realFrames` for measurement in a real UI host. `startup` is not wired into the app yet. +All launch examples below use `averageFrame`; replace it with `fps` to run the paced render benchmark. + +Case names are case-sensitive: `AnimatedVisibility`, `LazyGrid`, `LazyGrid-ItemLaunchedEffect`, +`LazyGrid-SmoothScroll`, `LazyGrid-SmoothScroll-ItemLaunchedEffect`, `VisualEffects`, `LazyList`, +`MultipleComponents`, `MultipleComponents-NoVectorGraphics`, `TextLayout`, `CanvasDrawing`, `HeavyShader`. ## Collector @@ -19,11 +30,11 @@ as `[type]-[UTC ISO time].json` in the output directory. Keep this process runni ## Desktop (JVM) -Pass `bechmarkCollectorUrl` and the test name as two positional arguments: +Pass `benchmarkCollectorUrl`, the test name, and the benchmark type as three positional arguments: ```bash ./gradlew :compose:mpp:newbenchmarks:benchmarks-app:run \ - --args="http://127.0.0.1:8090/reports LazyGrid" --console=plain + --args="http://127.0.0.1:8090/reports LazyGrid averageFrame" --console=plain ``` ## macOS (native ARM64) @@ -34,11 +45,11 @@ On an Apple Silicon Mac, build the executable: ./gradlew :compose:mpp:newbenchmarks:benchmarks-app:linkDebugExecutableMacosArm64 --console=plain ``` -Then run it with the same two arguments: +Then run it with the same three arguments: ```bash ./out/compose-multiplatform-core/compose/mpp/newbenchmarks/benchmarks-app/build/bin/macosArm64/debugExecutable/benchmarks-app.kexe \ - http://127.0.0.1:8090/reports LazyGrid + http://127.0.0.1:8090/reports LazyGrid averageFrame ``` This path assumes the default repository `out` directory. Adjust it if using a custom `OUT_DIR`. @@ -59,14 +70,14 @@ WasmJS: ./gradlew :compose:mpp:newbenchmarks:benchmarks-app:wasmJsBrowserDevelopmentRun --console=plain ``` -Open the address printed by the development server and add the `bechmarkCollectorUrl` and `test` query parameters. +Open the address printed by the development server and add the `benchmarkCollectorUrl`, `test`, and `type` query parameters. For example, if it uses port 8080: ```text -http://localhost:8080/?bechmarkCollectorUrl=http%3A%2F%2F127.0.0.1%3A8090%2Freports&test=LazyGrid +http://localhost:8080/?benchmarkCollectorUrl=http%3A%2F%2F127.0.0.1%3A8090%2Freports&test=LazyGrid&type=averageFrame ``` -URL-encode both parameter values. If the server opens a page without parameters, add them to the +URL-encode parameter values. If the server opens a page without parameters, add them to the address and reload the page. ## iOS (ARM64 simulator or device) @@ -80,9 +91,10 @@ open compose/mpp/newbenchmarks/benchmarks-ios-app/BenchmarksIosApp.xcodeproj 1. Select the **BenchmarksIosApp** scheme. 2. Select an ARM64 iOS simulator or a connected iOS device. 3. Open **Product → Scheme → Edit Scheme → Run → Arguments**. -4. Add two enabled launch arguments, in this order: - - `http://127.0.0.1:8090/reports` (`bechmarkCollectorUrl`) +4. Add three enabled launch arguments, in this order: + - `http://127.0.0.1:8090/reports` (`benchmarkCollectorUrl`) - `LazyGrid` + - `averageFrame` 5. Run the app. Xcode builds the `BenchmarksApp` Kotlin framework automatically. The shared scheme already contains these example arguments. For a physical device, select your @@ -94,4 +106,4 @@ team under **Signing & Capabilities**. See [iOS app setup](benchmarks-ios-app/RE but not from a physical iOS device. Future device reporting will need a reachable host address and a collector configured to listen on a network interface. - Browser reporting will need CORS support in the collector. It is not configured yet. -- These limits do not affect the current apps, which only display the input parameters. +- Without the required network setup, browser and physical-device runs cannot deliver reports. diff --git a/compose/mpp/newbenchmarks/benchmarks-app/build.gradle.kts b/compose/mpp/newbenchmarks/benchmarks-app/build.gradle.kts index 1c8451267d10e..d977c5b500383 100644 --- a/compose/mpp/newbenchmarks/benchmarks-app/build.gradle.kts +++ b/compose/mpp/newbenchmarks/benchmarks-app/build.gradle.kts @@ -59,6 +59,8 @@ kotlin { sourceSets { commonMain.dependencies { + implementation(project(":compose:mpp:newbenchmarks:benchmarks-runtime")) + implementation(project(":compose:mpp:newbenchmarks:benchmarks-report")) implementation(project(":compose:runtime:runtime")) implementation(project(":compose:ui:ui")) implementation(project(":compose:foundation:foundation")) diff --git a/compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/App.kt b/compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/App.kt index f1c0887225a7a..69f709f9bf062 100644 --- a/compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/App.kt +++ b/compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/App.kt @@ -16,21 +16,25 @@ package org.jetbrains.compose.benchmarks.app -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.text.BasicText import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp +import org.jetbrains.compose.benchmarks.MeasureApplicationRealFrames +import org.jetbrains.compose.benchmarks.report.BenchmarkReportType @Composable -internal fun App(bechmarkCollectorUrl: String, testName: String) { - BasicText( - text = "bechmarkCollectorUrl: $bechmarkCollectorUrl\nTest: $testName", - modifier = Modifier.padding(16.dp), - ) -} - -internal fun parseArguments(args: Array): Pair { - require(args.size == 2) { "Usage: benchmarks-app " } - return args[0] to args[1] +internal fun App( + benchmarkCollectorUrl: String, + testName: String, + benchmarkType: BenchmarkReportType, +) { + when (benchmarkType) { + BenchmarkReportType.REAL_FRAMES -> MeasureApplicationRealFrames( + id = testName, + reportApiUrl = benchmarkCollectorUrl, + warmupCount = 100, + frameCount = 1000, + content = benchmarkContent(testName), + ) + BenchmarkReportType.STARTUP -> error("Startup measurement is not implemented in the app yet") + else -> error("Unknown UI benchmark type: $benchmarkType") + } } diff --git a/compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/ArgumentsParser.kt b/compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/ArgumentsParser.kt new file mode 100644 index 0000000000000..768857d1b1216 --- /dev/null +++ b/compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/ArgumentsParser.kt @@ -0,0 +1,64 @@ +/* + * Copyright 2026 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jetbrains.compose.benchmarks.app + +import androidx.compose.runtime.Composable +import org.jetbrains.compose.benchmarks.app.benchmarks.animation.AnimatedVisibility +import org.jetbrains.compose.benchmarks.app.benchmarks.canvasdrawing.CanvasDrawing +import org.jetbrains.compose.benchmarks.app.benchmarks.complexlazylist.components.MainUiNoImageUseModel +import org.jetbrains.compose.benchmarks.app.benchmarks.heavyshader.HeavyShader +import org.jetbrains.compose.benchmarks.app.benchmarks.lazygrid.LazyGrid +import org.jetbrains.compose.benchmarks.app.benchmarks.multipleComponents.MultipleComponentsExample +import org.jetbrains.compose.benchmarks.app.benchmarks.textlayout.TextLayout +import org.jetbrains.compose.benchmarks.app.benchmarks.visualeffects.NYContent +import org.jetbrains.compose.benchmarks.report.BenchmarkReportType + + +internal fun parseBenchmarkType(value: String): BenchmarkReportType = when (value) { + "startup" -> BenchmarkReportType.STARTUP + "realFrames" -> BenchmarkReportType.REAL_FRAMES + "averageFrame" -> BenchmarkReportType.AVERAGE_FRAME + "fps" -> BenchmarkReportType.FPS + else -> error("Unknown benchmark type: $value") +} + +internal fun parseArguments(args: Array): Triple { + require(args.size == 3) { + "Usage: benchmarks-app " + } + return Triple(args[0], args[1], parseBenchmarkType(args[2])) +} + +internal fun benchmarkContent(testName: String): @Composable () -> Unit = when (testName) { + "AnimatedVisibility" -> { { AnimatedVisibility() } } + "LazyGrid" -> { { LazyGrid() } } + "LazyGrid-ItemLaunchedEffect" -> { { LazyGrid(withLaunchedEffectInItem = true) } } + "LazyGrid-SmoothScroll" -> { { LazyGrid(smoothScroll = true) } } + "LazyGrid-SmoothScroll-ItemLaunchedEffect" -> { + { LazyGrid(smoothScroll = true, withLaunchedEffectInItem = true) } + } + "VisualEffects" -> { { NYContent(1920, 1080) } } + "LazyList" -> { { MainUiNoImageUseModel() } } + "MultipleComponents" -> { { MultipleComponentsExample(isVectorGraphicsSupported = true) } } + "MultipleComponents-NoVectorGraphics" -> { + { MultipleComponentsExample(isVectorGraphicsSupported = false) } + } + "TextLayout" -> { { TextLayout() } } + "CanvasDrawing" -> { { CanvasDrawing() } } + "HeavyShader" -> { { HeavyShader() } } + else -> error("Unknown benchmark case: $testName") +} \ No newline at end of file diff --git a/compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/RenderBenchmarks.kt b/compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/RenderBenchmarks.kt new file mode 100644 index 0000000000000..f297d9b76b021 --- /dev/null +++ b/compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/RenderBenchmarks.kt @@ -0,0 +1,55 @@ +/* + * Copyright 2026 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jetbrains.compose.benchmarks.app + +import org.jetbrains.compose.benchmarks.MeasureRenderAverageFrame +import org.jetbrains.compose.benchmarks.MeasureRenderFps +import org.jetbrains.compose.benchmarks.report.BenchmarkReportType + +internal fun BenchmarkReportType.isRenderBenchmark(): Boolean = + this == BenchmarkReportType.AVERAGE_FRAME || this == BenchmarkReportType.FPS + +internal suspend fun runRenderBenchmark( + benchmarkCollectorUrl: String, + testName: String, + benchmarkType: BenchmarkReportType, +) { + val content = benchmarkContent(testName) + when (benchmarkType) { + BenchmarkReportType.AVERAGE_FRAME -> MeasureRenderAverageFrame( + id = testName, + reportApiUrl = benchmarkCollectorUrl, + width = 1920, + height = 1080, + targetFps = 120, + warmupCount = 100, + frameCount = 1000, + content = content, + ) + BenchmarkReportType.FPS -> MeasureRenderFps( + id = testName, + reportApiUrl = benchmarkCollectorUrl, + width = 1920, + height = 1080, + targetFps = 120, + warmupCount = 100, + frameCount = 1000, + content = content, + ) + else -> error("Unknown render benchmark type: $benchmarkType") + } +} diff --git a/compose/mpp/newbenchmarks/benchmarks-app/src/iosMain/kotlin/org/jetbrains/compose/benchmarks/app/Main.ios.kt b/compose/mpp/newbenchmarks/benchmarks-app/src/iosMain/kotlin/org/jetbrains/compose/benchmarks/app/Main.ios.kt index 177f52b49cad2..a1d3f1d4206a3 100644 --- a/compose/mpp/newbenchmarks/benchmarks-app/src/iosMain/kotlin/org/jetbrains/compose/benchmarks/app/Main.ios.kt +++ b/compose/mpp/newbenchmarks/benchmarks-app/src/iosMain/kotlin/org/jetbrains/compose/benchmarks/app/Main.ios.kt @@ -17,8 +17,33 @@ package org.jetbrains.compose.benchmarks.app import androidx.compose.ui.window.ComposeUIViewController +import kotlinx.coroutines.MainScope +import kotlinx.coroutines.launch import platform.UIKit.UIViewController -fun MainViewController(bechmarkCollectorUrl: String, testName: String): UIViewController = ComposeUIViewController { - App(bechmarkCollectorUrl, testName) +fun MainViewController( + benchmarkCollectorUrl: String, + testName: String, + benchmarkType: String, +): UIViewController { + val type = parseBenchmarkType(benchmarkType) + require(!type.isRenderBenchmark()) { "Render benchmarks must be launched without UI" } + return ComposeUIViewController { + App(benchmarkCollectorUrl, testName, type) + } +} + +fun isRenderBenchmark(benchmarkType: String): Boolean = + parseBenchmarkType(benchmarkType).isRenderBenchmark() + +fun runRenderBenchmark( + benchmarkCollectorUrl: String, + testName: String, + benchmarkType: String, +) { + val type = parseBenchmarkType(benchmarkType) + require(type.isRenderBenchmark()) { "Expected averageFrame or fps" } + MainScope().launch { + runRenderBenchmark(benchmarkCollectorUrl, testName, type) + } } diff --git a/compose/mpp/newbenchmarks/benchmarks-app/src/jvmMain/kotlin/org/jetbrains/compose/benchmarks/app/Main.jvm.kt b/compose/mpp/newbenchmarks/benchmarks-app/src/jvmMain/kotlin/org/jetbrains/compose/benchmarks/app/Main.jvm.kt index f0004bc9b8e82..31a6355974f4b 100644 --- a/compose/mpp/newbenchmarks/benchmarks-app/src/jvmMain/kotlin/org/jetbrains/compose/benchmarks/app/Main.jvm.kt +++ b/compose/mpp/newbenchmarks/benchmarks-app/src/jvmMain/kotlin/org/jetbrains/compose/benchmarks/app/Main.jvm.kt @@ -18,12 +18,20 @@ package org.jetbrains.compose.benchmarks.app import androidx.compose.ui.window.Window import androidx.compose.ui.window.application +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.runBlocking fun main(args: Array) { - val (bechmarkCollectorUrl, testName) = parseArguments(args) + val (benchmarkCollectorUrl, testName, benchmarkType) = parseArguments(args) + if (benchmarkType.isRenderBenchmark()) { + runBlocking(Dispatchers.Main) { + runRenderBenchmark(benchmarkCollectorUrl, testName, benchmarkType) + } + return + } application { Window(onCloseRequest = ::exitApplication, title = "Benchmarks") { - App(bechmarkCollectorUrl, testName) + App(benchmarkCollectorUrl, testName, benchmarkType) } } } diff --git a/compose/mpp/newbenchmarks/benchmarks-app/src/macosMain/kotlin/org/jetbrains/compose/benchmarks/app/Main.macos.kt b/compose/mpp/newbenchmarks/benchmarks-app/src/macosMain/kotlin/org/jetbrains/compose/benchmarks/app/Main.macos.kt index c2aafa743aef0..f233253bf835e 100644 --- a/compose/mpp/newbenchmarks/benchmarks-app/src/macosMain/kotlin/org/jetbrains/compose/benchmarks/app/Main.macos.kt +++ b/compose/mpp/newbenchmarks/benchmarks-app/src/macosMain/kotlin/org/jetbrains/compose/benchmarks/app/Main.macos.kt @@ -17,13 +17,20 @@ package org.jetbrains.compose.benchmarks.app import androidx.compose.ui.window.Window +import kotlinx.coroutines.runBlocking import platform.AppKit.NSApplication import platform.AppKit.NSApplicationActivationPolicy import platform.AppKit.NSApplicationDelegateProtocol import platform.darwin.NSObject fun main(args: Array) { - val (bechmarkCollectorUrl, testName) = parseArguments(args) + val (benchmarkCollectorUrl, testName, benchmarkType) = parseArguments(args) + if (benchmarkType.isRenderBenchmark()) { + runBlocking { + runRenderBenchmark(benchmarkCollectorUrl, testName, benchmarkType) + } + return + } val application = NSApplication.sharedApplication() application.setActivationPolicy(NSApplicationActivationPolicy.NSApplicationActivationPolicyRegular) application.delegate = object : NSObject(), NSApplicationDelegateProtocol { @@ -32,7 +39,7 @@ fun main(args: Array) { } } Window("Benchmarks") { - App(bechmarkCollectorUrl, testName) + App(benchmarkCollectorUrl, testName, benchmarkType) } application.run() } diff --git a/compose/mpp/newbenchmarks/benchmarks-app/src/webMain/kotlin/org/jetbrains/compose/benchmarks/app/Main.web.kt b/compose/mpp/newbenchmarks/benchmarks-app/src/webMain/kotlin/org/jetbrains/compose/benchmarks/app/Main.web.kt index 37097b2ac651e..9b604a937de5f 100644 --- a/compose/mpp/newbenchmarks/benchmarks-app/src/webMain/kotlin/org/jetbrains/compose/benchmarks/app/Main.web.kt +++ b/compose/mpp/newbenchmarks/benchmarks-app/src/webMain/kotlin/org/jetbrains/compose/benchmarks/app/Main.web.kt @@ -16,19 +16,42 @@ package org.jetbrains.compose.benchmarks.app -import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.window.ComposeViewport -import kotlin.js.ExperimentalWasmJsInterop -import kotlin.js.toJsString +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException +import kotlin.js.asJsException import kotlinx.browser.window +import kotlinx.coroutines.MainScope +import kotlinx.coroutines.launch +import kotlinx.coroutines.suspendCancellableCoroutine +import org.jetbrains.skiko.InternalSkikoApi +import org.jetbrains.skiko.wasm.awaitSkiko import org.w3c.dom.url.URLSearchParams -@OptIn(ExperimentalComposeUiApi::class, ExperimentalWasmJsInterop::class) +@OptIn(ExperimentalWasmJsInterop::class) fun main() { val parameters = URLSearchParams(window.location.search.toJsString()) - val bechmarkCollectorUrl = requireNotNull(parameters.get("bechmarkCollectorUrl")) { "Missing query parameter: bechmarkCollectorUrl" } + val benchmarkCollectorUrl = requireNotNull(parameters.get("benchmarkCollectorUrl")) { "Missing query parameter: benchmarkCollectorUrl" } val testName = requireNotNull(parameters.get("test")) { "Missing query parameter: test" } - ComposeViewport("root") { - App(bechmarkCollectorUrl, testName) + val benchmarkType = parseBenchmarkType( + requireNotNull(parameters.get("type")) { "Missing query parameter: type" } + ) + if (benchmarkType.isRenderBenchmark()) { + MainScope().launch { + suspendAwaitSkiko() + runRenderBenchmark(benchmarkCollectorUrl, testName, benchmarkType) + } + } else { + ComposeViewport("root") { + App(benchmarkCollectorUrl, testName, benchmarkType) + } } } + +@OptIn(InternalSkikoApi::class, ExperimentalWasmJsInterop::class) +internal suspend fun suspendAwaitSkiko(): JsAny = suspendCancellableCoroutine { cont -> + awaitSkiko.then( + onFulfilled = { cont.resume(it); null }, + onRejected = { cont.resumeWithException(it.asJsException()); null }, + ) +} diff --git a/compose/mpp/newbenchmarks/benchmarks-collector/src/main/kotlin/org/jetbrains/compose/benchmarks/collector/Main.kt b/compose/mpp/newbenchmarks/benchmarks-collector/src/main/kotlin/org/jetbrains/compose/benchmarks/collector/Main.kt index 60d9e1fd03a44..185d4b71ba068 100644 --- a/compose/mpp/newbenchmarks/benchmarks-collector/src/main/kotlin/org/jetbrains/compose/benchmarks/collector/Main.kt +++ b/compose/mpp/newbenchmarks/benchmarks-collector/src/main/kotlin/org/jetbrains/compose/benchmarks/collector/Main.kt @@ -29,6 +29,8 @@ import java.nio.file.Files import java.nio.file.Path import java.nio.file.StandardOpenOption.CREATE_NEW import java.time.Instant +import kotlin.time.Duration +import kotlin.time.DurationUnit import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import kotlinx.serialization.DeserializationStrategy @@ -70,13 +72,57 @@ internal fun Application.collectReports(outputDirectory: Path) { } val fileName = withContext(Dispatchers.IO) { - saveReport(outputDirectory, report.type, body) + val fileName = saveReport(outputDirectory, report.type, body) + printReport(report, outputDirectory.resolve(fileName)) + fileName } call.respondText(fileName, status = HttpStatusCode.Created) } } } +private fun printReport(report: BenchmarkReport, path: Path) { + println(buildString { + appendLine("${report.id} (${report.type})") + when (report) { + is StartupReport -> { + appendLine("Frames after first: ${report.frameCount}") + appendLine("Process to main: ${report.timeToMain}") + appendLine("Main to first frame: ${report.timeToFirstFrame}") + appendLine("Main to Nth frame: ${report.timeToNthFrame}") + } + is RealFramesReport -> { + appendLine("Frames: ${report.frameCount}, warmup: ${report.warmupCount}") + appendLine("Total duration: ${report.totalDuration}") + if (report.frameCount > 0 && report.totalDuration > Duration.ZERO) { + appendLine("Average frame time: ${report.totalDuration / report.frameCount}") + appendLine("FPS: ${report.frameCount / report.totalDuration.toDouble(DurationUnit.SECONDS)}") + } + } + is AverageFrameReport -> { + appendLine("Size: ${report.width}x${report.height}, target FPS: ${report.targetFps}") + appendLine("Frames: ${report.frameCount}, warmup: ${report.warmupCount}") + appendLine("Average CPU time: ${report.averageFrameInfo.cpu}") + appendLine("Average GPU time: ${report.averageFrameInfo.gpu}") + appendLine("FPS: ${report.averageFrameInfo.fps}") + } + is FpsReport -> { + appendLine("Size: ${report.width}x${report.height}, target FPS: ${report.targetFps}") + appendLine("Frames: ${report.frameCount}, warmup: ${report.warmupCount}") + val frames = report.frames + if (frames.isNotEmpty()) { + val cpu = frames.fold(Duration.ZERO) { total, frame -> total + frame.cpu } + val gpu = frames.fold(Duration.ZERO) { total, frame -> total + frame.gpu } + appendLine("Average CPU time: ${cpu / frames.size}") + appendLine("Average GPU time: ${gpu / frames.size}") + appendLine("Per-frame render FPS range: ${frames.minOf { it.fps }}..${frames.maxOf { it.fps }}") + } + } + } + appendLine("Saved to: $path") + }) +} + private object BenchmarkReportSerializer : JsonContentPolymorphicSerializer(BenchmarkReport::class) { override fun selectDeserializer(element: JsonElement): DeserializationStrategy { diff --git a/compose/mpp/newbenchmarks/benchmarks-ios-app/BenchmarksIosApp.xcodeproj/xcshareddata/xcschemes/BenchmarksIosApp.xcscheme b/compose/mpp/newbenchmarks/benchmarks-ios-app/BenchmarksIosApp.xcodeproj/xcshareddata/xcschemes/BenchmarksIosApp.xcscheme index 0f2e53beb218a..e0f8eef45d48c 100644 --- a/compose/mpp/newbenchmarks/benchmarks-ios-app/BenchmarksIosApp.xcodeproj/xcshareddata/xcschemes/BenchmarksIosApp.xcscheme +++ b/compose/mpp/newbenchmarks/benchmarks-ios-app/BenchmarksIosApp.xcodeproj/xcshareddata/xcschemes/BenchmarksIosApp.xcscheme @@ -14,6 +14,7 @@ + diff --git a/compose/mpp/newbenchmarks/benchmarks-ios-app/BenchmarksIosApp/BenchmarksIosApp.swift b/compose/mpp/newbenchmarks/benchmarks-ios-app/BenchmarksIosApp/BenchmarksIosApp.swift index 72feed73165cf..d2be9c3e88067 100644 --- a/compose/mpp/newbenchmarks/benchmarks-ios-app/BenchmarksIosApp/BenchmarksIosApp.swift +++ b/compose/mpp/newbenchmarks/benchmarks-ios-app/BenchmarksIosApp/BenchmarksIosApp.swift @@ -1,29 +1,36 @@ -import SwiftUI +import UIKit import BenchmarksApp @main -struct ComposeApp: App { - private let arguments = Array(ProcessInfo.processInfo.arguments.dropFirst()) +class AppDelegate: UIResponder, UIApplicationDelegate { + var window: UIWindow? - var body: some Scene { - WindowGroup { - ContentView( - bechmarkCollectorUrl: arguments.first ?? "", - testName: arguments.dropFirst().first ?? "" - ).ignoresSafeArea(.all) - } - } -} - -struct ContentView: UIViewControllerRepresentable { - let bechmarkCollectorUrl: String - let testName: String + func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil + ) -> Bool { + let arguments = Array(ProcessInfo.processInfo.arguments.dropFirst()) + precondition(arguments.count == 3, "Expected collector URL, test name and benchmark type") + let benchmarkCollectorUrl = arguments[0] + let testName = arguments[1] + let benchmarkType = arguments[2] - func makeUIViewController(context: Context) -> UIViewController { - Main_iosKt.MainViewController(bechmarkCollectorUrl: bechmarkCollectorUrl, testName: testName) - } - - func updateUIViewController(_ uiViewController: UIViewController, context: Context) { - // Updates will be handled by Compose. + if Main_iosKt.isRenderBenchmark(benchmarkType: benchmarkType) { + Main_iosKt.runRenderBenchmark( + benchmarkCollectorUrl: benchmarkCollectorUrl, + testName: testName, + benchmarkType: benchmarkType + ) + } else { + let window = UIWindow(frame: UIScreen.main.bounds) + window.rootViewController = Main_iosKt.MainViewController( + benchmarkCollectorUrl: benchmarkCollectorUrl, + testName: testName, + benchmarkType: benchmarkType + ) + window.makeKeyAndVisible() + self.window = window + } + return true } } diff --git a/compose/mpp/newbenchmarks/benchmarks-ios-app/README.md b/compose/mpp/newbenchmarks/benchmarks-ios-app/README.md index ba5c39931da7e..fe7a8a1b9d695 100644 --- a/compose/mpp/newbenchmarks/benchmarks-ios-app/README.md +++ b/compose/mpp/newbenchmarks/benchmarks-ios-app/README.md @@ -1,18 +1,22 @@ # Benchmarks iOS app Open `BenchmarksIosApp.xcodeproj` in Xcode and select the shared `BenchmarksIosApp` scheme. -The project follows the DemoApp setup: SwiftUI hosts the Compose view controller, and a Run Script -builds the static `BenchmarksApp` framework with `embedAndSignAppleFrameworkForXcode` before Swift compilation. +A Run Script builds the static `BenchmarksApp` framework with `embedAndSignAppleFrameworkForXcode` +before Swift compilation, as in DemoApp. A UIKit application delegate selects the launch path: +`averageFrame` and `fps` run without creating a window or Compose view controller; +UI benchmarks use a window with the Compose view controller. ## Parameters -In **Product → Scheme → Edit Scheme → Run → Arguments**, set two launch arguments: +In **Product → Scheme → Edit Scheme → Run → Arguments**, set three launch arguments: -1. `bechmarkCollectorUrl`, for example `http://127.0.0.1:8090/reports`. +1. `benchmarkCollectorUrl`, for example `http://127.0.0.1:8090/reports`. 2. Test name, for example `LazyGrid`. +3. Benchmark type: `averageFrame`, `fps`, or `realFrames`. `startup` is not wired into the app yet. -The shared scheme includes these example values. Both are displayed by the Compose application; -no benchmarks or network requests are performed. When launched without arguments, the values are empty. +The shared scheme uses `averageFrame`. The app runs the selected case and sends a report to the collector. +All three arguments are required. +See the [main README](../README.md) for collector setup and network limits. ## Build settings diff --git a/compose/mpp/newbenchmarks/benchmarks-runtime/build.gradle.kts b/compose/mpp/newbenchmarks/benchmarks-runtime/build.gradle.kts index 2f700027c8b80..6539eb619755e 100644 --- a/compose/mpp/newbenchmarks/benchmarks-runtime/build.gradle.kts +++ b/compose/mpp/newbenchmarks/benchmarks-runtime/build.gradle.kts @@ -44,5 +44,14 @@ kotlin { implementation("io.ktor:ktor-client-content-negotiation:3.6.1") implementation("io.ktor:ktor-serialization-kotlinx-json:3.6.1") } + jvmMain.dependencies { + implementation("io.ktor:ktor-client-cio:3.6.1") + } + appleMain.dependencies { + implementation("io.ktor:ktor-client-darwin:3.6.1") + } + webMain.dependencies { + implementation("io.ktor:ktor-client-js:3.6.1") + } } } diff --git a/compose/mpp/newbenchmarks/benchmarks-runtime/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/BenchmarkRunner.kt b/compose/mpp/newbenchmarks/benchmarks-runtime/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/BenchmarkRunner.kt index 7b03d42170333..5ee6bcbff390a 100644 --- a/compose/mpp/newbenchmarks/benchmarks-runtime/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/BenchmarkRunner.kt +++ b/compose/mpp/newbenchmarks/benchmarks-runtime/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/BenchmarkRunner.kt @@ -105,7 +105,7 @@ fun MeasureApplicationRealFrames( ) } -fun MeasureRenderAverageFrame( +suspend fun MeasureRenderAverageFrame( id: String, reportApiUrl: String, width: Int, @@ -116,31 +116,28 @@ fun MeasureRenderAverageFrame( content: @Composable () -> Unit ) { val measurement = ContentMeasurement(width, height, targetFps, warmupCount, content) - - ReportingScope.launch { - val result = measurement.measureAverageFrame(frameCount) - val report = AverageFrameReport( - id = id, - width = width, - height = height, - targetFps = targetFps, - warmupCount = warmupCount, - frameCount = frameCount, - averageFrameInfo = result, - ) - val client = createHttpClient() - try { - client.post(reportApiUrl) { - contentType(ContentType.Application.Json) - setBody(report) - } - } finally { - client.close() + val result = measurement.measureAverageFrame(frameCount) + val report = AverageFrameReport( + id = id, + width = width, + height = height, + targetFps = targetFps, + warmupCount = warmupCount, + frameCount = frameCount, + averageFrameInfo = result, + ) + val client = createHttpClient() + try { + client.post(reportApiUrl) { + contentType(ContentType.Application.Json) + setBody(report) } + } finally { + client.close() } } -fun MeasureRenderFps( +suspend fun MeasureRenderFps( id: String, reportApiUrl: String, width: Int, @@ -151,26 +148,23 @@ fun MeasureRenderFps( content: @Composable () -> Unit ) { val measurement = ContentMeasurement(width, height, targetFps, warmupCount, content) - - ReportingScope.launch { - val result = measurement.measureRenderFps(frameCount) - val report = FpsReport( - id = id, - width = width, - height = height, - targetFps = targetFps, - warmupCount = warmupCount, - frameCount = frameCount, - frames = result, - ) - val client = createHttpClient() - try { - client.post(reportApiUrl) { - contentType(ContentType.Application.Json) - setBody(report) - } - } finally { - client.close() + val result = measurement.measureRenderFps(frameCount) + val report = FpsReport( + id = id, + width = width, + height = height, + targetFps = targetFps, + warmupCount = warmupCount, + frameCount = frameCount, + frames = result, + ) + val client = createHttpClient() + try { + client.post(reportApiUrl) { + contentType(ContentType.Application.Json) + setBody(report) } + } finally { + client.close() } } From bfe8d44fb99024baf5d34326e3a61b0ba066c089 Mon Sep 17 00:00:00 2001 From: Konstantin Date: Tue, 10 Nov 2026 13:57:31 +0100 Subject: [PATCH 13/61] Add startup timing across platforms --- compose/mpp/newbenchmarks/README.md | 14 ++++- .../benchmarks/app/StartupTiming.apple.kt | 56 +++++++++++++++++++ .../jetbrains/compose/benchmarks/app/App.kt | 11 +++- .../compose/benchmarks/app/StartupTiming.kt | 35 ++++++++++++ .../compose/benchmarks/app/Main.ios.kt | 14 ++++- .../compose/benchmarks/app/Main.jvm.kt | 3 +- .../benchmarks/app/StartupTiming.jvm.kt | 28 ++++++++++ .../compose/benchmarks/app/Main.macos.kt | 3 +- .../compose/benchmarks/app/Main.web.kt | 3 +- .../benchmarks/app/StartupTiming.web.kt | 27 +++++++++ .../compose/benchmarks/collector/Main.kt | 2 +- .../BenchmarksIosApp/BenchmarksIosApp.swift | 5 ++ .../benchmarks-ios-app/README.md | 6 +- .../benchmarks/report/BenchmarkReports.kt | 2 +- .../compose/benchmarks/BenchmarkRunner.kt | 2 +- 15 files changed, 199 insertions(+), 12 deletions(-) create mode 100644 compose/mpp/newbenchmarks/benchmarks-app/src/appleMain/kotlin/org/jetbrains/compose/benchmarks/app/StartupTiming.apple.kt create mode 100644 compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/StartupTiming.kt create mode 100644 compose/mpp/newbenchmarks/benchmarks-app/src/jvmMain/kotlin/org/jetbrains/compose/benchmarks/app/StartupTiming.jvm.kt create mode 100644 compose/mpp/newbenchmarks/benchmarks-app/src/webMain/kotlin/org/jetbrains/compose/benchmarks/app/StartupTiming.web.kt diff --git a/compose/mpp/newbenchmarks/README.md b/compose/mpp/newbenchmarks/README.md index b4ac2cf7e1ca4..dd3ffbc7adc05 100644 --- a/compose/mpp/newbenchmarks/README.md +++ b/compose/mpp/newbenchmarks/README.md @@ -9,8 +9,18 @@ The benchmark case is composed only inside the offscreen scene. Both modes use a JVM and native macOS runs finish after the report is sent. Browser and iOS runs keep their platform application alive without creating a UI host. -Use `realFrames` for measurement in a real UI host. `startup` is not wired into the app yet. -All launch examples below use `averageFrame`; replace it with `fps` to run the paced render benchmark. +Use `realFrames` for measurement in a real UI host, or `startup` for its first frame and 30 additional +frame-clock events without warmup. All launch examples below use `averageFrame`; replace it with +`fps`, `realFrames`, or `startup` to select another mode. + +Startup timings use a monotonic mark captured before arguments and UI are processed. On iOS, the +mark is captured at the first Kotlin entry from `AppDelegate.init`. `timeToFirstFrame` and +`timeToNthFrame` are measured from this mark, not from process startup. They observe the Compose +frame clock, not GPU completion or screen presentation. `timeToMain` measures process-to-entry time: +JVM uses VM uptime as an approximation, Apple uses the process start timestamp, and browsers use +the page's navigation start, reconstructed from `performance.now()`. On the web, `timeToMain` therefore +measures page navigation to Kotlin entry, not browser process startup. +Restart the application for each cold startup measurement. Case names are case-sensitive: `AnimatedVisibility`, `LazyGrid`, `LazyGrid-ItemLaunchedEffect`, `LazyGrid-SmoothScroll`, `LazyGrid-SmoothScroll-ItemLaunchedEffect`, `VisualEffects`, `LazyList`, diff --git a/compose/mpp/newbenchmarks/benchmarks-app/src/appleMain/kotlin/org/jetbrains/compose/benchmarks/app/StartupTiming.apple.kt b/compose/mpp/newbenchmarks/benchmarks-app/src/appleMain/kotlin/org/jetbrains/compose/benchmarks/app/StartupTiming.apple.kt new file mode 100644 index 0000000000000..a158aa82b0b53 --- /dev/null +++ b/compose/mpp/newbenchmarks/benchmarks-app/src/appleMain/kotlin/org/jetbrains/compose/benchmarks/app/StartupTiming.apple.kt @@ -0,0 +1,56 @@ +/* + * Copyright 2026 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jetbrains.compose.benchmarks.app + +import kotlin.time.TimeSource +import kotlinx.cinterop.* +import platform.posix.* +import platform.darwin.* +import kotlin.time.Duration.Companion.nanoseconds + +@OptIn(ExperimentalForeignApi::class) +internal 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) { + error("Failed to get process start time") + } + + // 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/compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/App.kt b/compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/App.kt index 69f709f9bf062..8e7e5fd8f9d33 100644 --- a/compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/App.kt +++ b/compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/App.kt @@ -18,6 +18,7 @@ package org.jetbrains.compose.benchmarks.app import androidx.compose.runtime.Composable import org.jetbrains.compose.benchmarks.MeasureApplicationRealFrames +import org.jetbrains.compose.benchmarks.MeasureApplicationStartup import org.jetbrains.compose.benchmarks.report.BenchmarkReportType @Composable @@ -25,6 +26,7 @@ internal fun App( benchmarkCollectorUrl: String, testName: String, benchmarkType: BenchmarkReportType, + startupTiming: StartupTiming, ) { when (benchmarkType) { BenchmarkReportType.REAL_FRAMES -> MeasureApplicationRealFrames( @@ -34,7 +36,14 @@ internal fun App( frameCount = 1000, content = benchmarkContent(testName), ) - BenchmarkReportType.STARTUP -> error("Startup measurement is not implemented in the app yet") + BenchmarkReportType.STARTUP -> MeasureApplicationStartup( + id = testName, + mainMark = startupTiming.mainMark, + timeToMain = startupTiming.timeToMain, + reportApiUrl = benchmarkCollectorUrl, + frameCount = 30, + content = benchmarkContent(testName), + ) else -> error("Unknown UI benchmark type: $benchmarkType") } } diff --git a/compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/StartupTiming.kt b/compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/StartupTiming.kt new file mode 100644 index 0000000000000..018e0896e3aac --- /dev/null +++ b/compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/StartupTiming.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2026 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jetbrains.compose.benchmarks.app + +import androidx.compose.runtime.Immutable +import kotlin.time.Duration +import kotlin.time.TimeSource.Monotonic + +@Immutable +internal data class StartupTiming( + val mainMark: Monotonic.ValueTimeMark, + val timeToMain: Duration?, +) + +internal fun captureStartupTiming(): StartupTiming { + val mainMark = Monotonic.markNow() + val processStart = getProcessStartTime() + return StartupTiming(mainMark, mainMark - processStart) +} + +internal expect fun getProcessStartTime(): Monotonic.ValueTimeMark diff --git a/compose/mpp/newbenchmarks/benchmarks-app/src/iosMain/kotlin/org/jetbrains/compose/benchmarks/app/Main.ios.kt b/compose/mpp/newbenchmarks/benchmarks-app/src/iosMain/kotlin/org/jetbrains/compose/benchmarks/app/Main.ios.kt index a1d3f1d4206a3..957801ea3586d 100644 --- a/compose/mpp/newbenchmarks/benchmarks-app/src/iosMain/kotlin/org/jetbrains/compose/benchmarks/app/Main.ios.kt +++ b/compose/mpp/newbenchmarks/benchmarks-app/src/iosMain/kotlin/org/jetbrains/compose/benchmarks/app/Main.ios.kt @@ -21,6 +21,15 @@ import kotlinx.coroutines.MainScope import kotlinx.coroutines.launch import platform.UIKit.UIViewController +private var applicationStartupTiming: StartupTiming? = null + +// Called from AppDelegate.init, before launch arguments or UI are processed. +fun initializeStartupTiming() { + if (applicationStartupTiming == null) { + applicationStartupTiming = captureStartupTiming() + } +} + fun MainViewController( benchmarkCollectorUrl: String, testName: String, @@ -28,8 +37,11 @@ fun MainViewController( ): UIViewController { val type = parseBenchmarkType(benchmarkType) require(!type.isRenderBenchmark()) { "Render benchmarks must be launched without UI" } + val startupTiming = requireNotNull(applicationStartupTiming) { + "Call initializeStartupTiming() at application entry before creating the view controller" + } return ComposeUIViewController { - App(benchmarkCollectorUrl, testName, type) + App(benchmarkCollectorUrl, testName, type, startupTiming) } } diff --git a/compose/mpp/newbenchmarks/benchmarks-app/src/jvmMain/kotlin/org/jetbrains/compose/benchmarks/app/Main.jvm.kt b/compose/mpp/newbenchmarks/benchmarks-app/src/jvmMain/kotlin/org/jetbrains/compose/benchmarks/app/Main.jvm.kt index 31a6355974f4b..70609beef54c9 100644 --- a/compose/mpp/newbenchmarks/benchmarks-app/src/jvmMain/kotlin/org/jetbrains/compose/benchmarks/app/Main.jvm.kt +++ b/compose/mpp/newbenchmarks/benchmarks-app/src/jvmMain/kotlin/org/jetbrains/compose/benchmarks/app/Main.jvm.kt @@ -22,6 +22,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.runBlocking fun main(args: Array) { + val startupTiming = captureStartupTiming() val (benchmarkCollectorUrl, testName, benchmarkType) = parseArguments(args) if (benchmarkType.isRenderBenchmark()) { runBlocking(Dispatchers.Main) { @@ -31,7 +32,7 @@ fun main(args: Array) { } application { Window(onCloseRequest = ::exitApplication, title = "Benchmarks") { - App(benchmarkCollectorUrl, testName, benchmarkType) + App(benchmarkCollectorUrl, testName, benchmarkType, startupTiming) } } } diff --git a/compose/mpp/newbenchmarks/benchmarks-app/src/jvmMain/kotlin/org/jetbrains/compose/benchmarks/app/StartupTiming.jvm.kt b/compose/mpp/newbenchmarks/benchmarks-app/src/jvmMain/kotlin/org/jetbrains/compose/benchmarks/app/StartupTiming.jvm.kt new file mode 100644 index 0000000000000..311a22c4f1346 --- /dev/null +++ b/compose/mpp/newbenchmarks/benchmarks-app/src/jvmMain/kotlin/org/jetbrains/compose/benchmarks/app/StartupTiming.jvm.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2026 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jetbrains.compose.benchmarks.app + +import java.lang.management.ManagementFactory +import kotlin.time.Duration.Companion.milliseconds +import kotlin.time.TimeSource.Monotonic + +// RuntimeMXBean reports JVM startup, which is an approximation of process startup. +internal actual fun getProcessStartTime(): Monotonic.ValueTimeMark { + val runtime = ManagementFactory.getRuntimeMXBean() + val now = Monotonic.markNow() + return now - runtime.uptime.milliseconds +} diff --git a/compose/mpp/newbenchmarks/benchmarks-app/src/macosMain/kotlin/org/jetbrains/compose/benchmarks/app/Main.macos.kt b/compose/mpp/newbenchmarks/benchmarks-app/src/macosMain/kotlin/org/jetbrains/compose/benchmarks/app/Main.macos.kt index f233253bf835e..ad9b0f273cd87 100644 --- a/compose/mpp/newbenchmarks/benchmarks-app/src/macosMain/kotlin/org/jetbrains/compose/benchmarks/app/Main.macos.kt +++ b/compose/mpp/newbenchmarks/benchmarks-app/src/macosMain/kotlin/org/jetbrains/compose/benchmarks/app/Main.macos.kt @@ -24,6 +24,7 @@ import platform.AppKit.NSApplicationDelegateProtocol import platform.darwin.NSObject fun main(args: Array) { + val startupTiming = captureStartupTiming() val (benchmarkCollectorUrl, testName, benchmarkType) = parseArguments(args) if (benchmarkType.isRenderBenchmark()) { runBlocking { @@ -39,7 +40,7 @@ fun main(args: Array) { } } Window("Benchmarks") { - App(benchmarkCollectorUrl, testName, benchmarkType) + App(benchmarkCollectorUrl, testName, benchmarkType, startupTiming) } application.run() } diff --git a/compose/mpp/newbenchmarks/benchmarks-app/src/webMain/kotlin/org/jetbrains/compose/benchmarks/app/Main.web.kt b/compose/mpp/newbenchmarks/benchmarks-app/src/webMain/kotlin/org/jetbrains/compose/benchmarks/app/Main.web.kt index 9b604a937de5f..4550beea2e342 100644 --- a/compose/mpp/newbenchmarks/benchmarks-app/src/webMain/kotlin/org/jetbrains/compose/benchmarks/app/Main.web.kt +++ b/compose/mpp/newbenchmarks/benchmarks-app/src/webMain/kotlin/org/jetbrains/compose/benchmarks/app/Main.web.kt @@ -30,6 +30,7 @@ import org.w3c.dom.url.URLSearchParams @OptIn(ExperimentalWasmJsInterop::class) fun main() { + val startupTiming = captureStartupTiming() val parameters = URLSearchParams(window.location.search.toJsString()) val benchmarkCollectorUrl = requireNotNull(parameters.get("benchmarkCollectorUrl")) { "Missing query parameter: benchmarkCollectorUrl" } val testName = requireNotNull(parameters.get("test")) { "Missing query parameter: test" } @@ -43,7 +44,7 @@ fun main() { } } else { ComposeViewport("root") { - App(benchmarkCollectorUrl, testName, benchmarkType) + App(benchmarkCollectorUrl, testName, benchmarkType, startupTiming) } } } diff --git a/compose/mpp/newbenchmarks/benchmarks-app/src/webMain/kotlin/org/jetbrains/compose/benchmarks/app/StartupTiming.web.kt b/compose/mpp/newbenchmarks/benchmarks-app/src/webMain/kotlin/org/jetbrains/compose/benchmarks/app/StartupTiming.web.kt new file mode 100644 index 0000000000000..30b88d66ca754 --- /dev/null +++ b/compose/mpp/newbenchmarks/benchmarks-app/src/webMain/kotlin/org/jetbrains/compose/benchmarks/app/StartupTiming.web.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2026 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jetbrains.compose.benchmarks.app + +import kotlin.time.Duration.Companion.milliseconds +import kotlin.time.TimeSource.Monotonic +import kotlinx.browser.window + +// performance.now() is elapsed time since the page's navigation time origin. +internal actual fun getProcessStartTime(): Monotonic.ValueTimeMark { + val elapsedSinceNavigation = window.performance.now().milliseconds + return Monotonic.markNow() - elapsedSinceNavigation +} diff --git a/compose/mpp/newbenchmarks/benchmarks-collector/src/main/kotlin/org/jetbrains/compose/benchmarks/collector/Main.kt b/compose/mpp/newbenchmarks/benchmarks-collector/src/main/kotlin/org/jetbrains/compose/benchmarks/collector/Main.kt index 185d4b71ba068..d5f2f0dae453e 100644 --- a/compose/mpp/newbenchmarks/benchmarks-collector/src/main/kotlin/org/jetbrains/compose/benchmarks/collector/Main.kt +++ b/compose/mpp/newbenchmarks/benchmarks-collector/src/main/kotlin/org/jetbrains/compose/benchmarks/collector/Main.kt @@ -87,7 +87,7 @@ private fun printReport(report: BenchmarkReport, path: Path) { when (report) { is StartupReport -> { appendLine("Frames after first: ${report.frameCount}") - appendLine("Process to main: ${report.timeToMain}") + appendLine("Process to main: ${report.timeToMain ?: "unavailable"}") appendLine("Main to first frame: ${report.timeToFirstFrame}") appendLine("Main to Nth frame: ${report.timeToNthFrame}") } diff --git a/compose/mpp/newbenchmarks/benchmarks-ios-app/BenchmarksIosApp/BenchmarksIosApp.swift b/compose/mpp/newbenchmarks/benchmarks-ios-app/BenchmarksIosApp/BenchmarksIosApp.swift index d2be9c3e88067..3e55100fe6938 100644 --- a/compose/mpp/newbenchmarks/benchmarks-ios-app/BenchmarksIosApp/BenchmarksIosApp.swift +++ b/compose/mpp/newbenchmarks/benchmarks-ios-app/BenchmarksIosApp/BenchmarksIosApp.swift @@ -5,6 +5,11 @@ import BenchmarksApp class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? + override init() { + super.init() + Main_iosKt.initializeStartupTiming() + } + func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil diff --git a/compose/mpp/newbenchmarks/benchmarks-ios-app/README.md b/compose/mpp/newbenchmarks/benchmarks-ios-app/README.md index fe7a8a1b9d695..a1a41fc61d2b1 100644 --- a/compose/mpp/newbenchmarks/benchmarks-ios-app/README.md +++ b/compose/mpp/newbenchmarks/benchmarks-ios-app/README.md @@ -12,10 +12,12 @@ In **Product → Scheme → Edit Scheme → Run → Arguments**, set three launc 1. `benchmarkCollectorUrl`, for example `http://127.0.0.1:8090/reports`. 2. Test name, for example `LazyGrid`. -3. Benchmark type: `averageFrame`, `fps`, or `realFrames`. `startup` is not wired into the app yet. +3. Benchmark type: `averageFrame`, `fps`, `realFrames`, or `startup`. The shared scheme uses `averageFrame`. The app runs the selected case and sends a report to the collector. -All three arguments are required. +All three arguments are required. `AppDelegate.init` calls `initializeStartupTiming()` before launch +arguments and UI are processed. This captures the Kotlin entry mark and reads the process start time +for `startup` reports. Launch a new process for each cold startup measurement. See the [main README](../README.md) for collector setup and network limits. ## Build settings diff --git a/compose/mpp/newbenchmarks/benchmarks-report/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/report/BenchmarkReports.kt b/compose/mpp/newbenchmarks/benchmarks-report/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/report/BenchmarkReports.kt index eac3be117e353..8b4c7a5a79c39 100644 --- a/compose/mpp/newbenchmarks/benchmarks-report/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/report/BenchmarkReports.kt +++ b/compose/mpp/newbenchmarks/benchmarks-report/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/report/BenchmarkReports.kt @@ -45,7 +45,7 @@ data class StartupReport( override val id: String, override val type: BenchmarkReportType = BenchmarkReportType.STARTUP, val frameCount: Int, - val timeToMain: Duration, + val timeToMain: Duration? = null, val timeToFirstFrame: Duration, val timeToNthFrame: Duration, ) : BenchmarkReport diff --git a/compose/mpp/newbenchmarks/benchmarks-runtime/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/BenchmarkRunner.kt b/compose/mpp/newbenchmarks/benchmarks-runtime/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/BenchmarkRunner.kt index 5ee6bcbff390a..ac76315fe2d5a 100644 --- a/compose/mpp/newbenchmarks/benchmarks-runtime/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/BenchmarkRunner.kt +++ b/compose/mpp/newbenchmarks/benchmarks-runtime/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/BenchmarkRunner.kt @@ -39,7 +39,7 @@ private val ReportingScope = CoroutineScope(Dispatchers.Default + SupervisorJob( fun MeasureApplicationStartup( id: String, mainMark: Monotonic.ValueTimeMark, - timeToMain: Duration, + timeToMain: Duration?, reportApiUrl: String, frameCount: Int, content: @Composable () -> Unit, From 560eca24910966987e29ac14294251bd0b1638f0 Mon Sep 17 00:00:00 2001 From: Konstantin Date: Tue, 10 Nov 2026 14:18:11 +0100 Subject: [PATCH 14/61] Add run IDs and atomic report saving --- compose/mpp/newbenchmarks/README.md | 23 +++++++----- .../jetbrains/compose/benchmarks/app/App.kt | 7 ++-- .../compose/benchmarks/app/ArgumentsParser.kt | 15 +++++--- .../benchmarks/app/RenderBenchmarks.kt | 7 ++-- .../compose/benchmarks/app/Main.ios.kt | 6 ++-- .../compose/benchmarks/app/Main.jvm.kt | 6 ++-- .../compose/benchmarks/app/Main.macos.kt | 6 ++-- .../compose/benchmarks/app/Main.web.kt | 5 +-- .../compose/benchmarks/collector/Main.kt | 35 ++++++++++++------- .../xcschemes/BenchmarksIosApp.xcscheme | 1 + .../BenchmarksIosApp/BenchmarksIosApp.swift | 9 +++-- .../benchmarks-ios-app/README.md | 5 +-- .../benchmarks/report/BenchmarkReports.kt | 5 +++ .../compose/benchmarks/BenchmarkRunner.kt | 8 +++++ 14 files changed, 94 insertions(+), 44 deletions(-) diff --git a/compose/mpp/newbenchmarks/README.md b/compose/mpp/newbenchmarks/README.md index dd3ffbc7adc05..526508b1e5b75 100644 --- a/compose/mpp/newbenchmarks/README.md +++ b/compose/mpp/newbenchmarks/README.md @@ -35,16 +35,20 @@ Pass the listening port and output directory: --args="8090 $(pwd)/out/benchmark-reports" --console=plain ``` -The endpoint is `POST http://127.0.0.1:8090/reports`. It accepts a benchmark report as JSON and saves it -as `[type]-[UTC ISO time].json` in the output directory. Keep this process running. +The endpoint is `POST http://127.0.0.1:8090/reports`. It accepts a benchmark report as JSON with +`id`, `name`, `type`, and the mode-specific results. The app uses the supplied launch `id` as the +report ID and the case name as `name`. Use a unique ID, such as a UUID, for each run. The collector saves it as `[id]-[type]-[UTC ISO time].json` in the output directory. +The `id` must contain 1–128 ASCII letters, digits, underscores, or hyphens. +It writes a temporary `.tmp` file first, then publishes the complete report with an atomic rename. +Watch only `*.json` files when waiting for reports. Keep this process running. ## Desktop (JVM) -Pass `benchmarkCollectorUrl`, the test name, and the benchmark type as three positional arguments: +Pass `benchmarkCollectorUrl`, the test name, the benchmark type, and `id` as four positional arguments: ```bash ./gradlew :compose:mpp:newbenchmarks:benchmarks-app:run \ - --args="http://127.0.0.1:8090/reports LazyGrid averageFrame" --console=plain + --args="http://127.0.0.1:8090/reports LazyGrid averageFrame run-001" --console=plain ``` ## macOS (native ARM64) @@ -55,11 +59,11 @@ On an Apple Silicon Mac, build the executable: ./gradlew :compose:mpp:newbenchmarks:benchmarks-app:linkDebugExecutableMacosArm64 --console=plain ``` -Then run it with the same three arguments: +Then run it with the same four arguments: ```bash ./out/compose-multiplatform-core/compose/mpp/newbenchmarks/benchmarks-app/build/bin/macosArm64/debugExecutable/benchmarks-app.kexe \ - http://127.0.0.1:8090/reports LazyGrid averageFrame + http://127.0.0.1:8090/reports LazyGrid averageFrame run-001 ``` This path assumes the default repository `out` directory. Adjust it if using a custom `OUT_DIR`. @@ -80,11 +84,11 @@ WasmJS: ./gradlew :compose:mpp:newbenchmarks:benchmarks-app:wasmJsBrowserDevelopmentRun --console=plain ``` -Open the address printed by the development server and add the `benchmarkCollectorUrl`, `test`, and `type` query parameters. +Open the address printed by the development server and add the `benchmarkCollectorUrl`, `test`, `type`, and `id` query parameters. For example, if it uses port 8080: ```text -http://localhost:8080/?benchmarkCollectorUrl=http%3A%2F%2F127.0.0.1%3A8090%2Freports&test=LazyGrid&type=averageFrame +http://localhost:8080/?benchmarkCollectorUrl=http%3A%2F%2F127.0.0.1%3A8090%2Freports&test=LazyGrid&type=averageFrame&id=run-001 ``` URL-encode parameter values. If the server opens a page without parameters, add them to the @@ -101,10 +105,11 @@ open compose/mpp/newbenchmarks/benchmarks-ios-app/BenchmarksIosApp.xcodeproj 1. Select the **BenchmarksIosApp** scheme. 2. Select an ARM64 iOS simulator or a connected iOS device. 3. Open **Product → Scheme → Edit Scheme → Run → Arguments**. -4. Add three enabled launch arguments, in this order: +4. Add four enabled launch arguments, in this order: - `http://127.0.0.1:8090/reports` (`benchmarkCollectorUrl`) - `LazyGrid` - `averageFrame` + - `run-001` (`id`) 5. Run the app. Xcode builds the `BenchmarksApp` Kotlin framework automatically. The shared scheme already contains these example arguments. For a physical device, select your diff --git a/compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/App.kt b/compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/App.kt index 8e7e5fd8f9d33..6becb896dc56e 100644 --- a/compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/App.kt +++ b/compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/App.kt @@ -27,17 +27,20 @@ internal fun App( testName: String, benchmarkType: BenchmarkReportType, startupTiming: StartupTiming, + id: String, ) { when (benchmarkType) { BenchmarkReportType.REAL_FRAMES -> MeasureApplicationRealFrames( - id = testName, + id = id, + name = testName, reportApiUrl = benchmarkCollectorUrl, warmupCount = 100, frameCount = 1000, content = benchmarkContent(testName), ) BenchmarkReportType.STARTUP -> MeasureApplicationStartup( - id = testName, + id = id, + name = testName, mainMark = startupTiming.mainMark, timeToMain = startupTiming.timeToMain, reportApiUrl = benchmarkCollectorUrl, diff --git a/compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/ArgumentsParser.kt b/compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/ArgumentsParser.kt index 768857d1b1216..95afc65c10d00 100644 --- a/compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/ArgumentsParser.kt +++ b/compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/ArgumentsParser.kt @@ -36,11 +36,18 @@ internal fun parseBenchmarkType(value: String): BenchmarkReportType = when (valu else -> error("Unknown benchmark type: $value") } -internal fun parseArguments(args: Array): Triple { - require(args.size == 3) { - "Usage: benchmarks-app " +internal data class BenchmarkArguments( + val benchmarkCollectorUrl: String, + val testName: String, + val benchmarkType: BenchmarkReportType, + val id: String, +) + +internal fun parseArguments(args: Array): BenchmarkArguments { + require(args.size == 4) { + "Usage: benchmarks-app " } - return Triple(args[0], args[1], parseBenchmarkType(args[2])) + return BenchmarkArguments(args[0], args[1], parseBenchmarkType(args[2]), args[3]) } internal fun benchmarkContent(testName: String): @Composable () -> Unit = when (testName) { diff --git a/compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/RenderBenchmarks.kt b/compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/RenderBenchmarks.kt index f297d9b76b021..ee0cb1e6c35bd 100644 --- a/compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/RenderBenchmarks.kt +++ b/compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/RenderBenchmarks.kt @@ -27,11 +27,13 @@ internal suspend fun runRenderBenchmark( benchmarkCollectorUrl: String, testName: String, benchmarkType: BenchmarkReportType, + id: String, ) { val content = benchmarkContent(testName) when (benchmarkType) { BenchmarkReportType.AVERAGE_FRAME -> MeasureRenderAverageFrame( - id = testName, + id = id, + name = testName, reportApiUrl = benchmarkCollectorUrl, width = 1920, height = 1080, @@ -41,7 +43,8 @@ internal suspend fun runRenderBenchmark( content = content, ) BenchmarkReportType.FPS -> MeasureRenderFps( - id = testName, + id = id, + name = testName, reportApiUrl = benchmarkCollectorUrl, width = 1920, height = 1080, diff --git a/compose/mpp/newbenchmarks/benchmarks-app/src/iosMain/kotlin/org/jetbrains/compose/benchmarks/app/Main.ios.kt b/compose/mpp/newbenchmarks/benchmarks-app/src/iosMain/kotlin/org/jetbrains/compose/benchmarks/app/Main.ios.kt index 957801ea3586d..4da76410b922a 100644 --- a/compose/mpp/newbenchmarks/benchmarks-app/src/iosMain/kotlin/org/jetbrains/compose/benchmarks/app/Main.ios.kt +++ b/compose/mpp/newbenchmarks/benchmarks-app/src/iosMain/kotlin/org/jetbrains/compose/benchmarks/app/Main.ios.kt @@ -34,6 +34,7 @@ fun MainViewController( benchmarkCollectorUrl: String, testName: String, benchmarkType: String, + id: String, ): UIViewController { val type = parseBenchmarkType(benchmarkType) require(!type.isRenderBenchmark()) { "Render benchmarks must be launched without UI" } @@ -41,7 +42,7 @@ fun MainViewController( "Call initializeStartupTiming() at application entry before creating the view controller" } return ComposeUIViewController { - App(benchmarkCollectorUrl, testName, type, startupTiming) + App(benchmarkCollectorUrl, testName, type, startupTiming, id) } } @@ -52,10 +53,11 @@ fun runRenderBenchmark( benchmarkCollectorUrl: String, testName: String, benchmarkType: String, + id: String, ) { val type = parseBenchmarkType(benchmarkType) require(type.isRenderBenchmark()) { "Expected averageFrame or fps" } MainScope().launch { - runRenderBenchmark(benchmarkCollectorUrl, testName, type) + runRenderBenchmark(benchmarkCollectorUrl, testName, type, id) } } diff --git a/compose/mpp/newbenchmarks/benchmarks-app/src/jvmMain/kotlin/org/jetbrains/compose/benchmarks/app/Main.jvm.kt b/compose/mpp/newbenchmarks/benchmarks-app/src/jvmMain/kotlin/org/jetbrains/compose/benchmarks/app/Main.jvm.kt index 70609beef54c9..40e4707bcf0df 100644 --- a/compose/mpp/newbenchmarks/benchmarks-app/src/jvmMain/kotlin/org/jetbrains/compose/benchmarks/app/Main.jvm.kt +++ b/compose/mpp/newbenchmarks/benchmarks-app/src/jvmMain/kotlin/org/jetbrains/compose/benchmarks/app/Main.jvm.kt @@ -23,16 +23,16 @@ import kotlinx.coroutines.runBlocking fun main(args: Array) { val startupTiming = captureStartupTiming() - val (benchmarkCollectorUrl, testName, benchmarkType) = parseArguments(args) + val (benchmarkCollectorUrl, testName, benchmarkType, id) = parseArguments(args) if (benchmarkType.isRenderBenchmark()) { runBlocking(Dispatchers.Main) { - runRenderBenchmark(benchmarkCollectorUrl, testName, benchmarkType) + runRenderBenchmark(benchmarkCollectorUrl, testName, benchmarkType, id) } return } application { Window(onCloseRequest = ::exitApplication, title = "Benchmarks") { - App(benchmarkCollectorUrl, testName, benchmarkType, startupTiming) + App(benchmarkCollectorUrl, testName, benchmarkType, startupTiming, id) } } } diff --git a/compose/mpp/newbenchmarks/benchmarks-app/src/macosMain/kotlin/org/jetbrains/compose/benchmarks/app/Main.macos.kt b/compose/mpp/newbenchmarks/benchmarks-app/src/macosMain/kotlin/org/jetbrains/compose/benchmarks/app/Main.macos.kt index ad9b0f273cd87..ff846acf69914 100644 --- a/compose/mpp/newbenchmarks/benchmarks-app/src/macosMain/kotlin/org/jetbrains/compose/benchmarks/app/Main.macos.kt +++ b/compose/mpp/newbenchmarks/benchmarks-app/src/macosMain/kotlin/org/jetbrains/compose/benchmarks/app/Main.macos.kt @@ -25,10 +25,10 @@ import platform.darwin.NSObject fun main(args: Array) { val startupTiming = captureStartupTiming() - val (benchmarkCollectorUrl, testName, benchmarkType) = parseArguments(args) + val (benchmarkCollectorUrl, testName, benchmarkType, id) = parseArguments(args) if (benchmarkType.isRenderBenchmark()) { runBlocking { - runRenderBenchmark(benchmarkCollectorUrl, testName, benchmarkType) + runRenderBenchmark(benchmarkCollectorUrl, testName, benchmarkType, id) } return } @@ -40,7 +40,7 @@ fun main(args: Array) { } } Window("Benchmarks") { - App(benchmarkCollectorUrl, testName, benchmarkType, startupTiming) + App(benchmarkCollectorUrl, testName, benchmarkType, startupTiming, id) } application.run() } diff --git a/compose/mpp/newbenchmarks/benchmarks-app/src/webMain/kotlin/org/jetbrains/compose/benchmarks/app/Main.web.kt b/compose/mpp/newbenchmarks/benchmarks-app/src/webMain/kotlin/org/jetbrains/compose/benchmarks/app/Main.web.kt index 4550beea2e342..302a5686e40b7 100644 --- a/compose/mpp/newbenchmarks/benchmarks-app/src/webMain/kotlin/org/jetbrains/compose/benchmarks/app/Main.web.kt +++ b/compose/mpp/newbenchmarks/benchmarks-app/src/webMain/kotlin/org/jetbrains/compose/benchmarks/app/Main.web.kt @@ -34,17 +34,18 @@ fun main() { val parameters = URLSearchParams(window.location.search.toJsString()) val benchmarkCollectorUrl = requireNotNull(parameters.get("benchmarkCollectorUrl")) { "Missing query parameter: benchmarkCollectorUrl" } val testName = requireNotNull(parameters.get("test")) { "Missing query parameter: test" } + val id = requireNotNull(parameters.get("id")) { "Missing query parameter: id" } val benchmarkType = parseBenchmarkType( requireNotNull(parameters.get("type")) { "Missing query parameter: type" } ) if (benchmarkType.isRenderBenchmark()) { MainScope().launch { suspendAwaitSkiko() - runRenderBenchmark(benchmarkCollectorUrl, testName, benchmarkType) + runRenderBenchmark(benchmarkCollectorUrl, testName, benchmarkType, id) } } else { ComposeViewport("root") { - App(benchmarkCollectorUrl, testName, benchmarkType, startupTiming) + App(benchmarkCollectorUrl, testName, benchmarkType, startupTiming, id) } } } diff --git a/compose/mpp/newbenchmarks/benchmarks-collector/src/main/kotlin/org/jetbrains/compose/benchmarks/collector/Main.kt b/compose/mpp/newbenchmarks/benchmarks-collector/src/main/kotlin/org/jetbrains/compose/benchmarks/collector/Main.kt index d5f2f0dae453e..884ec9ee3a80f 100644 --- a/compose/mpp/newbenchmarks/benchmarks-collector/src/main/kotlin/org/jetbrains/compose/benchmarks/collector/Main.kt +++ b/compose/mpp/newbenchmarks/benchmarks-collector/src/main/kotlin/org/jetbrains/compose/benchmarks/collector/Main.kt @@ -24,10 +24,9 @@ import io.ktor.server.request.receiveText import io.ktor.server.response.respondText import io.ktor.server.routing.post import io.ktor.server.routing.routing -import java.nio.file.FileAlreadyExistsException import java.nio.file.Files import java.nio.file.Path -import java.nio.file.StandardOpenOption.CREATE_NEW +import java.nio.file.StandardCopyOption.ATOMIC_MOVE import java.time.Instant import kotlin.time.Duration import kotlin.time.DurationUnit @@ -71,8 +70,13 @@ internal fun Application.collectReports(outputDirectory: Path) { return@post } + if (!report.id.matches(Regex("[A-Za-z0-9_-]{1,128}"))) { + call.respondText("Invalid report id", status = HttpStatusCode.BadRequest) + return@post + } + val fileName = withContext(Dispatchers.IO) { - val fileName = saveReport(outputDirectory, report.type, body) + val fileName = saveReport(outputDirectory, report.id, report.type, body) printReport(report, outputDirectory.resolve(fileName)) fileName } @@ -83,7 +87,7 @@ internal fun Application.collectReports(outputDirectory: Path) { private fun printReport(report: BenchmarkReport, path: Path) { println(buildString { - appendLine("${report.id} (${report.type})") + appendLine("${report.name} (${report.type}), id: ${report.id}") when (report) { is StartupReport -> { appendLine("Frames after first: ${report.frameCount}") @@ -138,16 +142,23 @@ private object BenchmarkReportSerializer : } } -private fun saveReport(outputDirectory: Path, type: BenchmarkReportType, body: String): String { +@Synchronized +private fun saveReport(outputDirectory: Path, id: String, type: BenchmarkReportType, body: String): String { val typeName = Json.encodeToJsonElement( BenchmarkReportType.serializer(), type ).jsonPrimitive.content - val fileName = "$typeName-${Instant.now()}.json" - Files.writeString( - outputDirectory.resolve(fileName), - body, - CREATE_NEW - ) - return fileName + var fileName: String + do { + fileName = "$id-$typeName-${Instant.now()}.json" + } while (Files.exists(outputDirectory.resolve(fileName))) + + val temporaryFile = Files.createTempFile(outputDirectory, ".$typeName-", ".tmp") + try { + Files.writeString(temporaryFile, body) + Files.move(temporaryFile, outputDirectory.resolve(fileName), ATOMIC_MOVE) + return fileName + } finally { + Files.deleteIfExists(temporaryFile) + } } diff --git a/compose/mpp/newbenchmarks/benchmarks-ios-app/BenchmarksIosApp.xcodeproj/xcshareddata/xcschemes/BenchmarksIosApp.xcscheme b/compose/mpp/newbenchmarks/benchmarks-ios-app/BenchmarksIosApp.xcodeproj/xcshareddata/xcschemes/BenchmarksIosApp.xcscheme index e0f8eef45d48c..8ec396515b274 100644 --- a/compose/mpp/newbenchmarks/benchmarks-ios-app/BenchmarksIosApp.xcodeproj/xcshareddata/xcschemes/BenchmarksIosApp.xcscheme +++ b/compose/mpp/newbenchmarks/benchmarks-ios-app/BenchmarksIosApp.xcodeproj/xcshareddata/xcschemes/BenchmarksIosApp.xcscheme @@ -15,6 +15,7 @@ + diff --git a/compose/mpp/newbenchmarks/benchmarks-ios-app/BenchmarksIosApp/BenchmarksIosApp.swift b/compose/mpp/newbenchmarks/benchmarks-ios-app/BenchmarksIosApp/BenchmarksIosApp.swift index 3e55100fe6938..68d61b88d7aaf 100644 --- a/compose/mpp/newbenchmarks/benchmarks-ios-app/BenchmarksIosApp/BenchmarksIosApp.swift +++ b/compose/mpp/newbenchmarks/benchmarks-ios-app/BenchmarksIosApp/BenchmarksIosApp.swift @@ -15,23 +15,26 @@ class AppDelegate: UIResponder, UIApplicationDelegate { didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil ) -> Bool { let arguments = Array(ProcessInfo.processInfo.arguments.dropFirst()) - precondition(arguments.count == 3, "Expected collector URL, test name and benchmark type") + precondition(arguments.count == 4, "Expected collector URL, test name, benchmark type and id") let benchmarkCollectorUrl = arguments[0] let testName = arguments[1] let benchmarkType = arguments[2] + let id = arguments[3] if Main_iosKt.isRenderBenchmark(benchmarkType: benchmarkType) { Main_iosKt.runRenderBenchmark( benchmarkCollectorUrl: benchmarkCollectorUrl, testName: testName, - benchmarkType: benchmarkType + benchmarkType: benchmarkType, + id: id ) } else { let window = UIWindow(frame: UIScreen.main.bounds) window.rootViewController = Main_iosKt.MainViewController( benchmarkCollectorUrl: benchmarkCollectorUrl, testName: testName, - benchmarkType: benchmarkType + benchmarkType: benchmarkType, + id: id ) window.makeKeyAndVisible() self.window = window diff --git a/compose/mpp/newbenchmarks/benchmarks-ios-app/README.md b/compose/mpp/newbenchmarks/benchmarks-ios-app/README.md index a1a41fc61d2b1..8f8180e995ce6 100644 --- a/compose/mpp/newbenchmarks/benchmarks-ios-app/README.md +++ b/compose/mpp/newbenchmarks/benchmarks-ios-app/README.md @@ -8,14 +8,15 @@ UI benchmarks use a window with the Compose view controller. ## Parameters -In **Product → Scheme → Edit Scheme → Run → Arguments**, set three launch arguments: +In **Product → Scheme → Edit Scheme → Run → Arguments**, set four launch arguments: 1. `benchmarkCollectorUrl`, for example `http://127.0.0.1:8090/reports`. 2. Test name, for example `LazyGrid`. 3. Benchmark type: `averageFrame`, `fps`, `realFrames`, or `startup`. +4. Report `id`, for example `run-001`. Use a unique ID for each run. The shared scheme uses `averageFrame`. The app runs the selected case and sends a report to the collector. -All three arguments are required. `AppDelegate.init` calls `initializeStartupTiming()` before launch +All four arguments are required. `AppDelegate.init` calls `initializeStartupTiming()` before launch arguments and UI are processed. This captures the Kotlin entry mark and reads the process start time for `startup` reports. Launch a new process for each cold startup measurement. See the [main README](../README.md) for collector setup and network limits. diff --git a/compose/mpp/newbenchmarks/benchmarks-report/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/report/BenchmarkReports.kt b/compose/mpp/newbenchmarks/benchmarks-report/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/report/BenchmarkReports.kt index 8b4c7a5a79c39..f8dab214ce40e 100644 --- a/compose/mpp/newbenchmarks/benchmarks-report/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/report/BenchmarkReports.kt +++ b/compose/mpp/newbenchmarks/benchmarks-report/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/report/BenchmarkReports.kt @@ -37,12 +37,14 @@ enum class BenchmarkReportType { interface BenchmarkReport { val id: String + val name: String val type: BenchmarkReportType } @Serializable data class StartupReport( override val id: String, + override val name: String, override val type: BenchmarkReportType = BenchmarkReportType.STARTUP, val frameCount: Int, val timeToMain: Duration? = null, @@ -53,6 +55,7 @@ data class StartupReport( @Serializable data class RealFramesReport( override val id: String, + override val name: String, override val type: BenchmarkReportType = BenchmarkReportType.REAL_FRAMES, val warmupCount: Int, val frameCount: Int, @@ -70,6 +73,7 @@ data class FrameInfo( @Serializable data class AverageFrameReport( override val id: String, + override val name: String, override val type: BenchmarkReportType = BenchmarkReportType.AVERAGE_FRAME, val width: Int, val height: Int, @@ -82,6 +86,7 @@ data class AverageFrameReport( @Serializable data class FpsReport( override val id: String, + override val name: String, override val type: BenchmarkReportType = BenchmarkReportType.FPS, val width: Int, val height: Int, diff --git a/compose/mpp/newbenchmarks/benchmarks-runtime/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/BenchmarkRunner.kt b/compose/mpp/newbenchmarks/benchmarks-runtime/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/BenchmarkRunner.kt index ac76315fe2d5a..79b2428f4dad0 100644 --- a/compose/mpp/newbenchmarks/benchmarks-runtime/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/BenchmarkRunner.kt +++ b/compose/mpp/newbenchmarks/benchmarks-runtime/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/BenchmarkRunner.kt @@ -38,6 +38,7 @@ private val ReportingScope = CoroutineScope(Dispatchers.Default + SupervisorJob( @Composable fun MeasureApplicationStartup( id: String, + name: String, mainMark: Monotonic.ValueTimeMark, timeToMain: Duration?, reportApiUrl: String, @@ -48,6 +49,7 @@ fun MeasureApplicationStartup( ReportingScope.launch { val report = StartupReport( id = id, + name = name, frameCount = frameCount, timeToMain = timeToMain, timeToFirstFrame = result.timeOfFirstFrame - mainMark, @@ -71,6 +73,7 @@ fun MeasureApplicationStartup( @Composable fun MeasureApplicationRealFrames( id: String, + name: String, reportApiUrl: String, warmupCount: Int, frameCount: Int, @@ -80,6 +83,7 @@ fun MeasureApplicationRealFrames( ReportingScope.launch { val report = RealFramesReport( id = id, + name = name, warmupCount = warmupCount, frameCount = frameCount, frameTimes = result.frameTimes, @@ -107,6 +111,7 @@ fun MeasureApplicationRealFrames( suspend fun MeasureRenderAverageFrame( id: String, + name: String, reportApiUrl: String, width: Int, height: Int, @@ -119,6 +124,7 @@ suspend fun MeasureRenderAverageFrame( val result = measurement.measureAverageFrame(frameCount) val report = AverageFrameReport( id = id, + name = name, width = width, height = height, targetFps = targetFps, @@ -139,6 +145,7 @@ suspend fun MeasureRenderAverageFrame( suspend fun MeasureRenderFps( id: String, + name: String, reportApiUrl: String, width: Int, height: Int, @@ -151,6 +158,7 @@ suspend fun MeasureRenderFps( val result = measurement.measureRenderFps(frameCount) val report = FpsReport( id = id, + name = name, width = width, height = height, targetFps = targetFps, From af98b3fbdde560f8fd9f4f13c48598fa04501d1d Mon Sep 17 00:00:00 2001 From: Konstantin Date: Tue, 10 Nov 2026 14:41:55 +0100 Subject: [PATCH 15/61] Adopt the iOS scene lifecycle --- .../project.pbxproj | 2 + .../BenchmarksIosApp/BenchmarksIosApp.swift | 58 ++++++++++++------- .../BenchmarksIosApp/Info.plist | 17 ++++++ .../benchmarks-ios-app/README.md | 5 +- 4 files changed, 58 insertions(+), 24 deletions(-) diff --git a/compose/mpp/newbenchmarks/benchmarks-ios-app/BenchmarksIosApp.xcodeproj/project.pbxproj b/compose/mpp/newbenchmarks/benchmarks-ios-app/BenchmarksIosApp.xcodeproj/project.pbxproj index defe74cce0369..018b0e4093559 100644 --- a/compose/mpp/newbenchmarks/benchmarks-ios-app/BenchmarksIosApp.xcodeproj/project.pbxproj +++ b/compose/mpp/newbenchmarks/benchmarks-ios-app/BenchmarksIosApp.xcodeproj/project.pbxproj @@ -287,6 +287,7 @@ A93A954629CC810D00F8E227 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { + ARCHS = arm64; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Automatic; @@ -323,6 +324,7 @@ A93A954729CC810D00F8E227 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { + ARCHS = arm64; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Automatic; diff --git a/compose/mpp/newbenchmarks/benchmarks-ios-app/BenchmarksIosApp/BenchmarksIosApp.swift b/compose/mpp/newbenchmarks/benchmarks-ios-app/BenchmarksIosApp/BenchmarksIosApp.swift index 68d61b88d7aaf..5098e1325d019 100644 --- a/compose/mpp/newbenchmarks/benchmarks-ios-app/BenchmarksIosApp/BenchmarksIosApp.swift +++ b/compose/mpp/newbenchmarks/benchmarks-ios-app/BenchmarksIosApp/BenchmarksIosApp.swift @@ -3,7 +3,7 @@ import BenchmarksApp @main class AppDelegate: UIResponder, UIApplicationDelegate { - var window: UIWindow? + fileprivate var arguments: [String] = [] override init() { super.init() @@ -14,31 +14,45 @@ class AppDelegate: UIResponder, UIApplicationDelegate { _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil ) -> Bool { - let arguments = Array(ProcessInfo.processInfo.arguments.dropFirst()) + arguments = Array(ProcessInfo.processInfo.arguments.dropFirst()) precondition(arguments.count == 4, "Expected collector URL, test name, benchmark type and id") - let benchmarkCollectorUrl = arguments[0] - let testName = arguments[1] - let benchmarkType = arguments[2] - let id = arguments[3] - - if Main_iosKt.isRenderBenchmark(benchmarkType: benchmarkType) { + if Main_iosKt.isRenderBenchmark(benchmarkType: arguments[2]) { Main_iosKt.runRenderBenchmark( - benchmarkCollectorUrl: benchmarkCollectorUrl, - testName: testName, - benchmarkType: benchmarkType, - id: id - ) - } else { - let window = UIWindow(frame: UIScreen.main.bounds) - window.rootViewController = Main_iosKt.MainViewController( - benchmarkCollectorUrl: benchmarkCollectorUrl, - testName: testName, - benchmarkType: benchmarkType, - id: id + benchmarkCollectorUrl: arguments[0], + testName: arguments[1], + benchmarkType: arguments[2], + id: arguments[3] ) - window.makeKeyAndVisible() - self.window = window } return true } } + +class SceneDelegate: UIResponder, UIWindowSceneDelegate { + var window: UIWindow? + + func scene( + _ scene: UIScene, + willConnectTo session: UISceneSession, + options connectionOptions: UIScene.ConnectionOptions + ) { + guard let windowScene = scene as? UIWindowScene, + let appDelegate = UIApplication.shared.delegate as? AppDelegate else { + return + } + let arguments = appDelegate.arguments + if Main_iosKt.isRenderBenchmark(benchmarkType: arguments[2]) { + return + } + + let window = UIWindow(windowScene: windowScene) + window.rootViewController = Main_iosKt.MainViewController( + benchmarkCollectorUrl: arguments[0], + testName: arguments[1], + benchmarkType: arguments[2], + id: arguments[3] + ) + window.makeKeyAndVisible() + self.window = window + } +} diff --git a/compose/mpp/newbenchmarks/benchmarks-ios-app/BenchmarksIosApp/Info.plist b/compose/mpp/newbenchmarks/benchmarks-ios-app/BenchmarksIosApp/Info.plist index 903a507566d94..70d3de3a5be75 100644 --- a/compose/mpp/newbenchmarks/benchmarks-ios-app/BenchmarksIosApp/Info.plist +++ b/compose/mpp/newbenchmarks/benchmarks-ios-app/BenchmarksIosApp/Info.plist @@ -4,5 +4,22 @@ CADisableMinimumFrameDurationOnPhone + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneConfigurationName + Default Configuration + UISceneDelegateClassName + $(PRODUCT_MODULE_NAME).SceneDelegate + + + + diff --git a/compose/mpp/newbenchmarks/benchmarks-ios-app/README.md b/compose/mpp/newbenchmarks/benchmarks-ios-app/README.md index 8f8180e995ce6..3061ba00f04f6 100644 --- a/compose/mpp/newbenchmarks/benchmarks-ios-app/README.md +++ b/compose/mpp/newbenchmarks/benchmarks-ios-app/README.md @@ -3,8 +3,9 @@ Open `BenchmarksIosApp.xcodeproj` in Xcode and select the shared `BenchmarksIosApp` scheme. A Run Script builds the static `BenchmarksApp` framework with `embedAndSignAppleFrameworkForXcode` before Swift compilation, as in DemoApp. A UIKit application delegate selects the launch path: -`averageFrame` and `fps` run without creating a window or Compose view controller; -UI benchmarks use a window with the Compose view controller. +`averageFrame` and `fps` run without creating a window or Compose view controller. +The app adopts the UIScene lifecycle. UI benchmarks create their window and Compose view controller +in `SceneDelegate`, while render benchmarks leave the scene without a window. ## Parameters From 1412dfd063e728e2062727171d8da130f451fb47 Mon Sep 17 00:00:00 2001 From: Konstantin Date: Tue, 10 Nov 2026 15:25:50 +0100 Subject: [PATCH 16/61] Setup proguard for the desktop bench app --- compose/mpp/newbenchmarks/benchmarks-app/build.gradle.kts | 3 +++ compose/mpp/newbenchmarks/benchmarks-app/proguard-rules.pro | 6 ++++++ 2 files changed, 9 insertions(+) create mode 100644 compose/mpp/newbenchmarks/benchmarks-app/proguard-rules.pro diff --git a/compose/mpp/newbenchmarks/benchmarks-app/build.gradle.kts b/compose/mpp/newbenchmarks/benchmarks-app/build.gradle.kts index d977c5b500383..967566b5f25a1 100644 --- a/compose/mpp/newbenchmarks/benchmarks-app/build.gradle.kts +++ b/compose/mpp/newbenchmarks/benchmarks-app/build.gradle.kts @@ -87,6 +87,9 @@ compose { desktop { application { mainClass = "org.jetbrains.compose.benchmarks.app.Main_jvmKt" + buildTypes.release.proguard { + configurationFiles.from(project.file("proguard-rules.pro")) + } } } } diff --git a/compose/mpp/newbenchmarks/benchmarks-app/proguard-rules.pro b/compose/mpp/newbenchmarks/benchmarks-app/proguard-rules.pro new file mode 100644 index 0000000000000..1158bd86846f2 --- /dev/null +++ b/compose/mpp/newbenchmarks/benchmarks-app/proguard-rules.pro @@ -0,0 +1,6 @@ +# Ktor loads the JVM engine through META-INF/services. +-keep class io.ktor.client.engine.cio.CIOEngineContainer { *; } +-keep class io.ktor.serialization.kotlinx.json.KotlinxSerializationJsonExtensionProvider { *; } + +# JSON serialization resolves report serializers at runtime. +-keep class org.jetbrains.compose.benchmarks.report.** { *; } From 6313273926f7d46a336d0d7127dc0c77e7357696 Mon Sep 17 00:00:00 2001 From: Konstantin Date: Tue, 10 Nov 2026 15:26:23 +0100 Subject: [PATCH 17/61] Add launch script --- compose/mpp/newbenchmarks/README.md | 124 ------ .../newbenchmarks/launch-scripts/README.md | 118 +++++ .../launch-scripts/run_benchmarks.py | 421 ++++++++++++++++++ 3 files changed, 539 insertions(+), 124 deletions(-) delete mode 100644 compose/mpp/newbenchmarks/README.md create mode 100644 compose/mpp/newbenchmarks/launch-scripts/README.md create mode 100755 compose/mpp/newbenchmarks/launch-scripts/run_benchmarks.py diff --git a/compose/mpp/newbenchmarks/README.md b/compose/mpp/newbenchmarks/README.md deleted file mode 100644 index 526508b1e5b75..0000000000000 --- a/compose/mpp/newbenchmarks/README.md +++ /dev/null @@ -1,124 +0,0 @@ -# Benchmarks - -Run all commands from the repository root. Start the collector in one terminal, then start an app -in another terminal. - -`averageFrame` and `fps` run offscreen without creating an application window or Compose UI host. -The benchmark case is composed only inside the offscreen scene. Both modes use a 1920×1080 scene, -120 target FPS, 100 warmup frames, and 1000 measured frames, then send a report to the collector. -JVM and native macOS runs finish after the report is sent. Browser and iOS runs keep their platform -application alive without creating a UI host. - -Use `realFrames` for measurement in a real UI host, or `startup` for its first frame and 30 additional -frame-clock events without warmup. All launch examples below use `averageFrame`; replace it with -`fps`, `realFrames`, or `startup` to select another mode. - -Startup timings use a monotonic mark captured before arguments and UI are processed. On iOS, the -mark is captured at the first Kotlin entry from `AppDelegate.init`. `timeToFirstFrame` and -`timeToNthFrame` are measured from this mark, not from process startup. They observe the Compose -frame clock, not GPU completion or screen presentation. `timeToMain` measures process-to-entry time: -JVM uses VM uptime as an approximation, Apple uses the process start timestamp, and browsers use -the page's navigation start, reconstructed from `performance.now()`. On the web, `timeToMain` therefore -measures page navigation to Kotlin entry, not browser process startup. -Restart the application for each cold startup measurement. - -Case names are case-sensitive: `AnimatedVisibility`, `LazyGrid`, `LazyGrid-ItemLaunchedEffect`, -`LazyGrid-SmoothScroll`, `LazyGrid-SmoothScroll-ItemLaunchedEffect`, `VisualEffects`, `LazyList`, -`MultipleComponents`, `MultipleComponents-NoVectorGraphics`, `TextLayout`, `CanvasDrawing`, `HeavyShader`. - -## Collector - -Pass the listening port and output directory: - -```bash -./gradlew :compose:mpp:newbenchmarks:benchmarks-collector:run \ - --args="8090 $(pwd)/out/benchmark-reports" --console=plain -``` - -The endpoint is `POST http://127.0.0.1:8090/reports`. It accepts a benchmark report as JSON with -`id`, `name`, `type`, and the mode-specific results. The app uses the supplied launch `id` as the -report ID and the case name as `name`. Use a unique ID, such as a UUID, for each run. The collector saves it as `[id]-[type]-[UTC ISO time].json` in the output directory. -The `id` must contain 1–128 ASCII letters, digits, underscores, or hyphens. -It writes a temporary `.tmp` file first, then publishes the complete report with an atomic rename. -Watch only `*.json` files when waiting for reports. Keep this process running. - -## Desktop (JVM) - -Pass `benchmarkCollectorUrl`, the test name, the benchmark type, and `id` as four positional arguments: - -```bash -./gradlew :compose:mpp:newbenchmarks:benchmarks-app:run \ - --args="http://127.0.0.1:8090/reports LazyGrid averageFrame run-001" --console=plain -``` - -## macOS (native ARM64) - -On an Apple Silicon Mac, build the executable: - -```bash -./gradlew :compose:mpp:newbenchmarks:benchmarks-app:linkDebugExecutableMacosArm64 --console=plain -``` - -Then run it with the same four arguments: - -```bash -./out/compose-multiplatform-core/compose/mpp/newbenchmarks/benchmarks-app/build/bin/macosArm64/debugExecutable/benchmarks-app.kexe \ - http://127.0.0.1:8090/reports LazyGrid averageFrame run-001 -``` - -This path assumes the default repository `out` directory. Adjust it if using a custom `OUT_DIR`. - -## Browser (JS or WasmJS) - -Start one development server at a time. - -JS: - -```bash -./gradlew :compose:mpp:newbenchmarks:benchmarks-app:jsBrowserDevelopmentRun --console=plain -``` - -WasmJS: - -```bash -./gradlew :compose:mpp:newbenchmarks:benchmarks-app:wasmJsBrowserDevelopmentRun --console=plain -``` - -Open the address printed by the development server and add the `benchmarkCollectorUrl`, `test`, `type`, and `id` query parameters. -For example, if it uses port 8080: - -```text -http://localhost:8080/?benchmarkCollectorUrl=http%3A%2F%2F127.0.0.1%3A8090%2Freports&test=LazyGrid&type=averageFrame&id=run-001 -``` - -URL-encode parameter values. If the server opens a page without parameters, add them to the -address and reload the page. - -## iOS (ARM64 simulator or device) - -On macOS with Xcode installed: - -```bash -open compose/mpp/newbenchmarks/benchmarks-ios-app/BenchmarksIosApp.xcodeproj -``` - -1. Select the **BenchmarksIosApp** scheme. -2. Select an ARM64 iOS simulator or a connected iOS device. -3. Open **Product → Scheme → Edit Scheme → Run → Arguments**. -4. Add four enabled launch arguments, in this order: - - `http://127.0.0.1:8090/reports` (`benchmarkCollectorUrl`) - - `LazyGrid` - - `averageFrame` - - `run-001` (`id`) -5. Run the app. Xcode builds the `BenchmarksApp` Kotlin framework automatically. - -The shared scheme already contains these example arguments. For a physical device, select your -team under **Signing & Capabilities**. See [iOS app setup](benchmarks-ios-app/README.md) for more details. - -## Current network limits - -- The collector listens on `127.0.0.1` only. It is accessible from the same Mac and its iOS simulator, - but not from a physical iOS device. Future device reporting will need a reachable host address - and a collector configured to listen on a network interface. -- Browser reporting will need CORS support in the collector. It is not configured yet. -- Without the required network setup, browser and physical-device runs cannot deliver reports. diff --git a/compose/mpp/newbenchmarks/launch-scripts/README.md b/compose/mpp/newbenchmarks/launch-scripts/README.md new file mode 100644 index 0000000000000..48ece0bb6ab6d --- /dev/null +++ b/compose/mpp/newbenchmarks/launch-scripts/README.md @@ -0,0 +1,118 @@ +# Benchmark launcher + +`run_benchmarks.py` uses Python 3 and its standard library. No Playwright or Python packages are needed. +It runs on macOS and Linux. Apple targets require macOS and Xcode. + +Run these examples from the repository root. The script also works from another working directory. + +## Defaults + +- `--target` is required: `jvm`, `macos`, `js`, `wasmJs`, or `ios` (iOS Simulator). +- All 12 cases and all four modes (`averageFrame`, `fps`, `realFrames`, `startup`). +- One fresh application process and UUID per case/mode/repetition. +- `--repeat 1`, `--timeout 30` seconds per test, collector `--port 8090`. +- Build the collector and selected app once before the series. Apps use release/production artifacts. + Build time is not part of the test timeout. Use `--skip-build` to reuse existing artifacts. +- Continue after failed tests and timeouts. Exit with code 1 if any test fails; Ctrl+C exits with code 130. +- Reports go directly into one shared `reports` folder. The script waits for a matching report ID, + name, and type, then closes the test process before starting the next one. +- Stop the collector and web server at the end, including on interruption. Existing browser sessions + are not reused or closed. An iOS Simulator is left booted. + +The collector port must be free: this script starts its own collector, not an existing one. +Do not run two launchers against the same simulator application at the same time. + +## JVM + +Run everything: + +```bash +python3 compose/mpp/newbenchmarks/launch-scripts/run_benchmarks.py --target jvm +``` + +Select cases and modes, with three repetitions of each combination: + +```bash +python3 compose/mpp/newbenchmarks/launch-scripts/run_benchmarks.py \ + --target jvm --tests LazyGrid TextLayout --modes averageFrame startup --repeat 3 +``` + +Lists accept spaces or commas. `--tests all` and `--modes all` restore the defaults. +The JVM app is launched directly from its release uber-JAR, not through a Gradle run task. + +## Native macOS (ARM64) + +```bash +python3 compose/mpp/newbenchmarks/launch-scripts/run_benchmarks.py \ + --target macos --tests LazyGrid --modes averageFrame fps +``` + +## Browser (JS or WasmJS) + +Pass a browser executable, a command on `PATH`, or a macOS `.app` path. +Set `--browser-kind firefox` for Firefox; the default is `chromium`. +Each test gets a temporary browser profile and a separate instance. It is closed after a report, +crash, or timeout. Browser launches are visible by default. + +Chromium: + +```bash +python3 compose/mpp/newbenchmarks/launch-scripts/run_benchmarks.py \ + --target wasmJs --browser "/Applications/Google Chrome.app" \ + --tests LazyGrid --modes startup averageFrame +``` + +Firefox: + +```bash +python3 compose/mpp/newbenchmarks/launch-scripts/run_benchmarks.py \ + --target js --browser "/Applications/Firefox.app" --browser-kind firefox \ + --tests LazyGrid --modes averageFrame +``` + +Use `--browser-arg=--some-flag` to pass extra browser flags. For Chromium, for example: +`--browser-arg=--js-flags=--expose-gc` enables manual GC. + +The launcher serves production files on a free loopback port (`--web-port` can override it). +It forwards same-origin `POST /reports` requests to the collector. No CORS setup is needed. +The browser receives all application parameters in the page URL. + +## iOS Simulator (ARM64) + +Without `--simulator`, exactly one available simulator must already be booted: + +```bash +python3 compose/mpp/newbenchmarks/launch-scripts/run_benchmarks.py \ + --target ios --tests LazyGrid --modes startup +``` + +Or select a simulator by UUID (recommended) or a unique name. The launcher boots it if necessary: + +```bash +python3 compose/mpp/newbenchmarks/launch-scripts/run_benchmarks.py \ + --target ios --simulator YOUR_SIMULATOR_UUID --tests LazyGrid --modes startup --repeat 3 +``` + +The launcher builds the Release Xcode app, installs it once, then uses `simctl launch --console` +with the four application arguments. Each run terminates any previous instance of this benchmark +app on the selected simulator. After the report or timeout, it calls `simctl terminate`. +Physical iOS devices are not supported by this launcher. + +## Output and logs + +By default, each series creates a new directory: + +```text +out/benchmark-reports/--/ + reports/ # All collected benchmark JSON files, prefixed with the run UUID + logs/ # Build logs, collector log, and a separate log for each run UUID + summary.json # Parameters, statuses, timings, report paths, and errors +``` + +Set `--output-dir /path/to/new-directory` to choose the series directory. It must not already exist. +Use `--port 18090` if port 8090 is occupied. `OUT_DIR` is respected for Gradle artifacts and default output. +The default iOS DerivedData path is `$OUT_DIR/benchmarks-ios-derived` (or `out/benchmarks-ios-derived`). + +A test timeout limits launch/report waiting; process cleanup may take a few extra seconds. +Reports already collected are kept after failures or interruption. Inspect `summary.json` and the +per-test logs to diagnose failures. For application details and case names, see the [main README](../README.md). diff --git a/compose/mpp/newbenchmarks/launch-scripts/run_benchmarks.py b/compose/mpp/newbenchmarks/launch-scripts/run_benchmarks.py new file mode 100755 index 0000000000000..443d70553a9b6 --- /dev/null +++ b/compose/mpp/newbenchmarks/launch-scripts/run_benchmarks.py @@ -0,0 +1,421 @@ +#!/usr/bin/env python3 +# Copyright 2026 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Build and run local benchmarks, collecting one report per fresh application process.""" + +import argparse +from contextlib import contextmanager, ExitStack +from datetime import datetime, timezone +from functools import partial +from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer +import json +import os +from pathlib import Path +import plistlib +import shutil +import signal +import socket +import subprocess +import sys +import tempfile +import threading +import time +import traceback +from urllib.error import HTTPError, URLError +from urllib.parse import urlencode +from urllib.request import Request, urlopen +import uuid + + +REPO = Path(__file__).resolve().parents[4] +MODULES = REPO / "compose/mpp/newbenchmarks" +APP_TASK = ":compose:mpp:newbenchmarks:benchmarks-app:" +COLLECTOR_TASK = ":compose:mpp:newbenchmarks:benchmarks-collector:" +BUNDLE_ID = "org.jetbrains.compose.benchmarks.app.BenchmarksIosApp" +TESTS = ( + "AnimatedVisibility", "LazyGrid", "LazyGrid-ItemLaunchedEffect", "LazyGrid-SmoothScroll", + "LazyGrid-SmoothScroll-ItemLaunchedEffect", "VisualEffects", "LazyList", "MultipleComponents", + "MultipleComponents-NoVectorGraphics", "TextLayout", "CanvasDrawing", "HeavyShader", +) +MODES = ("averageFrame", "fps", "realFrames", "startup") + + +def parse_args(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--target", required=True, choices=("jvm", "macos", "js", "wasmJs", "ios")) + parser.add_argument("--port", type=int, default=8090, help="collector port (default: 8090)") + parser.add_argument("--tests", nargs="+", default=["all"], help="case names, space/comma separated") + parser.add_argument("--modes", nargs="+", default=["all"], help="modes, space/comma separated") + parser.add_argument("--repeat", type=int, default=1) + parser.add_argument("--timeout", type=float, default=30, help="seconds per test (default: 30)") + parser.add_argument("--output-dir", type=Path, help="new directory for this series") + parser.add_argument("--skip-build", action="store_true", help="reuse existing release artifacts") + parser.add_argument("--browser", help="Chromium/Firefox executable, command name, or macOS .app") + parser.add_argument("--browser-kind", choices=("chromium", "firefox"), default="chromium") + parser.add_argument("--browser-arg", action="append", default=[], help="extra browser flag; use --browser-arg=--flag") + parser.add_argument("--web-port", type=int, default=0, help="static/proxy server port (default: automatic)") + parser.add_argument("--simulator", help="iOS Simulator UUID or unique name (default: one booted device)") + args = parser.parse_args() + if os.name != "posix": + parser.error("this launcher requires macOS or Linux") + if args.target in ("ios", "macos") and sys.platform != "darwin": + parser.error("Apple targets require macOS") + if not 1 <= args.port <= 65535 or not 0 <= args.web_port <= 65535: + parser.error("invalid port") + if args.repeat < 1 or not 0 < args.timeout < float("inf"): + parser.error("repeat and timeout must be positive") + if args.target in ("js", "wasmJs") and not args.browser: + parser.error("web targets require --browser") + for name, available in (("tests", TESTS), ("modes", MODES)): + values = [item for value in getattr(args, name) for item in value.split(",")] + if values == ["all"]: + values = list(available) + unknown = set(values) - set(available) + if unknown: + parser.error(f"unknown {name}: {', '.join(sorted(unknown))}") + setattr(args, name, list(dict.fromkeys(values))) + return args + + +def stop_process(process): + # Each child has its own session. Never signal the user's other browser or a Gradle daemon. + try: + os.killpg(process.pid, signal.SIGTERM) + except ProcessLookupError: + process.wait() + return + except PermissionError: + # macOS may leave sandboxed XPC helpers in the group after the browser exits. + if process.poll() is None: + process.terminate() + try: + process.wait(timeout=3) + except subprocess.TimeoutExpired: + pass + try: + os.killpg(process.pid, signal.SIGKILL) + except ProcessLookupError: + pass + except PermissionError: + if process.poll() is None: + process.kill() + process.wait() + + +@contextmanager +def managed_process(command, log_path): + with log_path.open("ab", buffering=0) as log: + log.write(("$ " + repr([str(arg) for arg in command]) + "\n").encode()) + process = subprocess.Popen( + [str(arg) for arg in command], cwd=REPO, stdout=log, + stderr=subprocess.STDOUT, start_new_session=True, + ) + try: + yield process + finally: + stop_process(process) + + +def run_command(command, log_path): + print(f"Running: {' '.join(str(arg) for arg in command)}", flush=True) + with managed_process(command, log_path) as process: + if process.wait() != 0: + raise RuntimeError(f"command failed; see {log_path}") + + +def select_simulator(selection): + data = json.loads(subprocess.check_output( + ["xcrun", "simctl", "list", "devices", "available", "--json"], text=True, + )) + devices = [device for group in data["devices"].values() for device in group if device.get("isAvailable")] + matches = [device for device in devices if + (selection in (device["udid"], device["name"]) if selection else device["state"] == "Booted")] + if len(matches) != 1: + raise RuntimeError("specify --simulator UUID, or boot exactly one simulator before running") + return matches[0] + + +def executable(value): + path = Path(value).expanduser() + if path.suffix == ".app" and path.is_dir(): + with (path / "Contents/Info.plist").open("rb") as file: + path = path / "Contents/MacOS" / plistlib.load(file)["CFBundleExecutable"] + found = str(path.resolve()) if path.is_file() else shutil.which(value) + if not found or not os.access(found, os.X_OK): + raise RuntimeError(f"executable not found: {value}") + return found + + +def java_executable(): + home = os.environ.get("JAVA_HOME") + return executable(str(Path(home) / "bin/java") if home else "java") + + +def build(args, out, logs, simulator): + tasks = [COLLECTOR_TASK + "installDist"] + tasks += { + "jvm": [APP_TASK + "packageReleaseUberJarForCurrentOS"], + "macos": [APP_TASK + "linkReleaseExecutableMacosArm64"], + "js": [APP_TASK + "jsBrowserDistribution"], + "wasmJs": [APP_TASK + "wasmJsBrowserDistribution"], + "ios": [], + }[args.target] + run_command([REPO / "gradlew", *tasks, "--console=plain"], logs / "gradle-build.log") + if args.target == "ios": + run_command([ + "xcodebuild", "-project", MODULES / "benchmarks-ios-app/BenchmarksIosApp.xcodeproj", + "-scheme", "BenchmarksIosApp", "-configuration", "Release", "-sdk", "iphonesimulator", + "-destination", f"id={simulator['udid']}", "-derivedDataPath", out / "benchmarks-ios-derived", + "ARCHS=arm64", "CODE_SIGNING_ALLOWED=NO", f"CMP_OUT_DIR={out}", "build", + ], logs / "xcode-build.log") + + +def artifact(target, app_build, out): + if target == "jvm": + jars = list((app_build / "compose/jars").glob("*-release.jar")) + if not jars: + raise RuntimeError("release JVM jar not found; run without --skip-build") + return max(jars, key=lambda path: path.stat().st_mtime) + path = { + "macos": app_build / "bin/macosArm64/releaseExecutable/benchmarks-app.kexe", + "js": app_build / "dist/js/productionExecutable", + "wasmJs": app_build / "dist/wasmJs/productionExecutable", + "ios": out / "benchmarks-ios-derived/Build/Products/Release-iphonesimulator/BenchmarksIosApp.app", + }[target] + if not path.exists(): + raise RuntimeError(f"release artifact not found: {path}; run without --skip-build") + return path + + +def ensure_free_port(port): + with socket.socket() as probe: + try: + probe.bind(("127.0.0.1", port)) + except OSError as error: + raise RuntimeError(f"collector port {port} is already in use") from error + + +def wait_for_collector(process, port): + deadline = time.monotonic() + 30 + while time.monotonic() < deadline: + if process.poll() is not None: + raise RuntimeError("collector exited; see logs/collector.log") + try: + with socket.create_connection(("127.0.0.1", port), timeout=0.2): + return + except OSError: + time.sleep(0.1) + raise RuntimeError("collector did not start within 30 seconds; see logs/collector.log") + + +@contextmanager +def web_server(directory, port, collector_url, log_path): + # Proxy reports through the page's origin. The collector needs no CORS changes. + with log_path.open("a") as log: + class Handler(SimpleHTTPRequestHandler): + extensions_map = {**SimpleHTTPRequestHandler.extensions_map, ".wasm": "application/wasm", ".mjs": "text/javascript"} + + def log_message(self, template, *values): + log.write((template % values) + "\n") + log.flush() + + def do_POST(self): + if self.path != "/reports": + self.send_error(404) + return + body = self.rfile.read(int(self.headers.get("Content-Length", 0))) + request = Request(collector_url, data=body, headers={"Content-Type": "application/json"}) + try: + response = urlopen(request, timeout=10) + except HTTPError as error: + response = error + except (URLError, TimeoutError): + self.send_error(502, "Collector unavailable") + return + with response: + payload = response.read() + try: + self.send_response(response.code) + self.send_header("Content-Length", str(len(payload))) + self.end_headers() + self.wfile.write(payload) + except (BrokenPipeError, ConnectionResetError): + pass # The launcher may close the browser as soon as the report is saved. + + server = ThreadingHTTPServer(("127.0.0.1", port), partial(Handler, directory=str(directory))) + worker = threading.Thread(target=server.serve_forever, daemon=True) + worker.start() + try: + yield f"http://127.0.0.1:{server.server_port}" + finally: + server.shutdown() + server.server_close() + worker.join() + + +@contextmanager +def launch_test(args, app, browser, simulator, collector_url, web_url, name, mode, run_id, log_path): + parameters = [collector_url, name, mode, run_id] + if args.target == "ios": + try: + command = ["xcrun", "simctl", "launch", "--console", "--terminate-running-process", + simulator["udid"], BUNDLE_ID, *parameters] + with managed_process(command, log_path) as process: + yield lambda: process.poll() is not None + finally: + with log_path.open("ab") as log: + subprocess.run(["xcrun", "simctl", "terminate", simulator["udid"], BUNDLE_ID], + stdout=log, stderr=log, timeout=10) + return + + with ExitStack() as stack: + if args.target in ("js", "wasmJs"): + profile = stack.enter_context(tempfile.TemporaryDirectory(prefix="benchmark-browser-")) + query = urlencode({"benchmarkCollectorUrl": web_url + "/reports", "test": name, "type": mode, "id": run_id}) + url = web_url + "/?" + query + if args.browser_kind == "chromium": + command = [browser, f"--user-data-dir={profile}", "--no-first-run", + "--no-default-browser-check", "--new-window", *args.browser_arg, url] + else: + (Path(profile) / "user.js").write_text( + 'user_pref("browser.shell.checkDefaultBrowser", false);\n' + 'user_pref("browser.startup.homepage_override.mstone", "ignore");\n' + ) + command = [browser, "--no-remote", "--profile", profile, "--new-window", *args.browser_arg, url] + elif args.target == "jvm": + command = [java_executable(), "-jar", app, *parameters] + else: + command = [app, *parameters] + process = stack.enter_context(managed_process(command, log_path)) + yield lambda: process.poll() is not None + + +def wait_for_report(reports, run_id, name, mode, deadline, exited, collector): + while True: + for path in reports.glob(f"{run_id}-*.json"): + report = json.loads(path.read_text()) # Collector publishes complete files atomically. + if report.get("id") != run_id or report.get("name") != name or report.get("type") != mode: + raise RuntimeError(f"unexpected report contents: {path}") + return path + if collector.poll() is not None: + raise RuntimeError("collector exited; see logs/collector.log") + if time.monotonic() >= deadline: + raise TimeoutError("no report before test timeout") + if exited(): + # A headless app may exit immediately after POST. Check files before calling this a failure. + for path in reports.glob(f"{run_id}-*.json"): + report = json.loads(path.read_text()) + if report.get("id") == run_id and report.get("name") == name and report.get("type") == mode: + return path + raise RuntimeError("application exited without a report") + time.sleep(0.1) + + +def run_series(args, out, directory, summary): + logs, reports = directory / "logs", directory / "reports" + logs.mkdir() + reports.mkdir() + app_build = out / "compose-multiplatform-core/compose/mpp/newbenchmarks/benchmarks-app/build" + collector_build = out / "compose-multiplatform-core/compose/mpp/newbenchmarks/benchmarks-collector/build" + simulator = select_simulator(args.simulator) if args.target == "ios" else None + browser = executable(args.browser) if args.target in ("js", "wasmJs") else None + ensure_free_port(args.port) + if not args.skip_build: + build(args, out, logs, simulator) + app = artifact(args.target, app_build, out) + if simulator: + if simulator["state"] != "Booted": + run_command(["xcrun", "simctl", "boot", simulator["udid"]], logs / "simulator.log") + run_command(["xcrun", "simctl", "bootstatus", simulator["udid"], "-b"], logs / "simulator.log") + run_command(["xcrun", "simctl", "install", simulator["udid"], app], logs / "simulator.log") + + collector_url = f"http://127.0.0.1:{args.port}/reports" + with ExitStack() as stack: + ensure_free_port(args.port) + collector = stack.enter_context(managed_process( + [collector_build / "install/benchmarks-collector/bin/benchmarks-collector", str(args.port), reports], + logs / "collector.log", + )) + wait_for_collector(collector, args.port) + web_url = None + if browser: + web_url = stack.enter_context(web_server(app, args.web_port, collector_url, logs / "web-server.log")) + total = len(args.tests) * len(args.modes) * args.repeat + for name in args.tests: + for mode in args.modes: + for iteration in range(1, args.repeat + 1): + if collector.poll() is not None: + raise RuntimeError("collector exited; see logs/collector.log") + run_id = str(uuid.uuid4()) + log_path = logs / f"{run_id}.log" + result = {"id": run_id, "name": name, "type": mode, "iteration": iteration, + "status": "interrupted", "log": str(log_path.relative_to(directory))} + summary["runs"].append(result) + started = time.monotonic() + print(f"[{len(summary['runs'])}/{total}] {name} / {mode} / {run_id}", flush=True) + try: + with launch_test(args, app, browser, simulator, collector_url, web_url, + name, mode, run_id, log_path) as exited: + report = wait_for_report(reports, run_id, name, mode, + started + args.timeout, exited, collector) + result.update(status="passed", report=str(report.relative_to(directory))) + except Exception as error: + result.update(status="failed", error=str(error)) + with log_path.open("a") as log: + log.write(f"\nLauncher: {error}\n") + traceback.print_exc(file=log) + finally: + result["elapsedSeconds"] = round(time.monotonic() - started, 3) + print(f" {result['status']}: {result.get('error', result.get('report', ''))}", flush=True) + (directory / "summary.json").write_text(json.dumps(summary, indent=2) + "\n") + + +def handle_termination(signum, frame): + raise KeyboardInterrupt + + +def main(): + args = parse_args() + signal.signal(signal.SIGTERM, handle_termination) + out = Path(os.environ.get("OUT_DIR", REPO / "out")).expanduser() + if not out.is_absolute(): + out = REPO / out + out = out.resolve() + stamp = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ") + directory = (args.output_dir or out / "benchmark-reports" / f"{stamp}-{args.target}-{uuid.uuid4().hex[:8]}").expanduser().resolve() + directory.mkdir(parents=True, exist_ok=False) + summary = {"target": args.target, "tests": args.tests, "modes": args.modes, + "repeat": args.repeat, "timeoutSeconds": args.timeout, "runs": []} + print(f"Results: {directory}", flush=True) + exit_code = 0 + try: + run_series(args, out, directory, summary) + exit_code = int(any(run["status"] != "passed" for run in summary["runs"])) + except KeyboardInterrupt: + summary["error"] = "interrupted" + exit_code = 130 + except Exception as error: + summary["error"] = str(error) + print(f"Error: {error}", file=sys.stderr) + exit_code = 1 + finally: + (directory / "summary.json").write_text(json.dumps(summary, indent=2) + "\n") + passed = sum(run["status"] == "passed" for run in summary["runs"]) + print(f"Finished: {passed}/{len(summary['runs'])} passed. Results: {directory}", flush=True) + return exit_code + + +if __name__ == "__main__": + sys.exit(main()) From 3d105ea2a9fa6a3a781f381ab991dabb08bc6b54 Mon Sep 17 00:00:00 2001 From: Konstantin Date: Tue, 10 Nov 2026 15:29:24 +0100 Subject: [PATCH 18/61] Disable CORS limitations for collector --- .../benchmarks-collector/build.gradle.kts | 1 + .../compose/benchmarks/collector/Main.kt | 9 +++++ .../newbenchmarks/launch-scripts/README.md | 3 +- .../launch-scripts/run_benchmarks.py | 34 +++---------------- 4 files changed, 16 insertions(+), 31 deletions(-) diff --git a/compose/mpp/newbenchmarks/benchmarks-collector/build.gradle.kts b/compose/mpp/newbenchmarks/benchmarks-collector/build.gradle.kts index 98a70c534dc2f..f2549c025dc0e 100644 --- a/compose/mpp/newbenchmarks/benchmarks-collector/build.gradle.kts +++ b/compose/mpp/newbenchmarks/benchmarks-collector/build.gradle.kts @@ -23,6 +23,7 @@ dependencies { implementation(project(":compose:mpp:newbenchmarks:benchmarks-report")) implementation("io.ktor:ktor-server-core:3.6.1") implementation("io.ktor:ktor-server-cio:3.6.1") + implementation("io.ktor:ktor-server-cors:3.6.1") implementation(libs.kotlinSerializationJson) } diff --git a/compose/mpp/newbenchmarks/benchmarks-collector/src/main/kotlin/org/jetbrains/compose/benchmarks/collector/Main.kt b/compose/mpp/newbenchmarks/benchmarks-collector/src/main/kotlin/org/jetbrains/compose/benchmarks/collector/Main.kt index 884ec9ee3a80f..becea60ae4208 100644 --- a/compose/mpp/newbenchmarks/benchmarks-collector/src/main/kotlin/org/jetbrains/compose/benchmarks/collector/Main.kt +++ b/compose/mpp/newbenchmarks/benchmarks-collector/src/main/kotlin/org/jetbrains/compose/benchmarks/collector/Main.kt @@ -16,10 +16,14 @@ package org.jetbrains.compose.benchmarks.collector +import io.ktor.http.HttpHeaders +import io.ktor.http.HttpMethod import io.ktor.http.HttpStatusCode import io.ktor.server.application.Application +import io.ktor.server.application.install import io.ktor.server.cio.CIO import io.ktor.server.engine.embeddedServer +import io.ktor.server.plugins.cors.routing.CORS import io.ktor.server.request.receiveText import io.ktor.server.response.respondText import io.ktor.server.routing.post @@ -60,6 +64,11 @@ fun main(args: Array) { } internal fun Application.collectReports(outputDirectory: Path) { + install(CORS) { + anyHost() + allowMethod(HttpMethod.Post) + allowHeader(HttpHeaders.ContentType) + } routing { post("/reports") { val body = call.receiveText() diff --git a/compose/mpp/newbenchmarks/launch-scripts/README.md b/compose/mpp/newbenchmarks/launch-scripts/README.md index 48ece0bb6ab6d..ac148940b11b2 100644 --- a/compose/mpp/newbenchmarks/launch-scripts/README.md +++ b/compose/mpp/newbenchmarks/launch-scripts/README.md @@ -74,7 +74,8 @@ Use `--browser-arg=--some-flag` to pass extra browser flags. For Chromium, for e `--browser-arg=--js-flags=--expose-gc` enables manual GC. The launcher serves production files on a free loopback port (`--web-port` can override it). -It forwards same-origin `POST /reports` requests to the collector. No CORS setup is needed. +The browser sends reports directly to the collector port. There is no proxy: the collector allows +JSON POST requests from any origin and handles CORS preflight requests. The browser receives all application parameters in the page URL. ## iOS Simulator (ARM64) diff --git a/compose/mpp/newbenchmarks/launch-scripts/run_benchmarks.py b/compose/mpp/newbenchmarks/launch-scripts/run_benchmarks.py index 443d70553a9b6..953d5cf116ae6 100755 --- a/compose/mpp/newbenchmarks/launch-scripts/run_benchmarks.py +++ b/compose/mpp/newbenchmarks/launch-scripts/run_benchmarks.py @@ -33,9 +33,7 @@ import threading import time import traceback -from urllib.error import HTTPError, URLError from urllib.parse import urlencode -from urllib.request import Request, urlopen import uuid @@ -65,7 +63,7 @@ def parse_args(): parser.add_argument("--browser", help="Chromium/Firefox executable, command name, or macOS .app") parser.add_argument("--browser-kind", choices=("chromium", "firefox"), default="chromium") parser.add_argument("--browser-arg", action="append", default=[], help="extra browser flag; use --browser-arg=--flag") - parser.add_argument("--web-port", type=int, default=0, help="static/proxy server port (default: automatic)") + parser.add_argument("--web-port", type=int, default=0, help="static file server port (default: automatic)") parser.add_argument("--simulator", help="iOS Simulator UUID or unique name (default: one booted device)") args = parser.parse_args() if os.name != "posix": @@ -221,8 +219,7 @@ def wait_for_collector(process, port): @contextmanager -def web_server(directory, port, collector_url, log_path): - # Proxy reports through the page's origin. The collector needs no CORS changes. +def web_server(directory, port, log_path): with log_path.open("a") as log: class Handler(SimpleHTTPRequestHandler): extensions_map = {**SimpleHTTPRequestHandler.extensions_map, ".wasm": "application/wasm", ".mjs": "text/javascript"} @@ -231,29 +228,6 @@ def log_message(self, template, *values): log.write((template % values) + "\n") log.flush() - def do_POST(self): - if self.path != "/reports": - self.send_error(404) - return - body = self.rfile.read(int(self.headers.get("Content-Length", 0))) - request = Request(collector_url, data=body, headers={"Content-Type": "application/json"}) - try: - response = urlopen(request, timeout=10) - except HTTPError as error: - response = error - except (URLError, TimeoutError): - self.send_error(502, "Collector unavailable") - return - with response: - payload = response.read() - try: - self.send_response(response.code) - self.send_header("Content-Length", str(len(payload))) - self.end_headers() - self.wfile.write(payload) - except (BrokenPipeError, ConnectionResetError): - pass # The launcher may close the browser as soon as the report is saved. - server = ThreadingHTTPServer(("127.0.0.1", port), partial(Handler, directory=str(directory))) worker = threading.Thread(target=server.serve_forever, daemon=True) worker.start() @@ -283,7 +257,7 @@ def launch_test(args, app, browser, simulator, collector_url, web_url, name, mod with ExitStack() as stack: if args.target in ("js", "wasmJs"): profile = stack.enter_context(tempfile.TemporaryDirectory(prefix="benchmark-browser-")) - query = urlencode({"benchmarkCollectorUrl": web_url + "/reports", "test": name, "type": mode, "id": run_id}) + query = urlencode({"benchmarkCollectorUrl": collector_url, "test": name, "type": mode, "id": run_id}) url = web_url + "/?" + query if args.browser_kind == "chromium": command = [browser, f"--user-data-dir={profile}", "--no-first-run", @@ -351,7 +325,7 @@ def run_series(args, out, directory, summary): wait_for_collector(collector, args.port) web_url = None if browser: - web_url = stack.enter_context(web_server(app, args.web_port, collector_url, logs / "web-server.log")) + web_url = stack.enter_context(web_server(app, args.web_port, logs / "web-server.log")) total = len(args.tests) * len(args.modes) * args.repeat for name in args.tests: for mode in args.modes: From 8d6dded70cc3edd3670e6ec7b32f8dc4439eab09 Mon Sep 17 00:00:00 2001 From: Konstantin Date: Tue, 10 Nov 2026 15:37:16 +0100 Subject: [PATCH 19/61] Improve browser selection --- .../newbenchmarks/launch-scripts/README.md | 28 ++++-- .../launch-scripts/run_benchmarks.py | 97 ++++++++++++++++++- 2 files changed, 116 insertions(+), 9 deletions(-) diff --git a/compose/mpp/newbenchmarks/launch-scripts/README.md b/compose/mpp/newbenchmarks/launch-scripts/README.md index ac148940b11b2..f6e657ca516a3 100644 --- a/compose/mpp/newbenchmarks/launch-scripts/README.md +++ b/compose/mpp/newbenchmarks/launch-scripts/README.md @@ -50,9 +50,10 @@ python3 compose/mpp/newbenchmarks/launch-scripts/run_benchmarks.py \ ## Browser (JS or WasmJS) Pass a browser executable, a command on `PATH`, or a macOS `.app` path. -Set `--browser-kind firefox` for Firefox; the default is `chromium`. -Each test gets a temporary browser profile and a separate instance. It is closed after a report, -crash, or timeout. Browser launches are visible by default. +The family is detected from the executable name: Chromium-based browsers, Firefox, or Safari. +There is no `--browser-kind` option. Unknown names are rejected rather than treated as Chromium. +Chromium and Firefox get a temporary profile and a separate instance per test. Each instance is +closed after a report, crash, or timeout. Browser launches are visible by default. Chromium: @@ -66,12 +67,27 @@ Firefox: ```bash python3 compose/mpp/newbenchmarks/launch-scripts/run_benchmarks.py \ - --target js --browser "/Applications/Firefox.app" --browser-kind firefox \ + --target js --browser "/Applications/Firefox.app" \ --tests LazyGrid --modes averageFrame ``` -Use `--browser-arg=--some-flag` to pass extra browser flags. For Chromium, for example: -`--browser-arg=--js-flags=--expose-gc` enables manual GC. +Safari (macOS only): + +```bash +python3 compose/mpp/newbenchmarks/launch-scripts/run_benchmarks.py \ + --target js --browser "/Applications/Safari.app" \ + --tests LazyGrid --modes startup --repeat 3 +``` + +Close Safari before starting the series. The launcher refuses to use an existing Safari process. +Each test starts Safari with `open -W` and quits it after the report or timeout. Quitting uses AppleScript; +macOS may ask for Automation permission. If quitting fails, the launched Safari process is terminated. +Safari Technology Preview is also recognized by its executable name. +Safari uses its normal profile, not a temporary profile. Its cache and history are not reset between runs. +Finish Safari's first-launch setup before running benchmarks, and do not open Safari manually during a series. + +Use `--browser-arg=--some-flag` to pass extra Chromium/Firefox flags. For Chromium, for example: +`--browser-arg=--js-flags=--expose-gc` enables manual GC. Extra browser flags are not supported for Safari. The launcher serves production files on a free loopback port (`--web-port` can override it). The browser sends reports directly to the collector port. There is no proxy: the collector allows diff --git a/compose/mpp/newbenchmarks/launch-scripts/run_benchmarks.py b/compose/mpp/newbenchmarks/launch-scripts/run_benchmarks.py index 953d5cf116ae6..7626e2fafaf0f 100755 --- a/compose/mpp/newbenchmarks/launch-scripts/run_benchmarks.py +++ b/compose/mpp/newbenchmarks/launch-scripts/run_benchmarks.py @@ -60,8 +60,7 @@ def parse_args(): parser.add_argument("--timeout", type=float, default=30, help="seconds per test (default: 30)") parser.add_argument("--output-dir", type=Path, help="new directory for this series") parser.add_argument("--skip-build", action="store_true", help="reuse existing release artifacts") - parser.add_argument("--browser", help="Chromium/Firefox executable, command name, or macOS .app") - parser.add_argument("--browser-kind", choices=("chromium", "firefox"), default="chromium") + parser.add_argument("--browser", help="Chromium/Firefox/Safari executable, command name, or macOS .app") parser.add_argument("--browser-arg", action="append", default=[], help="extra browser flag; use --browser-arg=--flag") parser.add_argument("--web-port", type=int, default=0, help="static file server port (default: automatic)") parser.add_argument("--simulator", help="iOS Simulator UUID or unique name (default: one booted device)") @@ -156,6 +155,81 @@ def executable(value): return found +def detect_browser(browser): + name = Path(browser).name.casefold() + if name in ("safari", "safari technology preview"): + return "safari" + if "firefox" in name: + return "firefox" + if any(part in name for part in ("chrome", "chromium", "brave", "edge", "vivaldi")) or name == "arc": + return "chromium" + raise RuntimeError(f"cannot determine browser family from executable name: {browser}") + + +def safari_pids(browser): + processes = subprocess.check_output(["ps", "-ww", "-axo", "pid=,comm="], text=True) + executable_path = Path(browser).resolve() + matches = [] + for line in processes.splitlines(): + fields = line.strip().split(None, 1) + if len(fields) == 2 and Path(fields[1]).resolve() == executable_path: + matches.append(int(fields[0])) + return matches + + +def require_safari_closed(browser): + if safari_pids(browser): + raise RuntimeError("close Safari before starting the series; existing Safari sessions will not be closed") + + +def safari_bundle(browser): + for parent in Path(browser).parents: + if parent.suffix == ".app": + return parent + raise RuntimeError("Safari executable must be inside its .app bundle") + + +def quit_safari(browser, log_path): + pids = safari_pids(browser) + if not pids: + return + script = 'on run argv\n tell application (item 1 of argv) to quit\nend run' + with log_path.open("ab") as log: + try: + subprocess.run(["osascript", "-e", script, str(safari_bundle(browser))], + stdout=log, stderr=log, timeout=5) + except subprocess.TimeoutExpired: + log.write(b"Safari quit timed out; terminating the launched process.\n") + deadline = time.monotonic() + 3 + while time.monotonic() < deadline: + if not set(pids).intersection(safari_pids(browser)): + return + time.sleep(0.1) + # AppleScript may be denied by macOS Automation permissions. Only signal the owned Safari PIDs. + remaining = set(pids).intersection(safari_pids(browser)) + for pid in remaining: + try: + os.kill(pid, signal.SIGKILL) + except ProcessLookupError: + pass + deadline = time.monotonic() + 3 + while time.monotonic() < deadline: + if not remaining.intersection(safari_pids(browser)): + return + time.sleep(0.1) + raise RuntimeError("Safari did not stop; refusing to start the next test") + + +@contextmanager +def launch_safari(browser, url, log_path): + require_safari_closed(browser) + with managed_process(["open", "-W", "-a", safari_bundle(browser), url], log_path) as process: + try: + yield lambda: process.poll() is not None + finally: + quit_safari(browser, log_path) + + def java_executable(): home = os.environ.get("JAVA_HOME") return executable(str(Path(home) / "bin/java") if home else "java") @@ -256,9 +330,13 @@ def launch_test(args, app, browser, simulator, collector_url, web_url, name, mod with ExitStack() as stack: if args.target in ("js", "wasmJs"): - profile = stack.enter_context(tempfile.TemporaryDirectory(prefix="benchmark-browser-")) query = urlencode({"benchmarkCollectorUrl": collector_url, "test": name, "type": mode, "id": run_id}) url = web_url + "/?" + query + if args.browser_kind == "safari": + with launch_safari(browser, url, log_path) as exited: + yield exited + return + profile = stack.enter_context(tempfile.TemporaryDirectory(prefix="benchmark-browser-")) if args.browser_kind == "chromium": command = [browser, f"--user-data-dir={profile}", "--no-first-run", "--no-default-browser-check", "--new-window", *args.browser_arg, url] @@ -305,6 +383,17 @@ def run_series(args, out, directory, summary): collector_build = out / "compose-multiplatform-core/compose/mpp/newbenchmarks/benchmarks-collector/build" simulator = select_simulator(args.simulator) if args.target == "ios" else None browser = executable(args.browser) if args.target in ("js", "wasmJs") else None + if browser: + args.browser_kind = detect_browser(browser) + summary["browser"] = {"kind": args.browser_kind, "executable": browser} + print(f"Browser: {args.browser_kind} ({browser})", flush=True) + if args.browser_kind == "safari": + if sys.platform != "darwin": + raise RuntimeError("Safari requires macOS") + if args.browser_arg: + raise RuntimeError("--browser-arg is not supported for Safari") + safari_bundle(browser) + require_safari_closed(browser) ensure_free_port(args.port) if not args.skip_build: build(args, out, logs, simulator) @@ -332,6 +421,8 @@ def run_series(args, out, directory, summary): for iteration in range(1, args.repeat + 1): if collector.poll() is not None: raise RuntimeError("collector exited; see logs/collector.log") + if browser and args.browser_kind == "safari": + require_safari_closed(browser) run_id = str(uuid.uuid4()) log_path = logs / f"{run_id}.log" result = {"id": run_id, "name": name, "type": mode, "iteration": iteration, From 60c6be531f4444f3097b28ac765511cfb79097aa Mon Sep 17 00:00:00 2001 From: Konstantin Date: Wed, 11 Nov 2026 10:43:48 +0100 Subject: [PATCH 20/61] Delete original benchmarks --- compose/mpp/benchmarks/build.gradle.kts | 207 ------ compose/mpp/benchmarks/gradle.properties | 1 - .../gradle/wasm-d8-runtime.gradle.kts | 72 -- .../iosApp/Configuration/Config.xcconfig | 6 - .../iosApp/iosApp.xcodeproj/project.pbxproj | 382 ---------- .../AccentColor.colorset/Contents.json | 11 - .../AppIcon.appiconset/Contents.json | 98 --- .../iosApp/Assets.xcassets/Contents.json | 6 - .../iosApp/iosApp/ContentView.swift | 25 - .../mpp/benchmarks/iosApp/iosApp/Info.plist | 50 -- .../Preview Assets.xcassets/Contents.json | 6 - .../mpp/benchmarks/iosApp/iosApp/iOSApp.swift | 37 - .../appleMain/kotlin/BenchmarksSave.apple.kt | 6 - .../appleMain/kotlin/GraphicsContext.apple.kt | 65 -- .../kotlin/getProcessStartTime.apple.kt | 38 - .../src/appleMain/kotlin/runGC.native.kt | 7 - .../drawable/compose-multiplatform.png | Bin 34968 -> 0 bytes .../drawable/example1_cat.jpg | Bin 37524 -> 0 bytes .../drawable/example1_ic_call_answer.xml | 36 - .../drawable/example1_sailing.svg | 1 - .../example1_compose-community-primary.png | Bin 17662 -> 0 bytes .../font/jetbrainsmono_italic.ttf | Bin 141572 -> 0 bytes .../font/jetbrainsmono_regular.ttf | Bin 136708 -> 0 bytes .../kotlin/BenchmarkComposeScene.kt | 36 - .../src/commonMain/kotlin/Benchmarks.kt | 597 ---------------- .../src/commonMain/kotlin/BenchmarksSave.kt | 92 --- .../src/commonMain/kotlin/Config.kt | 236 ------- .../commonMain/kotlin/MeasureComposable.kt | 174 ----- .../animation/AnimatedVisibility.kt | 47 -- .../benchmarks/canvasdrawing/CanvasDrawing.kt | 159 ----- .../complexlazylist/components/MainUI.kt | 110 --- .../components/MultiLevelCell.kt | 351 ---------- .../components/refresh/LoadingIndicator.kt | 159 ----- .../components/refresh/SwipeProgress.kt | 17 - .../components/refresh/SwipeRefreshLayout.kt | 121 ---- .../SwipeRefreshNestedScrollConnection.kt | 134 ---- .../components/refresh/SwipeRefreshState.kt | 79 --- .../complexlazylist/models/FollowItem.kt | 16 - .../complexlazylist/models/HotItem.kt | 16 - .../complexlazylist/models/IBaseViewModel.kt | 10 - .../models/ICompositionModel.kt | 21 - .../models/ViewModelFactory.kt | 56 -- .../benchmarks/heavyshader/HeavyShader.kt | 135 ---- .../kotlin/benchmarks/lazygrid/LazyGrid.kt | 97 --- .../multipleComponents/Clickable.kt | 362 ---------- .../benchmarks/multipleComponents/MenuIcon.kt | 49 -- .../multipleComponents/MultipleComponents.kt | 662 ------------------ .../benchmarks/textlayout/TextLayout.kt | 117 ---- .../benchmarks/visualeffects/HappyNY.kt | 321 --------- .../kotlin/BenchmarksSave.desktop.kt | 8 - .../kotlin/BenchmarksSaveServer.kt | 92 --- .../kotlin/GraphicsContext.desktop.kt | 43 -- .../src/desktopMain/kotlin/main.desktop.kt | 51 -- .../src/desktopMain/kotlin/runGC.jvm.kt | 3 - .../src/iosMain/kotlin/Platform.ios.kt | 12 - .../benchmarks/src/iosMain/kotlin/main.ios.kt | 35 - .../benchmarks/src/jsMain/kotlin/Main.js.kt | 4 - .../src/macosMain/kotlin/main.macos.kt | 39 -- .../src/wasmJsMain/kotlin/Main.wasmJs.kt | 64 -- .../src/webMain/kotlin/BenchmarksSave.web.kt | 116 --- .../benchmarks/src/webMain/kotlin/Main.web.kt | 67 -- .../src/webMain/kotlin/runGC.web.kt | 3 - .../src/webMain/resources/index.html | 22 - .../src/webMain/resources/launcher.mjs | 5 - .../webMain/resources/launcher_jetstream3.mjs | 16 - .../src/webMain/resources/polyfills.mjs | 83 --- .../src/webMain/resources/styles.css | 28 - 67 files changed, 5919 deletions(-) delete mode 100644 compose/mpp/benchmarks/build.gradle.kts delete mode 100644 compose/mpp/benchmarks/gradle.properties delete mode 100644 compose/mpp/benchmarks/gradle/wasm-d8-runtime.gradle.kts delete mode 100644 compose/mpp/benchmarks/iosApp/Configuration/Config.xcconfig delete mode 100644 compose/mpp/benchmarks/iosApp/iosApp.xcodeproj/project.pbxproj delete mode 100644 compose/mpp/benchmarks/iosApp/iosApp/Assets.xcassets/AccentColor.colorset/Contents.json delete mode 100644 compose/mpp/benchmarks/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/Contents.json delete mode 100644 compose/mpp/benchmarks/iosApp/iosApp/Assets.xcassets/Contents.json delete mode 100644 compose/mpp/benchmarks/iosApp/iosApp/ContentView.swift delete mode 100644 compose/mpp/benchmarks/iosApp/iosApp/Info.plist delete mode 100644 compose/mpp/benchmarks/iosApp/iosApp/Preview Content/Preview Assets.xcassets/Contents.json delete mode 100644 compose/mpp/benchmarks/iosApp/iosApp/iOSApp.swift delete mode 100644 compose/mpp/benchmarks/src/appleMain/kotlin/BenchmarksSave.apple.kt delete mode 100644 compose/mpp/benchmarks/src/appleMain/kotlin/GraphicsContext.apple.kt delete mode 100644 compose/mpp/benchmarks/src/appleMain/kotlin/getProcessStartTime.apple.kt delete mode 100644 compose/mpp/benchmarks/src/appleMain/kotlin/runGC.native.kt delete mode 100644 compose/mpp/benchmarks/src/commonMain/composeResources/drawable/compose-multiplatform.png delete mode 100644 compose/mpp/benchmarks/src/commonMain/composeResources/drawable/example1_cat.jpg delete mode 100644 compose/mpp/benchmarks/src/commonMain/composeResources/drawable/example1_ic_call_answer.xml delete mode 100644 compose/mpp/benchmarks/src/commonMain/composeResources/drawable/example1_sailing.svg delete mode 100644 compose/mpp/benchmarks/src/commonMain/composeResources/files/example1_compose-community-primary.png delete mode 100644 compose/mpp/benchmarks/src/commonMain/composeResources/font/jetbrainsmono_italic.ttf delete mode 100644 compose/mpp/benchmarks/src/commonMain/composeResources/font/jetbrainsmono_regular.ttf delete mode 100644 compose/mpp/benchmarks/src/commonMain/kotlin/BenchmarkComposeScene.kt delete mode 100644 compose/mpp/benchmarks/src/commonMain/kotlin/Benchmarks.kt delete mode 100644 compose/mpp/benchmarks/src/commonMain/kotlin/BenchmarksSave.kt delete mode 100644 compose/mpp/benchmarks/src/commonMain/kotlin/Config.kt delete mode 100644 compose/mpp/benchmarks/src/commonMain/kotlin/MeasureComposable.kt delete mode 100644 compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/animation/AnimatedVisibility.kt delete mode 100644 compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/canvasdrawing/CanvasDrawing.kt delete mode 100644 compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/complexlazylist/components/MainUI.kt delete mode 100644 compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/complexlazylist/components/MultiLevelCell.kt delete mode 100644 compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/complexlazylist/components/refresh/LoadingIndicator.kt delete mode 100644 compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/complexlazylist/components/refresh/SwipeProgress.kt delete mode 100644 compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/complexlazylist/components/refresh/SwipeRefreshLayout.kt delete mode 100644 compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/complexlazylist/components/refresh/SwipeRefreshNestedScrollConnection.kt delete mode 100644 compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/complexlazylist/components/refresh/SwipeRefreshState.kt delete mode 100644 compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/complexlazylist/models/FollowItem.kt delete mode 100644 compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/complexlazylist/models/HotItem.kt delete mode 100644 compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/complexlazylist/models/IBaseViewModel.kt delete mode 100644 compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/complexlazylist/models/ICompositionModel.kt delete mode 100644 compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/complexlazylist/models/ViewModelFactory.kt delete mode 100644 compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/heavyshader/HeavyShader.kt delete mode 100644 compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/lazygrid/LazyGrid.kt delete mode 100644 compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/multipleComponents/Clickable.kt delete mode 100644 compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/multipleComponents/MenuIcon.kt delete mode 100644 compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/multipleComponents/MultipleComponents.kt delete mode 100644 compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/textlayout/TextLayout.kt delete mode 100644 compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/visualeffects/HappyNY.kt delete mode 100644 compose/mpp/benchmarks/src/desktopMain/kotlin/BenchmarksSave.desktop.kt delete mode 100644 compose/mpp/benchmarks/src/desktopMain/kotlin/BenchmarksSaveServer.kt delete mode 100644 compose/mpp/benchmarks/src/desktopMain/kotlin/GraphicsContext.desktop.kt delete mode 100644 compose/mpp/benchmarks/src/desktopMain/kotlin/main.desktop.kt delete mode 100644 compose/mpp/benchmarks/src/desktopMain/kotlin/runGC.jvm.kt delete mode 100644 compose/mpp/benchmarks/src/iosMain/kotlin/Platform.ios.kt delete mode 100644 compose/mpp/benchmarks/src/iosMain/kotlin/main.ios.kt delete mode 100644 compose/mpp/benchmarks/src/jsMain/kotlin/Main.js.kt delete mode 100644 compose/mpp/benchmarks/src/macosMain/kotlin/main.macos.kt delete mode 100644 compose/mpp/benchmarks/src/wasmJsMain/kotlin/Main.wasmJs.kt delete mode 100644 compose/mpp/benchmarks/src/webMain/kotlin/BenchmarksSave.web.kt delete mode 100644 compose/mpp/benchmarks/src/webMain/kotlin/Main.web.kt delete mode 100644 compose/mpp/benchmarks/src/webMain/kotlin/runGC.web.kt delete mode 100644 compose/mpp/benchmarks/src/webMain/resources/index.html delete mode 100644 compose/mpp/benchmarks/src/webMain/resources/launcher.mjs delete mode 100644 compose/mpp/benchmarks/src/webMain/resources/launcher_jetstream3.mjs delete mode 100644 compose/mpp/benchmarks/src/webMain/resources/polyfills.mjs delete mode 100644 compose/mpp/benchmarks/src/webMain/resources/styles.css diff --git a/compose/mpp/benchmarks/build.gradle.kts b/compose/mpp/benchmarks/build.gradle.kts deleted file mode 100644 index 788a123f87d91..0000000000000 --- a/compose/mpp/benchmarks/build.gradle.kts +++ /dev/null @@ -1,207 +0,0 @@ -/* - * Copyright 2026 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -@file:OptIn(ExperimentalWasmDsl::class) - -import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl -import org.jetbrains.kotlin.gradle.targets.js.webpack.KotlinWebpack -import org.jetbrains.kotlin.gradle.targets.js.webpack.KotlinWebpackConfig -import org.jetbrains.kotlin.gradle.targets.wasm.binaryen.BinaryenExec -import org.jetbrains.kotlin.gradle.targets.wasm.d8.D8Exec - -plugins { - id("org.jetbrains.compose") version "1.13.0-beta02" - id("org.jetbrains.kotlin.plugin.compose") version libs.versions.kotlin.get() - id("kotlin-multiplatform") - alias(libs.plugins.kotlinSerialization) -} - -version = "1.0-SNAPSHOT" - -kotlin { - jvm("desktop") - - listOf(iosArm64(), iosSimulatorArm64()).forEach { target -> - target.binaries.framework { - baseName = "benchmarks" - isStatic = true - } - } - - macosArm64 { - binaries.executable { - entryPoint = "main" - } - } - - wasmJs { - outputModuleName = "mpp-benchmarks" - browser { - commonWebpackConfig { - outputFileName = "benchmarks.js" - } - } - d8 { - compilerOptions.freeCompilerArgs.add("-Xwasm-attach-js-exception") - } - binaries.executable() - binaries.configureEach { - compilation.compileTaskProvider.configure { - compilerOptions.freeCompilerArgs.add("-Xwasm-use-new-exception-proposal") - } - } - } - - js { - outputModuleName = "mpp-benchmarks" - browser { - commonWebpackConfig { - outputFileName = "benchmarks.js" - } - } - binaries.executable() - } - - applyDefaultHierarchyTemplate() - - sourceSets { - commonMain.dependencies { - implementation(project(":compose:runtime:runtime")) - implementation(project(":compose:ui:ui")) - implementation(project(":compose:foundation:foundation")) - implementation(project(":compose:material:material")) - implementation("org.jetbrains.compose.components:components-resources:1.13.0-beta02") { - exclude(group = "org.jetbrains.compose.runtime") - exclude(group = "org.jetbrains.compose.ui") - } - - implementation(libs.skiko) - implementation(libs.kotlinCoroutinesCore) - implementation(libs.kotlinSerializationJson) - implementation("org.jetbrains.kotlinx:kotlinx-io-core:0.9.2") - implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.8.0") - } - - webMain.dependencies { - implementation(libs.kotlinXw3c) - implementation("io.ktor:ktor-client-core:3.6.1") - implementation("io.ktor:ktor-client-js:3.6.1") - implementation("io.ktor:ktor-client-content-negotiation:3.6.1") - implementation("io.ktor:ktor-serialization-kotlinx-json:3.6.1") - } - - val desktopMain by getting { - dependencies { - implementation(libs.kotlinCoroutinesSwing) - implementation(libs.skikoAwtRuntime) - implementation("io.ktor:ktor-server-core:3.6.1") - implementation("io.ktor:ktor-server-netty:3.6.1") - implementation("io.ktor:ktor-server-content-negotiation:3.6.1") - implementation("io.ktor:ktor-serialization-kotlinx-json:3.6.1") - implementation("io.ktor:ktor-client-core:3.6.1") - implementation("io.ktor:ktor-client-java:3.6.1") - implementation("io.ktor:ktor-client-content-negotiation:3.6.1") - implementation("io.ktor:ktor-server-cors:3.6.1") - } - } - } -} - -compose.resources { - packageOfResClass = "compose_benchmarks.benchmarks.generated.resources" -} - -val runArguments = providers.gradleProperty("runArguments").orElse("") -val versionInfo = "versionInfo=\"local (Kotlin ${libs.versions.kotlin.get()})\"" -val benchmarkArguments = runArguments.map { arguments -> - (arguments.split(" ").filter { it.isNotEmpty() } + versionInfo) - .map { it.replace(" ", "%20") } -} - -compose.desktop { - application { - mainClass = "Main_desktopKt" - args.addAll(benchmarkArguments.get()) - } -} - -apply(from = "gradle/wasm-d8-runtime.gradle.kts") - -tasks.withType().matching { it.name.startsWith("runReleaseExecutableMacos") }.configureEach { - args(benchmarkArguments.get()) -} - -tasks.named("wasmJsBrowserProductionRun") { - val query = benchmarkArguments.get() - .mapIndexed { index, arg -> "arg$index=$arg" } - .joinToString("&") - devServerProperty = (devServerProperty.orNull ?: KotlinWebpackConfig.DevServer()).copy( - open = "http://localhost:8080?$query" - ) -} - -tasks.named("wasmJsD8DevelopmentRun") { - dependsOn("prepareWasmJsD8DevelopmentRuntime") - inputFileProperty.set(layout.buildDirectory.file("d8/development/launcher.mjs")) -} - -tasks.named("wasmJsD8ProductionRun") { - dependsOn("prepareWasmJsD8ProductionRuntime") - inputFileProperty.set(layout.buildDirectory.file("d8/production/launcher.mjs")) -} - -tasks.withType().configureEach { - args(benchmarkArguments.get()) -} - -tasks.withType().configureEach { - binaryenArguments.add("-g") -} - -val benchmarkRepositoryDir = rootProject.layout.projectDirectory.asFile -val benchmarkGradlew = rootProject.layout.projectDirectory.file("gradlew").asFile - -tasks.register("runBrowserAndSaveStats") { - group = "application" - description = "Runs the benchmark results server and the Wasm browser benchmarks." - val repositoryDir = benchmarkRepositoryDir - val gradlew = benchmarkGradlew - val arguments = runArguments - doLast { - var serverProcess: Process? = null - var clientProcess: Process? = null - try { - serverProcess = ProcessBuilder( - gradlew.absolutePath, - ":compose:mpp:benchmarks:run", - "-PrunArguments=runServer=true saveStatsToJSON=true", - "--console=plain" - ).directory(repositoryDir).inheritIO().start() - - clientProcess = ProcessBuilder( - gradlew.absolutePath, - ":compose:mpp:benchmarks:wasmJsBrowserProductionRun", - "-PrunArguments=" + arguments.get() + " saveStatsToJSON=true", - "--console=plain" - ).directory(repositoryDir).inheritIO().start() - - serverProcess.waitFor() - } finally { - serverProcess?.destroy() - clientProcess?.destroy() - } - } -} diff --git a/compose/mpp/benchmarks/gradle.properties b/compose/mpp/benchmarks/gradle.properties deleted file mode 100644 index d15154e785f1e..0000000000000 --- a/compose/mpp/benchmarks/gradle.properties +++ /dev/null @@ -1 +0,0 @@ -org.jetbrains.compose.experimental.macos.enabled=true diff --git a/compose/mpp/benchmarks/gradle/wasm-d8-runtime.gradle.kts b/compose/mpp/benchmarks/gradle/wasm-d8-runtime.gradle.kts deleted file mode 100644 index 846161ad18069..0000000000000 --- a/compose/mpp/benchmarks/gradle/wasm-d8-runtime.gradle.kts +++ /dev/null @@ -1,72 +0,0 @@ -import org.gradle.api.tasks.Sync -import java.io.File -import kotlin.text.replace - -val jsJodaEsmFile = rootProject.layout.buildDirectory.file( - "wasm/node_modules/@js-joda/core/dist/js-joda.esm.js" -) -val wasmResourcesDirectory = project.layout.buildDirectory.dir("processedResources/wasmJs/main") -val skikoRuntime = tasks.named("unpackSkikoRuntimeForWasmJs") - -object D8RuntimePatcher { - private fun File.replaceRequired(oldValue: String, newValue: String) { - check(isFile) { "Expected D8 runtime file does not exist: $this" } - - val original = readText() - check(oldValue in original) { - "Cannot prepare the D8 runtime: expected content was not found in $this" - } - writeText(original.replace(oldValue, newValue)) - } - - fun patch(directory: File) { - directory.resolve("mpp-benchmarks.import-object.mjs").apply { - replaceRequired("from './skiko.mjs'", "from './skikod8.mjs'") - replaceRequired("from '@js-joda/core'", "from './js-joda.esm.js'") - } - - directory.resolve("skikod8.mjs").replaceRequired( - oldValue = "loadSkikoWASM().then(", - newValue = """ - loadSkikoWASM({ - locateFile: (path) => String(import.meta.url) - .replace(/^file:\/\//, '') - .replace(/\/[^/]+${'$'}/, '/' + path) - }).then( - """.trimIndent() - ) - } -} - -fun registerD8Runtime(variant: String, compiledPath: String, outputPath: String) = - tasks.register("prepareWasmJsD8${variant}Runtime") { - dependsOn("wasmJs${variant}ExecutableCompileSync", ":kotlinWasmNpmInstall") - from(project.layout.buildDirectory.dir(compiledPath)) - from(wasmResourcesDirectory) - from(skikoRuntime) - from(jsJodaEsmFile) - into(project.layout.buildDirectory.dir(outputPath)) - doLast { - D8RuntimePatcher.patch(destinationDir) - } - } - -val d8DevelopmentDirectory = project.layout.buildDirectory.dir("d8/development") -val d8ProductionDirectory = project.layout.buildDirectory.dir("d8/production") -val prepareWasmJsD8DevelopmentRuntime = registerD8Runtime( - variant = "Development", - compiledPath = "compileSync/wasmJs/main/developmentExecutable/kotlin", - outputPath = "d8/development" -) -val prepareWasmJsD8ProductionRuntime = registerD8Runtime( - variant = "Production", - compiledPath = "compileSync/wasmJs/main/productionExecutable/optimized", - outputPath = "d8/production" -) - -tasks.register("buildD8Distribution", org.gradle.api.tasks.bundling.Zip::class.java) { - dependsOn(prepareWasmJsD8ProductionRuntime) - from(d8ProductionDirectory) - archiveFileName.set("d8-distribution.zip") - destinationDirectory.set(project.layout.buildDirectory.dir("distributions")) -} diff --git a/compose/mpp/benchmarks/iosApp/Configuration/Config.xcconfig b/compose/mpp/benchmarks/iosApp/Configuration/Config.xcconfig deleted file mode 100644 index 0efa34475184f..0000000000000 --- a/compose/mpp/benchmarks/iosApp/Configuration/Config.xcconfig +++ /dev/null @@ -1,6 +0,0 @@ -TEAM_ID= -BUNDLE_ID=org.jetbrains.benchmarks -APP_NAME=ComposeBenchmarks - -// Override CMP_OUT_DIR when the Gradle build uses a custom OUT_DIR. -CMP_OUT_DIR=$(SRCROOT)/../../../../out diff --git a/compose/mpp/benchmarks/iosApp/iosApp.xcodeproj/project.pbxproj b/compose/mpp/benchmarks/iosApp/iosApp.xcodeproj/project.pbxproj deleted file mode 100644 index 073ef13b7fab2..0000000000000 --- a/compose/mpp/benchmarks/iosApp/iosApp.xcodeproj/project.pbxproj +++ /dev/null @@ -1,382 +0,0 @@ -// !$*UTF8*$! -{ - archiveVersion = 1; - classes = { - }; - objectVersion = 50; - objects = { - -/* Begin PBXBuildFile section */ - 058557BB273AAA24004C7B11 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 058557BA273AAA24004C7B11 /* Assets.xcassets */; }; - 058557D9273AAEEB004C7B11 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 058557D8273AAEEB004C7B11 /* Preview Assets.xcassets */; }; - 2152FB042600AC8F00CF470E /* iOSApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2152FB032600AC8F00CF470E /* iOSApp.swift */; }; - 7555FF83242A565900829871 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7555FF82242A565900829871 /* ContentView.swift */; }; -/* End PBXBuildFile section */ - -/* Begin PBXFileReference section */ - 058557BA273AAA24004C7B11 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; - 058557D8273AAEEB004C7B11 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; - 2152FB032600AC8F00CF470E /* iOSApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = iOSApp.swift; sourceTree = ""; }; - 7555FF7B242A565900829871 /* ComposeBenchmarks.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ComposeBenchmarks.app; sourceTree = BUILT_PRODUCTS_DIR; }; - 7555FF82242A565900829871 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; - 7555FF8C242A565B00829871 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - AB3632DC29227652001CCB65 /* Config.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Config.xcconfig; sourceTree = ""; }; -/* End PBXFileReference section */ - -/* Begin PBXFrameworksBuildPhase section */ - F85CB1118929364A9C6EFABC /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXFrameworksBuildPhase section */ - -/* Begin PBXGroup section */ - 058557D7273AAEEB004C7B11 /* Preview Content */ = { - isa = PBXGroup; - children = ( - 058557D8273AAEEB004C7B11 /* Preview Assets.xcassets */, - ); - path = "Preview Content"; - sourceTree = ""; - }; - 7555FF72242A565900829871 = { - isa = PBXGroup; - children = ( - AB1DB47929225F7C00F7AF9C /* Configuration */, - 7555FF7D242A565900829871 /* iosApp */, - 7555FF7C242A565900829871 /* Products */, - ); - sourceTree = ""; - }; - 7555FF7C242A565900829871 /* Products */ = { - isa = PBXGroup; - children = ( - 7555FF7B242A565900829871 /* ComposeBenchmarks.app */, - ); - name = Products; - sourceTree = ""; - }; - 7555FF7D242A565900829871 /* iosApp */ = { - isa = PBXGroup; - children = ( - 058557BA273AAA24004C7B11 /* Assets.xcassets */, - 7555FF82242A565900829871 /* ContentView.swift */, - 7555FF8C242A565B00829871 /* Info.plist */, - 2152FB032600AC8F00CF470E /* iOSApp.swift */, - 058557D7273AAEEB004C7B11 /* Preview Content */, - ); - path = iosApp; - sourceTree = ""; - }; - AB1DB47929225F7C00F7AF9C /* Configuration */ = { - isa = PBXGroup; - children = ( - AB3632DC29227652001CCB65 /* Config.xcconfig */, - ); - path = Configuration; - sourceTree = ""; - }; -/* End PBXGroup section */ - -/* Begin PBXNativeTarget section */ - 7555FF7A242A565900829871 /* iosApp */ = { - isa = PBXNativeTarget; - buildConfigurationList = 7555FFA5242A565B00829871 /* Build configuration list for PBXNativeTarget "iosApp" */; - buildPhases = ( - 05E104082A8AADF800D11C3D /* Compile Kotlin */, - 7555FF77242A565900829871 /* Sources */, - 7555FF79242A565900829871 /* Resources */, - F85CB1118929364A9C6EFABC /* Frameworks */, - ); - buildRules = ( - ); - dependencies = ( - ); - name = iosApp; - productName = iosApp; - productReference = 7555FF7B242A565900829871 /* ComposeBenchmarks.app */; - productType = "com.apple.product-type.application"; - }; -/* End PBXNativeTarget section */ - -/* Begin PBXProject section */ - 7555FF73242A565900829871 /* Project object */ = { - isa = PBXProject; - attributes = { - LastSwiftUpdateCheck = 1130; - LastUpgradeCheck = 1130; - ORGANIZATIONNAME = orgName; - TargetAttributes = { - 7555FF7A242A565900829871 = { - CreatedOnToolsVersion = 11.3.1; - }; - }; - }; - buildConfigurationList = 7555FF76242A565900829871 /* Build configuration list for PBXProject "iosApp" */; - compatibilityVersion = "Xcode 9.3"; - developmentRegion = en; - hasScannedForEncodings = 0; - knownRegions = ( - en, - Base, - ); - mainGroup = 7555FF72242A565900829871; - productRefGroup = 7555FF7C242A565900829871 /* Products */; - projectDirPath = ""; - projectRoot = ""; - targets = ( - 7555FF7A242A565900829871 /* iosApp */, - ); - }; -/* End PBXProject section */ - -/* Begin PBXResourcesBuildPhase section */ - 7555FF79242A565900829871 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 058557D9273AAEEB004C7B11 /* Preview Assets.xcassets in Resources */, - 058557BB273AAA24004C7B11 /* Assets.xcassets in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXResourcesBuildPhase section */ - -/* Begin PBXShellScriptBuildPhase section */ - 05E104082A8AADF800D11C3D /* Compile Kotlin */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - ); - name = "Compile Kotlin"; - outputFileListPaths = ( - ); - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "cd \"$SRCROOT/../../../..\"\n./gradlew :compose:mpp:benchmarks:embedAndSignAppleFrameworkForXcode --console=plain\n"; - }; -/* End PBXShellScriptBuildPhase section */ - -/* Begin PBXSourcesBuildPhase section */ - 7555FF77242A565900829871 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 2152FB042600AC8F00CF470E /* iOSApp.swift in Sources */, - 7555FF83242A565900829871 /* ContentView.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXSourcesBuildPhase section */ - -/* Begin XCBuildConfiguration section */ - 7555FFA3242A565B00829871 /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = AB3632DC29227652001CCB65 /* Config.xcconfig */; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_ENABLE_OBJC_WEAK = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_TESTABILITY = YES; - GCC_C_LANGUAGE_STANDARD = gnu11; - GCC_DYNAMIC_NO_PIC = NO; - GCC_NO_COMMON_BLOCKS = YES; - GCC_OPTIMIZATION_LEVEL = 0; - GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "$(inherited)", - ); - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 14.1; - MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; - MTL_FAST_MATH = YES; - ONLY_ACTIVE_ARCH = YES; - SDKROOT = iphoneos; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - }; - name = Debug; - }; - 7555FFA4242A565B00829871 /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = AB3632DC29227652001CCB65 /* Config.xcconfig */; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_ENABLE_OBJC_WEAK = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - GCC_C_LANGUAGE_STANDARD = gnu11; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 14.1; - MTL_ENABLE_DEBUG_INFO = NO; - MTL_FAST_MATH = YES; - SDKROOT = iphoneos; - SWIFT_COMPILATION_MODE = wholemodule; - SWIFT_OPTIMIZATION_LEVEL = "-O"; - VALIDATE_PRODUCT = YES; - }; - name = Release; - }; - 7555FFA6242A565B00829871 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CODE_SIGN_IDENTITY = "Apple Development"; - CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_ASSET_PATHS = "\"iosApp/Preview Content\""; - DEVELOPMENT_TEAM = "${TEAM_ID}"; - ENABLE_PREVIEWS = YES; - FRAMEWORK_SEARCH_PATHS = "$(CMP_OUT_DIR)/compose-multiplatform-core/compose/mpp/benchmarks/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)"; - INFOPLIST_FILE = iosApp/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 14.1; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - OTHER_LDFLAGS = ( - "$(inherited)", - "-framework", - "benchmarks", - ); - PRODUCT_BUNDLE_IDENTIFIER = "${BUNDLE_ID}${TEAM_ID}"; - PRODUCT_NAME = "${APP_NAME}"; - PROVISIONING_PROFILE_SPECIFIER = ""; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Debug; - }; - 7555FFA7242A565B00829871 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CODE_SIGN_IDENTITY = "Apple Development"; - CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_ASSET_PATHS = "\"iosApp/Preview Content\""; - DEVELOPMENT_TEAM = "${TEAM_ID}"; - ENABLE_PREVIEWS = YES; - FRAMEWORK_SEARCH_PATHS = "$(CMP_OUT_DIR)/compose-multiplatform-core/compose/mpp/benchmarks/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)"; - INFOPLIST_FILE = iosApp/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 14.1; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - OTHER_LDFLAGS = ( - "$(inherited)", - "-framework", - "benchmarks", - ); - PRODUCT_BUNDLE_IDENTIFIER = "${BUNDLE_ID}${TEAM_ID}"; - PRODUCT_NAME = "${APP_NAME}"; - PROVISIONING_PROFILE_SPECIFIER = ""; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Release; - }; -/* End XCBuildConfiguration section */ - -/* Begin XCConfigurationList section */ - 7555FF76242A565900829871 /* Build configuration list for PBXProject "iosApp" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 7555FFA3242A565B00829871 /* Debug */, - 7555FFA4242A565B00829871 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 7555FFA5242A565B00829871 /* Build configuration list for PBXNativeTarget "iosApp" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 7555FFA6242A565B00829871 /* Debug */, - 7555FFA7242A565B00829871 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; -/* End XCConfigurationList section */ - }; - rootObject = 7555FF73242A565900829871 /* Project object */; -} diff --git a/compose/mpp/benchmarks/iosApp/iosApp/Assets.xcassets/AccentColor.colorset/Contents.json b/compose/mpp/benchmarks/iosApp/iosApp/Assets.xcassets/AccentColor.colorset/Contents.json deleted file mode 100644 index ee7e3ca03f870..0000000000000 --- a/compose/mpp/benchmarks/iosApp/iosApp/Assets.xcassets/AccentColor.colorset/Contents.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "colors" : [ - { - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} \ No newline at end of file diff --git a/compose/mpp/benchmarks/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/Contents.json b/compose/mpp/benchmarks/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/Contents.json deleted file mode 100644 index fb88a396bf389..0000000000000 --- a/compose/mpp/benchmarks/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/Contents.json +++ /dev/null @@ -1,98 +0,0 @@ -{ - "images" : [ - { - "idiom" : "iphone", - "scale" : "2x", - "size" : "20x20" - }, - { - "idiom" : "iphone", - "scale" : "3x", - "size" : "20x20" - }, - { - "idiom" : "iphone", - "scale" : "2x", - "size" : "29x29" - }, - { - "idiom" : "iphone", - "scale" : "3x", - "size" : "29x29" - }, - { - "idiom" : "iphone", - "scale" : "2x", - "size" : "40x40" - }, - { - "idiom" : "iphone", - "scale" : "3x", - "size" : "40x40" - }, - { - "idiom" : "iphone", - "scale" : "2x", - "size" : "60x60" - }, - { - "idiom" : "iphone", - "scale" : "3x", - "size" : "60x60" - }, - { - "idiom" : "ipad", - "scale" : "1x", - "size" : "20x20" - }, - { - "idiom" : "ipad", - "scale" : "2x", - "size" : "20x20" - }, - { - "idiom" : "ipad", - "scale" : "1x", - "size" : "29x29" - }, - { - "idiom" : "ipad", - "scale" : "2x", - "size" : "29x29" - }, - { - "idiom" : "ipad", - "scale" : "1x", - "size" : "40x40" - }, - { - "idiom" : "ipad", - "scale" : "2x", - "size" : "40x40" - }, - { - "idiom" : "ipad", - "scale" : "1x", - "size" : "76x76" - }, - { - "idiom" : "ipad", - "scale" : "2x", - "size" : "76x76" - }, - { - "idiom" : "ipad", - "scale" : "2x", - "size" : "83.5x83.5" - }, - { - "idiom" : "ios-marketing", - "scale" : "1x", - "size" : "1024x1024" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} \ No newline at end of file diff --git a/compose/mpp/benchmarks/iosApp/iosApp/Assets.xcassets/Contents.json b/compose/mpp/benchmarks/iosApp/iosApp/Assets.xcassets/Contents.json deleted file mode 100644 index 4aa7c5350bfce..0000000000000 --- a/compose/mpp/benchmarks/iosApp/iosApp/Assets.xcassets/Contents.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - } -} \ No newline at end of file diff --git a/compose/mpp/benchmarks/iosApp/iosApp/ContentView.swift b/compose/mpp/benchmarks/iosApp/iosApp/ContentView.swift deleted file mode 100644 index dad9234931cfd..0000000000000 --- a/compose/mpp/benchmarks/iosApp/iosApp/ContentView.swift +++ /dev/null @@ -1,25 +0,0 @@ -import UIKit -import SwiftUI -import benchmarks - -struct ContentView: View { - var body: some View { - if (Main_iosKt.runReal()) { - ComposeView() - .ignoresSafeArea(.all) // Compose has own keyboard handler - } else { - VStack { - Text("Hello, world!") - } - .padding() - } - } -} - -struct ComposeView: UIViewControllerRepresentable { - func makeUIViewController(context: Context) -> UIViewController { - Main_iosKt.MainViewController() - } - - func updateUIViewController(_ uiViewController: UIViewController, context: Context) {} -} diff --git a/compose/mpp/benchmarks/iosApp/iosApp/Info.plist b/compose/mpp/benchmarks/iosApp/iosApp/Info.plist deleted file mode 100644 index 412e3781280f8..0000000000000 --- a/compose/mpp/benchmarks/iosApp/iosApp/Info.plist +++ /dev/null @@ -1,50 +0,0 @@ - - - - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - $(PRODUCT_BUNDLE_PACKAGE_TYPE) - CFBundleShortVersionString - 1.0 - CFBundleVersion - 1 - LSRequiresIPhoneOS - - CADisableMinimumFrameDurationOnPhone - - UIApplicationSceneManifest - - UIApplicationSupportsMultipleScenes - - - UILaunchScreen - - UIRequiredDeviceCapabilities - - armv7 - - UISupportedInterfaceOrientations - - UIInterfaceOrientationPortrait - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UISupportedInterfaceOrientations~ipad - - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - - diff --git a/compose/mpp/benchmarks/iosApp/iosApp/Preview Content/Preview Assets.xcassets/Contents.json b/compose/mpp/benchmarks/iosApp/iosApp/Preview Content/Preview Assets.xcassets/Contents.json deleted file mode 100644 index 4aa7c5350bfce..0000000000000 --- a/compose/mpp/benchmarks/iosApp/iosApp/Preview Content/Preview Assets.xcassets/Contents.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - } -} \ No newline at end of file diff --git a/compose/mpp/benchmarks/iosApp/iosApp/iOSApp.swift b/compose/mpp/benchmarks/iosApp/iosApp/iOSApp.swift deleted file mode 100644 index 2ea04889350db..0000000000000 --- a/compose/mpp/benchmarks/iosApp/iosApp/iOSApp.swift +++ /dev/null @@ -1,37 +0,0 @@ -import SwiftUI -import benchmarks -import Combine - -class MemoryWarningMonitor: ObservableObject { - private var cancellable: AnyCancellable? - - init() { - self.cancellable = NotificationCenter.default - .publisher(for: UIApplication.didReceiveMemoryWarningNotification) - .sink { _ in - self.didReceiveMemoryWarning() - } - } - - private func didReceiveMemoryWarning() { -// print("Memory warning received! Cleaning resources.") - RunGC_nativeKt.runGC() - } -} - -@main -struct iOSApp: App { - init() { - MemoryWarningMonitor() - Main_iosKt.setGlobalFromArgs(args: ProcessInfo.processInfo.arguments) - if (!Main_iosKt.runReal()) { - Main_iosKt.runBenchmarks() - } - } - - var body: some Scene { - WindowGroup { - ContentView() - } - } -} \ No newline at end of file diff --git a/compose/mpp/benchmarks/src/appleMain/kotlin/BenchmarksSave.apple.kt b/compose/mpp/benchmarks/src/appleMain/kotlin/BenchmarksSave.apple.kt deleted file mode 100644 index 815b87fb501a2..0000000000000 --- a/compose/mpp/benchmarks/src/appleMain/kotlin/BenchmarksSave.apple.kt +++ /dev/null @@ -1,6 +0,0 @@ -/* - * Copyright 2020-2025 JetBrains s.r.o. and respective authors and developers. - * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. - */ - -actual fun saveBenchmarkStats(name: String, stats: BenchmarkStats) = saveBenchmarkStatsOnDisk(name, stats) diff --git a/compose/mpp/benchmarks/src/appleMain/kotlin/GraphicsContext.apple.kt b/compose/mpp/benchmarks/src/appleMain/kotlin/GraphicsContext.apple.kt deleted file mode 100644 index fb6564fcc41cf..0000000000000 --- a/compose/mpp/benchmarks/src/appleMain/kotlin/GraphicsContext.apple.kt +++ /dev/null @@ -1,65 +0,0 @@ -import kotlinx.cinterop.ExperimentalForeignApi -import kotlinx.cinterop.objcPtr -import org.jetbrains.skia.BackendRenderTarget -import org.jetbrains.skia.ColorSpace -import org.jetbrains.skia.DirectContext -import org.jetbrains.skia.PixelGeometry -import org.jetbrains.skia.Surface -import org.jetbrains.skia.SurfaceColorFormat -import org.jetbrains.skia.SurfaceOrigin -import org.jetbrains.skia.SurfaceProps -import platform.Metal.MTLCreateSystemDefaultDevice -import platform.Metal.MTLPixelFormatBGRA8Unorm -import platform.Metal.MTLTextureDescriptor -import platform.Metal.MTLTextureType2D -import platform.Metal.MTLTextureUsageRenderTarget -import platform.Metal.MTLTextureUsageShaderRead -import platform.Metal.MTLTextureUsageShaderWrite -import kotlin.coroutines.resume -import kotlin.coroutines.suspendCoroutine - -@OptIn(ExperimentalForeignApi::class) -fun graphicsContext() = object : GraphicsContext { - private val device = MTLCreateSystemDefaultDevice() ?: throw IllegalStateException("Can't create MTLDevice") - private val commandQueue = device.newCommandQueue() ?: throw IllegalStateException("Can't create MTLCommandQueue") - private val directContext = DirectContext.makeMetal(device.objcPtr(), commandQueue.objcPtr()) - - override fun surface(width: Int, height: Int): Surface { - val descriptor = MTLTextureDescriptor() - descriptor.width = width.toULong() - descriptor.height = height.toULong() - descriptor.usage = MTLTextureUsageShaderRead or MTLTextureUsageShaderWrite or MTLTextureUsageRenderTarget - descriptor.textureType = MTLTextureType2D - descriptor.pixelFormat = MTLPixelFormatBGRA8Unorm - descriptor.mipmapLevelCount = 1UL - - val texture = device.newTextureWithDescriptor(descriptor) ?: throw IllegalStateException("Can't create MTLTexture") - - val renderTarget = BackendRenderTarget.makeMetal(width, height, texture.objcPtr()) - - return Surface.makeFromBackendRenderTarget( - directContext, - renderTarget, - SurfaceOrigin.TOP_LEFT, - SurfaceColorFormat.BGRA_8888, - ColorSpace.sRGB, - SurfaceProps(pixelGeometry = PixelGeometry.UNKNOWN) - ) ?: throw IllegalStateException("Can't create Surface") - } - - override suspend fun awaitGPUCompletion() { - val commandBuffer = commandQueue.commandBuffer() ?: return - - suspendCoroutine { continuation -> - commandBuffer.addCompletedHandler { - continuation.resume(Unit) - } - - commandBuffer.commit() - } - } -} - - - - diff --git a/compose/mpp/benchmarks/src/appleMain/kotlin/getProcessStartTime.apple.kt b/compose/mpp/benchmarks/src/appleMain/kotlin/getProcessStartTime.apple.kt deleted file mode 100644 index d02de1d7c330a..0000000000000 --- a/compose/mpp/benchmarks/src/appleMain/kotlin/getProcessStartTime.apple.kt +++ /dev/null @@ -1,38 +0,0 @@ -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/compose/mpp/benchmarks/src/appleMain/kotlin/runGC.native.kt b/compose/mpp/benchmarks/src/appleMain/kotlin/runGC.native.kt deleted file mode 100644 index 7b499db732553..0000000000000 --- a/compose/mpp/benchmarks/src/appleMain/kotlin/runGC.native.kt +++ /dev/null @@ -1,7 +0,0 @@ -import kotlin.native.runtime.GC -import kotlin.native.runtime.NativeRuntimeApi - -@OptIn(NativeRuntimeApi::class) -actual fun runGC() { - GC.collect() -} \ No newline at end of file diff --git a/compose/mpp/benchmarks/src/commonMain/composeResources/drawable/compose-multiplatform.png b/compose/mpp/benchmarks/src/commonMain/composeResources/drawable/compose-multiplatform.png deleted file mode 100644 index 8b74accd0beb7831c819df311cd38e47cb57f601..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 34968 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 diff --git a/compose/mpp/benchmarks/src/commonMain/composeResources/drawable/example1_cat.jpg b/compose/mpp/benchmarks/src/commonMain/composeResources/drawable/example1_cat.jpg deleted file mode 100644 index 62f987d6dcf1aeff1e227fdcea04c6bed7c26875..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 37524 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 Hf4=?~qAD`< diff --git a/compose/mpp/benchmarks/src/commonMain/composeResources/drawable/example1_ic_call_answer.xml b/compose/mpp/benchmarks/src/commonMain/composeResources/drawable/example1_ic_call_answer.xml deleted file mode 100644 index da8a42f54a3ec..0000000000000 --- a/compose/mpp/benchmarks/src/commonMain/composeResources/drawable/example1_ic_call_answer.xml +++ /dev/null @@ -1,36 +0,0 @@ - - - - diff --git a/compose/mpp/benchmarks/src/commonMain/composeResources/drawable/example1_sailing.svg b/compose/mpp/benchmarks/src/commonMain/composeResources/drawable/example1_sailing.svg deleted file mode 100644 index ba23d0fe090c4..0000000000000 --- a/compose/mpp/benchmarks/src/commonMain/composeResources/drawable/example1_sailing.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/compose/mpp/benchmarks/src/commonMain/composeResources/files/example1_compose-community-primary.png b/compose/mpp/benchmarks/src/commonMain/composeResources/files/example1_compose-community-primary.png deleted file mode 100644 index a73ddd79899482e2f1252de2a3e9d4b8225f140b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 17662 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 diff --git a/compose/mpp/benchmarks/src/commonMain/composeResources/font/jetbrainsmono_italic.ttf b/compose/mpp/benchmarks/src/commonMain/composeResources/font/jetbrainsmono_italic.ttf deleted file mode 100644 index 44e1f4a7f3acd89e15ab5050607ee7bb497f5d1a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 141572 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

-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 diff --git a/compose/mpp/benchmarks/src/commonMain/kotlin/BenchmarkComposeScene.kt b/compose/mpp/benchmarks/src/commonMain/kotlin/BenchmarkComposeScene.kt deleted file mode 100644 index 7354aea9df219..0000000000000 --- a/compose/mpp/benchmarks/src/commonMain/kotlin/BenchmarkComposeScene.kt +++ /dev/null @@ -1,36 +0,0 @@ -import androidx.compose.runtime.Composable -import androidx.compose.ui.InternalComposeUiApi -import androidx.compose.ui.graphics.Canvas -import androidx.compose.ui.platform.FrameRecomposer -import androidx.compose.ui.platform.registerSkikoComposeImplementation -import androidx.compose.ui.scene.CanvasLayersComposeScene -import androidx.compose.ui.unit.IntSize -import kotlinx.coroutines.Dispatchers - -@OptIn(InternalComposeUiApi::class) -internal class BenchmarkComposeScene(width: Int, height: Int) { - init { - registerSkikoComposeImplementation() - } - - private val frameRecomposer = FrameRecomposer(Dispatchers.Unconfined) - private val scene = CanvasLayersComposeScene( - frameRecomposer = frameRecomposer, - size = IntSize(width, height) - ) - - fun setContent(content: @Composable () -> Unit) { - scene.setContent(content = content) - } - - fun render(canvas: Canvas, nanoTime: Long) { - frameRecomposer.performFrame(nanoTime) - scene.measureAndLayout() - scene.draw(canvas) - } - - fun close() { - scene.close() - frameRecomposer.close() - } -} diff --git a/compose/mpp/benchmarks/src/commonMain/kotlin/Benchmarks.kt b/compose/mpp/benchmarks/src/commonMain/kotlin/Benchmarks.kt deleted file mode 100644 index e2fd20870c4fe..0000000000000 --- a/compose/mpp/benchmarks/src/commonMain/kotlin/Benchmarks.kt +++ /dev/null @@ -1,597 +0,0 @@ -import androidx.compose.runtime.Composable -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier -import androidx.compose.runtime.withFrameNanos -import kotlin.time.Duration.Companion.nanoseconds -import kotlin.time.TimeSource -import benchmarks.animation.AnimatedVisibility -import benchmarks.complexlazylist.components.MainUiNoImageUseModel -import benchmarks.multipleComponents.MultipleComponentsExample -import benchmarks.lazygrid.LazyGrid -import benchmarks.visualeffects.NYContent -import benchmarks.textlayout.TextLayout -import benchmarks.canvasdrawing.CanvasDrawing -import benchmarks.heavyshader.HeavyShader -import kotlinx.coroutines.delay -import kotlinx.serialization.Serializable -import kotlinx.serialization.json.Json -import kotlin.math.roundToInt -import kotlin.time.Duration -import kotlin.time.Duration.Companion.milliseconds -import kotlin.time.Duration.Companion.seconds -import kotlin.time.DurationUnit - -enum class BenchmarkFrameTimeKind { - CPU, GPU -} - - -fun BenchmarkFrameTimeKind.toPrettyPrintString(): String = - when (this) { - BenchmarkFrameTimeKind.CPU -> "CPU" - BenchmarkFrameTimeKind.GPU -> "GPU" - } - -data class BenchmarkFrame( - val cpuDuration: Duration, - val gpuDuration: Duration -) { - fun duration(kind: BenchmarkFrameTimeKind): Duration = - when (kind) { - BenchmarkFrameTimeKind.CPU -> cpuDuration - BenchmarkFrameTimeKind.GPU -> gpuDuration - } -} - -@Serializable -data class BenchmarkConditions( - val frameCount: Int, - val warmupCount: Int -) { - fun prettyPrint() { - println("$frameCount frames (warmup $warmupCount)") - } - - fun putFormattedValuesTo(map: MutableMap) { - map.put("Frames/warmup", "$frameCount/$warmupCount") - } -} - -@Serializable -data class FPSInfo( - val fps: Double -) { - - fun prettyPrint() { - println("Average FPS: $fps") - } - - fun putFormattedValuesTo(map: MutableMap) { - map.put("Average FPS", fps.toString()) - } -} - -@Serializable -data class FrameInfo( - val cpuTime: Duration, - val gpuTime: Duration, -) { - val totalTime = cpuTime + gpuTime - - fun prettyPrint() { - if (gpuTime == Duration.ZERO) { - println("Average frame time: $totalTime") - } else { - println("CPU average frame time: $cpuTime") - println("GPU average frame time: $gpuTime") - println("TOTAL average frame time: $totalTime") - } - } - - fun putFormattedValuesTo(map: MutableMap) { - map.put("CPU avg frame (ms)", cpuTime.formatAsMilliseconds()) - map.put("GPU avg frame (ms)", gpuTime.formatAsMilliseconds()) - map.put("TOTAL avg frame (ms)", totalTime.formatAsMilliseconds()) - } -} - -@Serializable -data class BenchmarkPercentileAverage( - val percentile: Double, - val average: Duration -) - -@Serializable -data class MissedFrames( - val count: Int, - val ratio: Double -) { - fun prettyPrint(description: String) { - println( - """ - Missed frames ($description): - - count: $count - - ratio: $ratio - - """.trimIndent() - ) - } - - fun putFormattedValuesTo(description: String, map: MutableMap) { - map.put("Missed frames ($description)", "$ratio") - } -} - -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, - val frameBudget: Duration, - val conditions: BenchmarkConditions, - val averageFrameInfo: FrameInfo?, - val averageFPSInfo: FPSInfo, - val percentileCPUAverage: List, - val percentileGPUAverage: List, - val noBufferingMissedFrames: MissedFrames, - val doubleBufferingMissedFrames: MissedFrames, - val startupTimeInfo: StartupTimeInfo? = null, -) { - fun prettyPrint() { - println("# $name") - val versionInfo = Config.versionInfo - if (versionInfo != null) { - println("Version: $versionInfo") - } - 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() - println() - } - if (Config.isModeEnabled(Mode.VSYNC_EMULATION)) { - percentileCPUAverage.prettyPrint(BenchmarkFrameTimeKind.CPU) - println() - percentileGPUAverage.prettyPrint(BenchmarkFrameTimeKind.GPU) - println() - noBufferingMissedFrames.prettyPrint("no buffering") - doubleBufferingMissedFrames.prettyPrint("double buffering") - } - if (Config.isModeEnabled(Mode.REAL)) { - averageFPSInfo.prettyPrint() - averageFrameInfo?.prettyPrint() - noBufferingMissedFrames.prettyPrint("estimation of real") - println() - } - } - - fun putFormattedValuesTo(map: MutableMap) { - val versionInfo = Config.versionInfo - if (versionInfo != null) { - 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) - } - if (Config.isModeEnabled(Mode.VSYNC_EMULATION)) { - percentileCPUAverage.putFormattedValuesTo(BenchmarkFrameTimeKind.CPU, map) - percentileGPUAverage.putFormattedValuesTo(BenchmarkFrameTimeKind.GPU, map) - noBufferingMissedFrames.putFormattedValuesTo("no buffering", map) - doubleBufferingMissedFrames.putFormattedValuesTo("double buffering", map) - } - if (Config.isModeEnabled(Mode.REAL)) { - averageFPSInfo.putFormattedValuesTo(map) - } - } - - private fun List.prettyPrint(kind: BenchmarkFrameTimeKind) { - forEach { - println("Worst p${(it.percentile * 100).roundToInt()} ${kind.toPrettyPrintString()} (ms): ${it.average}") - } - } - - private fun List.putFormattedValuesTo( - kind: BenchmarkFrameTimeKind, - map: MutableMap - ) { - forEach { - map.put( - "Worst p${(it.percentile * 100).roundToInt()} ${kind.toPrettyPrintString()} (ms)", - it.average.formatAsMilliseconds() - ) - } - } - - fun toJsonString(): String = json.encodeToString(serializer(), this) -} - -class BenchmarkResult( - private val name: String, - private val frameBudget: Duration, - private val conditions: BenchmarkConditions, - 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) - - val startIndex = ((frames.size - 1) * percentile).roundToInt() - - val percentileFrames = frames.sortedBy { it.duration(kind) }.subList(frames.size - startIndex - 1, frames.size) - - return averageDuration(percentileFrames) { - it.duration(kind) - } - } - - fun generateStats(): BenchmarkStats { - val noBufferingMissedFramesCount = frames.count { - it.cpuDuration + it.gpuDuration > frameBudget - } - - val doubleBufferingMissedFrames = frames.count { - maxOf(it.cpuDuration, it.gpuDuration) > frameBudget - } - - return BenchmarkStats( - name, - frameBudget, - conditions, - averageFrameInfo, - averageFPSInfo, - listOf(0.01, 0.02, 0.05, 0.1, 0.25, 0.5).map { percentile -> - val average = percentileAverageFrameTime(percentile, BenchmarkFrameTimeKind.CPU) - - BenchmarkPercentileAverage(percentile, average) - }, - listOf(0.01, 0.1, 0.5).map { percentile -> - val average = percentileAverageFrameTime(percentile, BenchmarkFrameTimeKind.GPU) - - BenchmarkPercentileAverage(percentile, average) - }, - MissedFrames(noBufferingMissedFramesCount, noBufferingMissedFramesCount / frames.size.toDouble()), - MissedFrames(doubleBufferingMissedFrames, doubleBufferingMissedFrames / frames.size.toDouble()), - startupTimeInfo = startupTimeInfo, - ) - } - - private fun averageDuration(frames: List, selector: (BenchmarkFrame) -> Duration): Duration = - frames.fold(Duration.ZERO) { acc, frame -> - acc + selector(frame) - } / frames.size - -} - -private fun Duration.formatAsMilliseconds(): String = (inWholeMicroseconds / 1000.0).toString() - -data class Benchmark( - val name: String, - val frameCount: Int, - val content: @Composable () -> Unit -) - -fun Benchmark(name: String, content: @Composable () -> Unit): Benchmark { - return Benchmark(name, Config.getBenchmarkProblemSize(name, Config.frameCount), content) -} - -expect val isSvgSupported: Boolean - -fun getBenchmarks(): List = listOf( - Benchmark("AnimatedVisibility") { AnimatedVisibility() }, - Benchmark("LazyGrid") { LazyGrid() }, - Benchmark("LazyGrid-ItemLaunchedEffect") { - LazyGrid(smoothScroll = false, withLaunchedEffectInItem = true) - }, - Benchmark("LazyGrid-SmoothScroll") { - LazyGrid(smoothScroll = true) - }, - Benchmark("LazyGrid-SmoothScroll-ItemLaunchedEffect") { - LazyGrid(smoothScroll = true, withLaunchedEffectInItem = true) - }, - Benchmark("VisualEffects") { - NYContent(1920, 1080) - }, - Benchmark("LazyList") { MainUiNoImageUseModel() }, - if (isSvgSupported) Benchmark("MultipleComponents") { MultipleComponentsExample(isVectorGraphicsSupported = true) } else null, - Benchmark("MultipleComponents-NoVectorGraphics") { - MultipleComponentsExample(isVectorGraphicsSupported = false) - }, - Benchmark("TextLayout") { TextLayout() }, - Benchmark("CanvasDrawing") { CanvasDrawing() }, - Benchmark("HeavyShader") { HeavyShader() } -).filterNotNull().sortedBy { it.name } - -suspend fun runBenchmark( - benchmark: Benchmark, - width: Int, - height: Int, - targetFps: Int, - graphicsContext: GraphicsContext?, - warmupCount: Int = Config.warmupCount, -) { - if (Config.isBenchmarkEnabled(benchmark.name)) { - val stats = measureComposable( - name = benchmark.name, - warmupCount = warmupCount, - frameCount = benchmark.frameCount, - width = width, - height = height, - targetFps = targetFps, - 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 && printJsonToOutput) { - println("JSON_START") - println(stats.toJsonString()) - println("JSON_END") - } - if (Config.saveStats()) { - saveBenchmarkStats(name = stats.name, stats = stats) - } -} - -suspend fun runBenchmarks( - benchmarks: List, - width: Int = 1920, - height: Int = 1080, - targetFps: Int = 120, - warmupCount: Int = Config.warmupCount, - graphicsContext: GraphicsContext? = null -) { - if (Config.listBenchmarks) { - println("AVAILABLE_BENCHMARKS_START") - benchmarks.forEach { println(it.name) } - println("AVAILABLE_BENCHMARKS_END") - return - } - println() - println("Running emulating $targetFps FPS") - println() - for (benchmark in benchmarks) { - runBenchmark(benchmark, width, height, targetFps, graphicsContext, warmupCount) - } -} - -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, - deviceFrameRate: Int, - onExit: () -> Unit -) { - var currentBenchmarkIndex by remember { mutableStateOf(0) } - 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 - - if (currentBenchmarkIndex == benchmarks.size) { - if (Config.reportAtTheEnd) { - results.forEach { it.prettyPrint() } - } - onExit() - 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 (phase != BenchmarkPhase.EMPTY_SCREEN) { - benchmark.content() - } - - 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 - } - BenchmarkPhase.WARMUP -> { - repeat(Config.warmupCount) { - withFrameNanos { } - } - phaseBeforeEmptyScreen = BenchmarkPhase.WARMUP - phase = BenchmarkPhase.EMPTY_SCREEN - } - BenchmarkPhase.EMPTY_SCREEN -> { - delay(Config.emptyScreenDelay.milliseconds) - withFrameNanos { } - 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") - } - } - 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 - } - } - } - } - } else { - LaunchedEffect(benchmark.name) { - currentBenchmarkIndex++ - } - } -} - -suspend fun runBenchmarks( - width: Int = 1920, - height: Int = 1080, - targetFps: Int = 120, - warmupCount: Int = Config.warmupCount, - graphicsContext: GraphicsContext? = null -) { - runBenchmarks(getBenchmarks(), width, height, targetFps, warmupCount, graphicsContext) -} diff --git a/compose/mpp/benchmarks/src/commonMain/kotlin/BenchmarksSave.kt b/compose/mpp/benchmarks/src/commonMain/kotlin/BenchmarksSave.kt deleted file mode 100644 index 8eb8e39ee81fc..0000000000000 --- a/compose/mpp/benchmarks/src/commonMain/kotlin/BenchmarksSave.kt +++ /dev/null @@ -1,92 +0,0 @@ -/* - * Copyright 2020-2025 JetBrains s.r.o. and respective authors and developers. - * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. - */ - -import kotlinx.datetime.LocalDateTime -import kotlinx.datetime.TimeZone -import kotlinx.datetime.format.FormatStringsInDatetimeFormats -import kotlinx.datetime.format.byUnicodePattern -import kotlinx.datetime.toLocalDateTime -import kotlinx.io.IOException -import kotlinx.io.RawSink -import kotlinx.io.RawSource -import kotlinx.io.buffered -import kotlinx.io.files.Path -import kotlinx.io.files.SystemFileSystem -import kotlinx.io.readByteArray -import kotlin.time.Clock - -// port for the benchmarks save server -val BENCHMARK_SERVER_PORT = 8090 - -private val BENCHMARKS_SAVE_DIR = "build/benchmarks" -private fun pathToCsv(name: String) = Path("$BENCHMARKS_SAVE_DIR/$name.csv") -private fun pathToJson(name: String) = Path("$BENCHMARKS_SAVE_DIR/json-reports/$name.json") - -internal fun saveJson(benchmarkName: String, jsonString: String) { - val jsonPath = pathToJson(benchmarkName) - SystemFileSystem.createDirectories(jsonPath.parent!!) - SystemFileSystem.sink(jsonPath).writeText(jsonString) - println("JSON results saved to ${SystemFileSystem.resolve(jsonPath)}") -} - -fun saveBenchmarkStatsOnDisk(name: String, stats: BenchmarkStats) { - try { - if (Config.saveStatsToCSV) { - val path = pathToCsv(name) - - val keyToValue = mutableMapOf() - keyToValue.put("Date", currentFormattedDate) - stats.putFormattedValuesTo(keyToValue) - - var text = if (SystemFileSystem.exists(path)) { - SystemFileSystem.source(path).readText() - } else { - keyToValue.keys.joinToString(",") + "\n" - } - - fun escapeForCSV(value: String) = value.replace(",", ";") - text += keyToValue.values.joinToString(",", transform = ::escapeForCSV) + "\n" - - SystemFileSystem.createDirectories(path.parent!!) - SystemFileSystem.sink(path).writeText(text) - println("CSV results saved to ${SystemFileSystem.resolve(path)}") - println() - } else if (Config.saveStatsToJSON) { - saveJson(name, stats.toJsonString()) - println() - } - } catch (_: IOException) { - // IOException "Read-only file system" is thrown on iOS without writing permissions - } catch (_: UnsupportedOperationException) { - // UnsupportedOperationException is thrown in browser - } -} - -/** - * Saves benchmark statistics to disk or sends them to a server. - * This is an expect function with platform-specific implementations. - */ -expect fun saveBenchmarkStats(name: String, stats: BenchmarkStats) - -expect val printJsonToOutput: Boolean - -private fun RawSource.readText() = use { - it.buffered().readByteArray().decodeToString() -} - -internal fun RawSink.writeText(text: String) = use { - it.buffered().apply { - write(text.encodeToByteArray()) - flush() - } -} - -@OptIn(FormatStringsInDatetimeFormats::class) -private val currentFormattedDate: String get() { - val currentTime = Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault()) - return LocalDateTime.Format { - byUnicodePattern("dd-MM-yyyy HH:mm:ss") - }.format(currentTime) -} \ No newline at end of file diff --git a/compose/mpp/benchmarks/src/commonMain/kotlin/Config.kt b/compose/mpp/benchmarks/src/commonMain/kotlin/Config.kt deleted file mode 100644 index ec5279e61b95b..0000000000000 --- a/compose/mpp/benchmarks/src/commonMain/kotlin/Config.kt +++ /dev/null @@ -1,236 +0,0 @@ -enum class Mode { - SIMPLE, - VSYNC_EMULATION, - REAL, - STARTUP -} - -object Args { - - private fun argToSet(arg: String): Set = arg.substring(arg.indexOf('=') + 1) - .split(",").filter{!it.isEmpty()}.map{it.uppercase()}.toSet() - - - private fun argToMap(arg: String): Map = arg.substring(arg.indexOf('=') + 1) - .split(",").filter { !it.isEmpty() }.map { it.uppercase() }.associate { - if (it.contains('(') && it.contains(')')) { - it.substringBefore('(') to it.substringAfter('(').substringBefore(')').toInt() - } else { - it to -1 - } - } - - private fun String.decodeArg() = replace("%20", " ") - - /** - * Parses command line arguments to create a [Config] for benchmarks run. - * - * @param args an array of strings representing the command line arguments. - * Each argument can specify either of these settings: - * modes, benchmarks, disabledBenchmarks - comma separated values, - * versionInfo, saveStatsToCSV, saveStatsToJSON, parallel, warmupCount, frameCount, emptyScreenDelay, reportAtTheEnd, listBenchmarks - single values. - * - * Example: benchmarks=AnimatedVisibility(100),modes=SIMPLE,versionInfo=Kotlin_2_1_20,saveStatsToCSV=true,warmupCount=50,frameCount=100,emptyScreenDelay=2000,reportAtTheEnd=true - */ - fun parseArgs(args: Array): Config { - val modes = mutableSetOf() - val benchmarks = mutableMapOf() - val disabledBenchmarks = mutableSetOf() - var versionInfo: String? = null - var saveStatsToCSV: Boolean = false - var saveStatsToJSON: Boolean = false - var runServer: Boolean = false - var parallelRendering: Boolean = false - var warmupCount: Int? = null - var frameCount: Int? = null - 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)) { - modes.addAll(argToSet(arg.decodeArg()).map { Mode.valueOf(it) }) - } else if (arg.startsWith("benchmarks=", ignoreCase = true)) { - benchmarks += argToMap(arg.decodeArg()) - } else if (arg.startsWith("versionInfo=", ignoreCase = true)) { - versionInfo = arg.substringAfter("=").decodeArg() - } else if (arg.startsWith("saveStatsToCSV=", ignoreCase = true)) { - saveStatsToCSV = arg.substringAfter("=").toBoolean() - } else if (arg.startsWith("saveStatsToJSON=", ignoreCase = true)) { - saveStatsToJSON = arg.substringAfter("=").toBoolean() - } else if (arg.startsWith("disabledBenchmarks=", ignoreCase = true)) { - disabledBenchmarks += argToMap(arg.decodeArg()).keys - } else if (arg.startsWith("runServer=", ignoreCase = true)) { - runServer = arg.substringAfter("=").toBoolean() - } else if (arg.startsWith("parallel=", ignoreCase = true)) { - parallelRendering = arg.substringAfter("=").toBoolean() - } else if (arg.startsWith("warmupCount=", ignoreCase = true)) { - warmupCount = arg.substringAfter("=").toInt() - } else if (arg.startsWith("frameCount=", ignoreCase = true)) { - frameCount = arg.substringAfter("=").toInt() - } else if (arg.startsWith("emptyScreenDelay=", ignoreCase = true)) { - emptyScreenDelay = arg.substringAfter("=").toLong() - } else if (arg.startsWith("reportAtTheEnd=", ignoreCase = true)) { - 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( - modes = modes, - benchmarks = benchmarks, - disabledBenchmarks = disabledBenchmarks, - versionInfo = versionInfo, - saveStatsToCSV = saveStatsToCSV, - saveStatsToJSON = saveStatsToJSON, - runServer = runServer, - parallelRendering = parallelRendering, - warmupCount = warmupCount ?: defaultWarmupCount, - frameCount = frameCount ?: 1000, - emptyScreenDelay = emptyScreenDelay ?: 2000L, - reportAtTheEnd = reportAtTheEnd, - listBenchmarks = listBenchmarks, - startupFrameCount = startupFrameCount ?: 30, - startupLongestFramesCount = startupLongestFramesCount ?: 3 - ) - } -} - -/** - * Represents the benchmarks configuration parsed from command line arguments or configured programmatically. - * - * @property modes The set of enabled execution modes. If empty, all modes are considered enabled by default checks. - * @property benchmarks A map of explicitly mentioned benchmarks to their specific problem sizes. - * A value of -1 indicates the benchmark is enabled but should use its default size. - * If the map is empty, all benchmarks are considered enabled by default checks. - * @property disabledBenchmarks A set of benchmarks to skip. - * @property versionInfo Optional string containing version information. - * @property saveStatsToCSV Flag indicating whether statistics should be saved to a CSV file. - * @property saveStatsToJSON Flag indicating whether statistics should be saved to a JSON file. - * @property parallelRendering Flag indicating whether we should use prallelRenderin on iOS - * @property warmupCount Number of warmup frames to run before starting the benchmark. - * @property frameCount Number of frames to run for each benchmark. - * @property emptyScreenDelay Delay in milliseconds between warmup and benchmark. - * @property reportAtTheEnd Flag indicating whether we should report results at the end of all benchmarks. - * @property listBenchmarks Flag indicating whether we should print available benchmarks and exit. - */ -data class Config( - val modes: Set = emptySet(), - val benchmarks: Map = emptyMap(), // Name -> Problem Size (-1 for default) - val disabledBenchmarks: Set = emptySet(), - val versionInfo: String? = null, - val saveStatsToCSV: Boolean = false, - val saveStatsToJSON: Boolean = false, - val runServer: Boolean = false, - val parallelRendering: Boolean = false, - val warmupCount: Int = 100, - val frameCount: Int = 1000, - val emptyScreenDelay: Long = 2000L, - val reportAtTheEnd: 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.contains(mode) - - /** - * Checks if a specific benchmark is enabled - */ - fun isBenchmarkEnabled(benchmark: String): Boolean { - val normalizedName = benchmark.uppercase() - // Enabled if the benchmarks map is empty OR if the specific benchmark is present - return (benchmarks.isEmpty() || benchmarks.containsKey(normalizedName)) - && !disabledBenchmarks.contains(normalizedName) - && !disabledBenchmarks.contains(benchmark) - } - - /** - * Returns the problem size configured for [benchmark], or [default] if not set. - * - * @param benchmark Benchmark name (case-insensitive). - * @param default Fallback size when no configuration is found. - * @return The problem size to use. - */ - fun getBenchmarkProblemSize(benchmark: String, default: Int): Int { - val normalizedName = benchmark.uppercase() - val problemSize = benchmarks[normalizedName] ?: -1 - return if (problemSize == -1) default else problemSize - } - - companion object { - private var global: Config = Config() - - val versionInfo: String? - get() = global.versionInfo - - val saveStatsToCSV: Boolean - get() = global.saveStatsToCSV - - val saveStatsToJSON: Boolean - get() = global.saveStatsToJSON - - val runServer: Boolean - get() = global.runServer - - val parallelRendering: Boolean - get() = global.parallelRendering - - val warmupCount: Int - get() = global.warmupCount - - val frameCount: Int - get() = global.frameCount - - val emptyScreenDelay: Long - get() = global.emptyScreenDelay - - val reportAtTheEnd: Boolean - get() = global.reportAtTheEnd - - 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 - } - - fun setGlobalFromArgs(args: Array) { - this.global = Args.parseArgs(args) - } - - fun isModeEnabled(mode: Mode): Boolean = - global.isModeEnabled(mode) - - fun isBenchmarkEnabled(benchmark: String): Boolean = - global.isBenchmarkEnabled(benchmark) - - fun getBenchmarkProblemSize(benchmark: String, default: Int): Int = - global.getBenchmarkProblemSize(benchmark, default) - - fun saveStats() = saveStatsToCSV || saveStatsToJSON - } -} \ No newline at end of file diff --git a/compose/mpp/benchmarks/src/commonMain/kotlin/MeasureComposable.kt b/compose/mpp/benchmarks/src/commonMain/kotlin/MeasureComposable.kt deleted file mode 100644 index b38d513eb8a4f..0000000000000 --- a/compose/mpp/benchmarks/src/commonMain/kotlin/MeasureComposable.kt +++ /dev/null @@ -1,174 +0,0 @@ -import androidx.compose.runtime.Composable -import androidx.compose.ui.graphics.asComposeCanvas -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.DurationUnit -import kotlin.time.TimeSource.Monotonic.markNow -import kotlin.time.measureTime - -const val nanosPerSecond = 1E9.toLong() - -interface GraphicsContext { - fun surface(width: Int, height: Int): Surface - - suspend fun awaitGPUCompletion() -} - -expect fun runGC() - -suspend inline fun preciseDelay(duration: Duration) { - val liveDelay: Duration - if (duration.inWholeMilliseconds > 2) { - //experiments have shown that for precise delay we should do live delay at least 2 ms - val delayMillis = duration.inWholeMilliseconds - 2 - val delayStart = markNow() - delay(delayMillis) - liveDelay = duration - delayStart.elapsedNow() - } else { - liveDelay = duration - } - val liveDelayStart = markNow() - while (liveDelayStart.elapsedNow() < liveDelay){} -} - -/** - * Some of the benchmarks involved an asynchronous fetch operation for resources when running in a browser. - * To let the fetch operation result be handled by compose, the benchmark loop must yield the event loop. - * Otherwise, such benchmark do not run a part of workload, making the stats less meaningful. - * - * It makes sense for all platforms, since there could be some work scheduled to run on the same Thread - * as the benchmarks runner. But without yielding, it won't be dispatched. - */ -private suspend inline fun yieldEventLoop() { - yield() -} - -@OptIn(ExperimentalTime::class) -internal suspend fun measureComposable( - name: String, - warmupCount: Int, - frameCount: Int, - width: Int, - height: Int, - targetFps: Int, - graphicsContext: GraphicsContext?, - content: @Composable () -> Unit -): BenchmarkResult { - val surface = graphicsContext?.surface(width, height) ?: Surface.makeNull(width, height) - val scene = BenchmarkComposeScene(width, height) - try { - val nanosPerFrame = (1.0 / targetFps.toDouble() * nanosPerSecond).toLong() - scene.setContent(content) - - // warmup - repeat(warmupCount) { - scene.mimicSkikoRender(surface, it * nanosPerFrame, width, height) - surface.flushAndSubmit(false) - graphicsContext?.awaitGPUCompletion() - yieldEventLoop() - } - - runGC() - - var cpuTotalTime = Duration.ZERO - var gpuTotalTime = Duration.ZERO - if (Config.isModeEnabled(Mode.SIMPLE)) { - cpuTotalTime = measureTime { - repeat(frameCount) { - scene.mimicSkikoRender(surface, it * nanosPerFrame, width, height) - surface.flushAndSubmit(false) - gpuTotalTime += measureTime { - graphicsContext?.awaitGPUCompletion() - } - yieldEventLoop() - } - } - cpuTotalTime -= gpuTotalTime - } - - val frames = MutableList(frameCount) { - BenchmarkFrame(Duration.INFINITE, Duration.INFINITE) - } - - if (Config.isModeEnabled(Mode.VSYNC_EMULATION)) { - var nextVSync = Duration.ZERO - var missedFrames = 0; - - runGC() - - val start = markNow() - - repeat(frameCount) { - val frameStart = start + nextVSync - - scene.mimicSkikoRender(surface, it * nextVSync.inWholeNanoseconds, width, height) - surface.flushAndSubmit(false) - - val cpuTime = frameStart.elapsedNow() - - graphicsContext?.awaitGPUCompletion() - - val frameTime = frameStart.elapsedNow() - - frames[it] = BenchmarkFrame(cpuTime, frameTime - cpuTime) - - 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 - preciseDelay(timeUntilNextVSync) - } - - yieldEventLoop() - } - } - - return BenchmarkResult( - name, - nanosPerFrame.nanoseconds, - BenchmarkConditions(frameCount, warmupCount), - FrameInfo(cpuTotalTime / frameCount, gpuTotalTime / frameCount), - FPSInfo(frameCount.toDouble() / (cpuTotalTime + gpuTotalTime).toDouble(DurationUnit.SECONDS)), - frames, - ) - } finally { - scene.close() - surface.close() - runGC() // cleanup for next benchmarks - } -} - -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 a simplified logic and it still can differ from the real cases: - * - Rendering into an intermediate picture. Benchmarks can show incorrect results without it. - * For example, we had a case when they showed an improvement by 10%, - * but there was a regression by 10% - * - Clearing the canvas each frame - * - * 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. - */ -private fun BenchmarkComposeScene.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() -} diff --git a/compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/animation/AnimatedVisibility.kt b/compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/animation/AnimatedVisibility.kt deleted file mode 100644 index 5b802922a61bb..0000000000000 --- a/compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/animation/AnimatedVisibility.kt +++ /dev/null @@ -1,47 +0,0 @@ -package benchmarks.animation - -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.EnterExitState -import androidx.compose.animation.core.Transition -import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.material.MaterialTheme -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.runtime.withFrameNanos -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import compose_benchmarks.benchmarks.generated.resources.Res -import compose_benchmarks.benchmarks.generated.resources.compose_multiplatform -import org.jetbrains.compose.resources.ExperimentalResourceApi -import org.jetbrains.compose.resources.painterResource - -@OptIn(ExperimentalResourceApi::class) -@Composable -fun AnimatedVisibility() { - MaterialTheme { - var showImage by remember { mutableStateOf(false) } - var transition: Transition? = null - LaunchedEffect(showImage) { - do { - withFrameNanos {} - } while (transition?.isRunning == true) - - showImage = !showImage - } - Column(Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) { - AnimatedVisibility(showImage) { - transition = this.transition - Image( - painterResource(Res.drawable.compose_multiplatform), - null - ) - } - } - } -} \ No newline at end of file diff --git a/compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/canvasdrawing/CanvasDrawing.kt b/compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/canvasdrawing/CanvasDrawing.kt deleted file mode 100644 index bad963ec2ddae..0000000000000 --- a/compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/canvasdrawing/CanvasDrawing.kt +++ /dev/null @@ -1,159 +0,0 @@ -/* - * Copyright 2020-2026 JetBrains s.r.o. and respective authors and developers. - * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. - */ - -package benchmarks.canvasdrawing - -import androidx.compose.animation.core.LinearEasing -import androidx.compose.animation.core.RepeatMode -import androidx.compose.animation.core.animateFloat -import androidx.compose.animation.core.infiniteRepeatable -import androidx.compose.animation.core.rememberInfiniteTransition -import androidx.compose.animation.core.tween -import androidx.compose.foundation.Canvas -import androidx.compose.foundation.background -import androidx.compose.foundation.gestures.scrollBy -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.runtime.withFrameMillis -import androidx.compose.ui.Modifier -import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.graphics.Brush -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.Path -import androidx.compose.ui.unit.dp -import kotlin.math.PI -import kotlin.math.cos -import kotlin.math.sin -import kotlin.random.Random -import kotlinx.coroutines.isActive - -private const val ITEM_COUNT = 1200 - -@Composable -fun CanvasDrawing() { - val listState = rememberLazyListState() - var scrollForward by remember { mutableStateOf(true) } - - LaunchedEffect(Unit) { - while (isActive) { - withFrameMillis { } - val currentItem = listState.firstVisibleItemIndex - if (currentItem == 0) scrollForward = true - if (currentItem > ITEM_COUNT - 100) scrollForward = false - listState.scrollBy(if (scrollForward) 33f else -33f) - } - } - - LazyColumn( - state = listState, - modifier = Modifier.fillMaxSize().background(Color.Black) - ) { - items(ITEM_COUNT) { index -> - CanvasDrawingItem(index) - } - } -} - -@Composable -private fun CanvasDrawingItem(index: Int) { - val animatedValue by rememberInfiniteTransition().animateFloat( - initialValue = 0f, - targetValue = 360f, - animationSpec = infiniteRepeatable( - animation = tween(3000, easing = LinearEasing), - repeatMode = RepeatMode.Restart - ) - ) - - Canvas( - modifier = Modifier - .fillMaxWidth() - .height(300.dp) - .padding(8.dp) - ) { - val seed = index * 12345L - val random = Random(seed) - - drawRect( - brush = Brush.verticalGradient( - colors = listOf( - Color(random.nextInt(0xFFFFFF) or 0xFF000000.toInt()), - Color(random.nextInt(0xFFFFFF) or 0xFF000000.toInt()) - ) - ) - ) - - repeat(200) { i -> - val angle = animatedValue + i * 7.2f - val radius = size.minDimension / 4f - val x = size.width / 2 + cos(angle * PI / 180.0).toFloat() * radius * random.nextFloat() - val y = size.height / 2 + sin(angle * PI / 180.0).toFloat() * radius * random.nextFloat() - - val path = Path().apply { - moveTo(x, y) - repeat(40) { j -> - val pointAngle = angle + j * 45f + animatedValue - val pointRadius = 20f + random.nextFloat() * 30f - val px = x + cos(pointAngle * PI / 180.0).toFloat() * pointRadius - val py = y + sin(pointAngle * PI / 180.0).toFloat() * pointRadius - if (j % 2 == 0) { - lineTo(px, py) - } else { - quadraticBezierTo( - x + random.nextFloat() * 50f - 25f, - y + random.nextFloat() * 50f - 25f, - px, py - ) - } - } - close() - } - - drawPath( - path = path, - brush = Brush.radialGradient( - colors = listOf( - Color(random.nextInt(0xFFFFFF) or 0x80000000.toInt()), - Color(random.nextInt(0xFFFFFF) or 0x40000000.toInt()) - ), - center = Offset(x, y) - ) - ) - } - - repeat(70) { i -> - val lineY = i * (size.height / 30) - drawLine( - color = Color(random.nextInt(0xFFFFFF) or 0xFF000000.toInt()), - start = Offset(0f, lineY), - end = Offset(size.width, lineY + sin(animatedValue * i).toFloat() * 20f), - strokeWidth = 2f - ) - } - - repeat(70) { i -> - val circleX = (i % 8) * (size.width / 8) + (size.width / 16) - val circleY = (i / 8) * (size.height / 5) + (size.height / 10) - val circleRadius = 15f + sin(animatedValue + i).toFloat() * 10f - - drawCircle( - color = Color(random.nextInt(0xFFFFFF) or 0xFF000000.toInt()), - radius = circleRadius, - center = Offset(circleX, circleY), - alpha = 0.7f - ) - } - } -} diff --git a/compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/complexlazylist/components/MainUI.kt b/compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/complexlazylist/components/MainUI.kt deleted file mode 100644 index 47b05d6a528cf..0000000000000 --- a/compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/complexlazylist/components/MainUI.kt +++ /dev/null @@ -1,110 +0,0 @@ -package benchmarks.complexlazylist.components - -import androidx.compose.foundation.gestures.scrollBy -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.itemsIndexed -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.material.MaterialTheme -import androidx.compose.runtime.* -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch -import benchmarks.complexlazylist.components.refresh.LOADING_MORE -import benchmarks.complexlazylist.components.refresh.LoadingIndicatorDefault -import benchmarks.complexlazylist.components.refresh.NORMAL -import benchmarks.complexlazylist.components.refresh.REFRESHING -import benchmarks.complexlazylist.components.refresh.SwipeRefreshLayout -import benchmarks.complexlazylist.components.refresh.rememberSwipeRefreshState -import benchmarks.complexlazylist.models.IBaseViewModel -import benchmarks.complexlazylist.models.ICompositionModel -import benchmarks.complexlazylist.models.fetchCompositionModels - -// just for demo -internal var models: MutableList = mutableStateListOf() - -@Composable -fun MainUiNoImageUseModel() { - MaterialTheme { - MainLazyColumnItemsList(noImage = true, useJson = false) - DisposableEffect(Unit) { - onDispose { - models.clear() - } - } - } -} - -@Composable -fun MainLazyColumnItemsList(noImage: Boolean, useJson: Boolean) { - val scope = rememberCoroutineScope() - val state = rememberSwipeRefreshState(NORMAL) - - LaunchedEffect(scope) { - scope.launch(Dispatchers.Default) { - fetchCompositionModels(false) { list -> - for (item in list) - models.add(item) - } - } - } - - SwipeRefreshLayout( - state = state, - indicator = { modifier, s, indicatorHeight -> - LoadingIndicatorDefault(modifier, s, indicatorHeight) - }, - onRefresh = { - scope.launch { - state.loadState = REFRESHING - delay(2000) - fetchCompositionModels(useJson) { - models.clear() - for (item in it) - models.add(item) - state.loadState = NORMAL - } - } - - }, - onLoadMore = { - scope.launch { - state.loadState = LOADING_MORE - delay(2000L) - fetchCompositionModels(useJson) { - for (item in it) - models.add(item) - state.loadState = NORMAL - } - } - } - ) { modifier -> - run { - val state = rememberLazyListState() - LaunchedEffect(Unit) { - while (true) { - withFrameMillis { } - state.scrollBy(20f) - } - } - LazyColumn(modifier, state = state) { - itemsIndexed( - items = models, - key = { index, _ -> - index - } - ) { _, item -> - when (item) { - is ICompositionModel -> MultiCellUI(item) - // .. todo need more types - else -> throw RuntimeException("Unexpected") - } - } - } - } - } -} - -@Composable -fun MultiCellUI(item: IBaseViewModel) { - DecoratedCell(item) -} \ No newline at end of file diff --git a/compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/complexlazylist/components/MultiLevelCell.kt b/compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/complexlazylist/components/MultiLevelCell.kt deleted file mode 100644 index 77ef62ce5ecbe..0000000000000 --- a/compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/complexlazylist/components/MultiLevelCell.kt +++ /dev/null @@ -1,351 +0,0 @@ -package benchmarks.complexlazylist.components - -import androidx.compose.animation.core.animate -import androidx.compose.animation.core.animateDpAsState -import androidx.compose.animation.core.tween -import androidx.compose.foundation.* -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.MaterialTheme.typography -import androidx.compose.material.Text -import androidx.compose.runtime.* -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.alpha -import androidx.compose.ui.draw.clip -import androidx.compose.ui.draw.scale -import androidx.compose.ui.draw.shadow -import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.graphics.Brush -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.Shape -import androidx.compose.ui.graphics.SolidColor -import androidx.compose.ui.graphics.painter.ColorPainter -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import benchmarks.complexlazylist.models.IBaseViewModel -import benchmarks.complexlazylist.models.ICompositionItem -import benchmarks.complexlazylist.models.ICompositionModel -import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.launch - -@Composable -internal fun DecoratedCell(model: IBaseViewModel) { - Column( - modifier = Modifier - .fillMaxWidth() - .border(1.dp, Color.LightGray, RoundedCornerShape(4.dp)) - .background( - brush = Brush.linearGradient( - colors = listOf( - Color.LightGray.copy(1f), - Color.LightGray.copy(0.5f), - Color.LightGray.copy(0f) - ), - start = Offset(320f, 0f), - end = Offset(320f, 164f) - ) - ), - verticalArrangement = Arrangement.spacedBy((-60).dp) - ) { - CompositionView(model) - Box( - modifier = Modifier.fillMaxWidth() - .heightIn(164.dp), - contentAlignment = Alignment.BottomCenter, - ) { - Row( - modifier = Modifier - .fillMaxWidth() - .heightIn(64.dp) - .padding(8.dp) - .border(1.dp, Color.Cyan.copy(0.3f), RoundedCornerShape(4.dp)) - .padding(8.dp) - .background(color = Color.Cyan.copy(alpha = 0.1f)) - ) { - - Avatar( - modifier = Modifier - .padding(4.dp) - .align(Alignment.CenterVertically) - ) - - TextInfo( - Modifier - .weight(1f) - .align(Alignment.CenterVertically) - ) - - FollowBtn( - Modifier.align(Alignment.CenterVertically) - ) - } - } - } -} - -@Composable -internal fun CompositionView(model: IBaseViewModel) { - if (model !is ICompositionModel) - return - Box( - modifier = Modifier.clip(RoundedCornerShape(10.dp)).background(Color.White).padding(10.dp) - ) { - model.overlyTopLeft?.let { overlyTopLeft(it) } - model.overlyView1?.let { overlyView1(it) } - model.overlyView2?.let { overlyView2(it) } - model.overlyView3?.let { overlyView3(it) } - model.overlyTopRight?.let { overlyTopRight(it) } - model.label?.let { overlyLabel(it) } - } -} - -@Composable -internal fun overlyTopLeft(compositionItem: ICompositionItem) { - compositionItem.bgColor?.let { Color.DarkGray }?.let { ColorPainter(it) }?.let { - compositionItem.alpha?.toFloat()?.let { it1 -> - compositionItem.radius?.toFloat()?.let { RoundedCornerShape(it.dp) }?.let { it2 -> - Modifier - .padding(start = 0.dp, top = 0.dp) - .width(50.dp) - .height(50.dp) - .alpha(it1) - .clip(shape = it2) - .shadow(3.dp) - } - }?.let { it2 -> - Image( - painter = it, - modifier = it2, - contentDescription = null - ) - } - } -} - -@Composable -internal fun overlyView1(compositionItem: ICompositionItem) { - compositionItem.bgColor?.let { Color.Red }?.let { ColorPainter(it) }?.let { - compositionItem.alpha?.toFloat()?.let { it1 -> - compositionItem.radius?.toFloat()?.let { RoundedCornerShape(it.dp) }?.let { it2 -> - Modifier - .padding(start = 20.dp, top = 20.dp) - .width(150.dp) - .height(100.dp) - .alpha(it1) - .clip(shape = it2) - .shadow(3.dp) - } - }?.let { it2 -> - Image( - painter = it, - modifier = it2, - contentDescription = null - ) - } - } -} - -@Composable -internal fun overlyView2(compositionItem: ICompositionItem) { - compositionItem.bgColor?.let { Color.Yellow }?.let { ColorPainter(it) }?.let { - compositionItem.alpha?.toFloat()?.let { it1 -> - compositionItem.radius?.toFloat()?.let { RoundedCornerShape(it.dp) }?.let { it2 -> - Modifier - .padding(start = 100.dp, top = 50.dp) - .width(150.dp) - .height(100.dp) - .alpha(it1) - .clip(shape = it2) - .shadow(3.dp) - } - }?.let { it2 -> - Image( - painter = it, - modifier = it2, - contentDescription = null - ) - } - } -} - -@Composable -internal fun overlyView3(compositionItem: ICompositionItem) { - compositionItem.bgColor?.let { Color.Green }?.let { ColorPainter(it) }?.let { - compositionItem.alpha?.toFloat()?.let { it1 -> - compositionItem.radius?.toFloat()?.let { RoundedCornerShape(it.dp) }?.let { it2 -> - Modifier - .padding(start = 110.dp, top = 60.dp) - .width(130.dp) - .height(80.dp) - .alpha(it1) - .clip(shape = it2) - .shadow(3.dp) - } - }?.let { it2 -> - Image( - painter = it, - modifier = it2, - contentDescription = null - ) - } - } -} - -@Composable -internal fun overlyTopRight(compositionItem: ICompositionItem) { - compositionItem.bgColor?.let { Color.LightGray }?.let { ColorPainter(it) }?.let { - compositionItem.alpha?.toFloat()?.let { it1 -> - compositionItem.radius?.toFloat()?.let { RoundedCornerShape(it.dp) }?.let { it2 -> - Modifier - .padding(start = 160.dp, top = 15.dp) - .width(90.dp) - .height(60.dp) - .alpha(it1) - .clip(shape = it2) - .shadow(3.dp) - } - }?.let { it2 -> - Image( - painter = it, - modifier = it2, - contentDescription = null - ) - } - } -} - -@Composable -internal fun overlyLabel(compositionItem: ICompositionItem) { - Box(modifier = Modifier.fillMaxSize().padding(), contentAlignment = Alignment.TopCenter) { - compositionItem.text?.let { - Text( - text = it, - modifier = Modifier.background(Color.Black.copy(0.5f)).alpha(0.5f) - .padding(start = 10.dp, end = 10.dp).width(200.dp).height(100.dp), - color = Color.Cyan - ) - } - } -} - -@Composable -internal fun Avatar(modifier: Modifier) { - Image( - modifier = modifier - .size(50.dp) - .clip(CircleShape) - .border( - shape = CircleShape, - border = BorderStroke( - width = 2.dp, - brush = Brush.linearGradient( - colors = listOf(Color.Blue, Color.Yellow, Color.Green, Color.Red), - start = Offset(0f, 0f), - end = Offset(100f, 100f) - ) - ) - ) - .border( - shape = CircleShape, - border = BorderStroke(4.dp, SolidColor(Color.White)) - ), - painter = ColorPainter(color = Color.Gray), - contentDescription = null, // decorative - ) -} - -@Composable -internal fun TextInfo(modifier: Modifier) { - Column(modifier = modifier) { - Text( - text = "name", - color = Color.Black, - maxLines = 1, - style = TextStyle( - fontWeight = FontWeight.Bold, - fontSize = 16.sp, - letterSpacing = 0.15.sp - ) - ) - Text( - text = "desc", - color = Color.Black.copy(alpha = 0.75f), - maxLines = 1, - style = TextStyle( // here - fontWeight = FontWeight.Normal, - fontSize = 14.sp, - letterSpacing = 0.25.sp - ) - ) - } -} - -@Composable -internal fun FollowBtn(modifier: Modifier) { - var checked by remember { mutableStateOf(false) } - val backgroundShape: Shape = remember { RoundedCornerShape(4.dp) } - var clickCount by remember { mutableStateOf(1) } - val offsetX by animateDpAsState( - if (checked) (-80).dp else (-10).dp, - animationSpec = tween(durationMillis = 3000) - ) - - var alpha by remember { mutableStateOf(1f) } - var scale by remember { mutableStateOf(1f) } - val scope = rememberCoroutineScope() - Row( - horizontalArrangement = Arrangement.spacedBy(10.dp), - verticalAlignment = Alignment.CenterVertically, - modifier = modifier.offset(x = offsetX), - ) { - Text( - text = if (checked) "unfollow" else "follow", - style = typography.body1.copy(color = Color.White), - textAlign = TextAlign.Center, - modifier = modifier - .width(100.dp) - .clickable(onClick = { - clickCount++ - checked = !checked - scope.launch { - coroutineScope { - launch { - animate(1f, 0.5f) { value, _ -> alpha = value } - } - launch { - animate(1f, 0.5f) { value, _ -> scale = value } - } - } - coroutineScope { - launch { - animate(0.5f, 1f) { value, _ -> alpha = value } - } - launch { - animate(0.5f, 1f) { value, _ -> scale = value } - } - } - } - }) - .shadow(5.dp, shape = backgroundShape) - .clip(backgroundShape) - .background( - brush = Brush.verticalGradient( - colors = listOf( - Color.Red, - Color.Yellow, - ), - startY = 0f, - endY = 80f - ) - ) - .padding(6.dp) - .scale(scale) - .alpha(alpha) - ) - } -} diff --git a/compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/complexlazylist/components/refresh/LoadingIndicator.kt b/compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/complexlazylist/components/refresh/LoadingIndicator.kt deleted file mode 100644 index 359f4ea74fd67..0000000000000 --- a/compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/complexlazylist/components/refresh/LoadingIndicator.kt +++ /dev/null @@ -1,159 +0,0 @@ -package benchmarks.complexlazylist.components.refresh - -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.core.LinearEasing -import androidx.compose.animation.core.RepeatMode -import androidx.compose.animation.core.animate -import androidx.compose.animation.core.infiniteRepeatable -import androidx.compose.animation.core.tween -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.foundation.Canvas -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.BoxScope -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.aspectRatio -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.rotate -import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.StrokeCap -import androidx.compose.ui.graphics.drawscope.Stroke -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.max - -@Composable -internal fun BoxScope.LoadingIndicatorDefault( - modifier: Modifier, - state: SwipeRefreshState, - indicatorHeight: Dp -) { - val height = max(30.dp, with(LocalDensity.current) { - state.progress.offset.toDp() - }) - var prev by remember { mutableStateOf(Offset(0f, 0f)) } - var prevOffsetAngle by remember { mutableStateOf(0f) } - var offsetAngle by remember { mutableStateOf(0f) } - var startAngle by remember { mutableStateOf(prev.x) } - var endAngle by remember { mutableStateOf(prev.y) } - - LaunchedEffect(state.loadState == REFRESHING || state.loadState == LOADING_MORE) { - if (!(state.loadState == REFRESHING || state.loadState == LOADING_MORE)) { - prevOffsetAngle = offsetAngle - return@LaunchedEffect - } - animate( - initialValue = prevOffsetAngle, - targetValue = prevOffsetAngle + 360f, - animationSpec = infiniteRepeatable( - animation = tween(5000, easing = LinearEasing), - repeatMode = RepeatMode.Restart - ) - ) { value, _ -> - offsetAngle = value - } - } - - LaunchedEffect(state.loadState == REFRESHING || state.loadState == LOADING_MORE, endAngle) { - if (!(state.loadState == REFRESHING || state.loadState == LOADING_MORE)) return@LaunchedEffect - if ((endAngle - startAngle).toInt() == 270) { - animate( - initialValue = startAngle, - targetValue = endAngle - 10f, - animationSpec = tween(durationMillis = 700) - ) { value, _ -> - startAngle = value - } - } - } - - LaunchedEffect(state.loadState == REFRESHING || state.loadState == LOADING_MORE, startAngle) { - if (!(state.loadState == REFRESHING || state.loadState == LOADING_MORE)) if (!(state.loadState == REFRESHING || state.loadState == LOADING_MORE)) { - prev = Offset(startAngle, endAngle) - return@LaunchedEffect - } - if ((endAngle - startAngle).toInt() == 10 - || endAngle.toInt() == prev.y.toInt() - ) { - animate( - initialValue = endAngle, - targetValue = startAngle + 270f, - animationSpec = tween(durationMillis = 700), - ) { value, _ -> - endAngle = value - } - } - } - - Box( - modifier - .fillMaxWidth() - .height(height), contentAlignment = Alignment.Center - ) { - if (state.isSwipeInProgress) { - if (state.progress.offset <= with(LocalDensity.current) { indicatorHeight.toPx() }) { - Text(text = if (state.progress.location == TOP) "下拉刷新" else "上拉加载更多") - } else { - Text(text = if (state.progress.location == TOP) "松开刷新" else "松开加载更多") - } - } else { - AnimatedVisibility( - visible = state.loadState == REFRESHING || state.loadState == LOADING_MORE, - modifier = Modifier.align(Alignment.Center), - enter = fadeIn(), - exit = fadeOut() - ) { - Row( - modifier = Modifier - .align(alignment = Alignment.Center), - horizontalArrangement = Arrangement.SpaceEvenly - ) { - Box( - modifier = Modifier - .padding(10.dp) - .height(20.dp).width(20.dp) - .aspectRatio(1f) - .rotate(offsetAngle) - ) { - Canvas( - modifier = Modifier.height(20.dp).width(20.dp) - ) { - drawArc( - color = Color.Gray, - startAngle = startAngle, - sweepAngle = endAngle - startAngle, - useCenter = false, - style = Stroke(width = 5f, cap = StrokeCap.Round) - ) - } - } - - Text( - modifier = Modifier.padding(10.dp), - text = "Loading...", - color = Color.Gray, - fontWeight = FontWeight.W500, - textAlign = TextAlign.Center - ) - } - } - } - } -} \ No newline at end of file diff --git a/compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/complexlazylist/components/refresh/SwipeProgress.kt b/compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/complexlazylist/components/refresh/SwipeProgress.kt deleted file mode 100644 index 3c8e0486d8762..0000000000000 --- a/compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/complexlazylist/components/refresh/SwipeProgress.kt +++ /dev/null @@ -1,17 +0,0 @@ -package benchmarks.complexlazylist.components.refresh - -import androidx.compose.runtime.Immutable - -internal const val NONE = 0 - -internal const val TOP = 1 - -internal const val BOTTOM = 2 - -@Immutable -internal data class SwipeProgress( - val location: Int = NONE, - val offset: Float = 0f, - /*@FloatRange(from = 0.0, to = 1.0)*/ - val fraction: Float = 0f -) diff --git a/compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/complexlazylist/components/refresh/SwipeRefreshLayout.kt b/compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/complexlazylist/components/refresh/SwipeRefreshLayout.kt deleted file mode 100644 index 6d9142da34ffd..0000000000000 --- a/compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/complexlazylist/components/refresh/SwipeRefreshLayout.kt +++ /dev/null @@ -1,121 +0,0 @@ -package benchmarks.complexlazylist.components.refresh - -import androidx.compose.animation.core.animate -import androidx.compose.animation.core.tween -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.BoxScope -import androidx.compose.foundation.layout.BoxWithConstraints -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.rememberUpdatedState -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.graphicsLayer -import androidx.compose.ui.input.nestedscroll.nestedScroll -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp -import kotlin.math.min - -@Composable -internal fun SwipeRefreshLayout( - state: SwipeRefreshState, - onRefresh: () -> Unit, - onLoadMore: () -> Unit, - modifier: Modifier = Modifier, - refreshTriggerDistance: Dp = 120.dp, - indicationHeight: Dp = 60.dp, - refreshEnabled: Boolean = true, - loadMoreEnabled: Boolean = true, - indicator: @Composable BoxScope.(modifier: Modifier, state: SwipeRefreshState, indicatorHeight: Dp) -> Unit = { m, s, height -> - LoadingIndicatorDefault(m, s, height) - }, - content: @Composable (modifier: Modifier) -> Unit, -) { - val refreshTriggerPx = with(LocalDensity.current) { refreshTriggerDistance.toPx() } - val indicationHeightPx = with(LocalDensity.current) { indicationHeight.toPx() } - - // Our LaunchedEffect, which animates the indicator to its resting position - LaunchedEffect(state.isSwipeInProgress) { - if (!state.isSwipeInProgress) { - // If there's not a swipe in progress, rest the indicator at 0f - state.animateOffsetTo(0f) - } - } - - val coroutineScope = rememberCoroutineScope() - val updatedOnRefresh = rememberUpdatedState(onRefresh) - val updatedOnLoadMore = rememberUpdatedState(onLoadMore) - - val nestedScrollConnection = remember(state, coroutineScope) { - SwipeRefreshNestedScrollConnection( - state, - coroutineScope, - onRefresh = { updatedOnRefresh.value.invoke() }, - onLoadMore = { updatedOnLoadMore.value.invoke() } - ) - }.apply { - this.refreshEnabled = refreshEnabled - this.loadMoreEnabled = loadMoreEnabled - this.refreshTrigger = refreshTriggerPx - this.indicatorHeight = indicationHeightPx - } - - BoxWithConstraints(modifier.nestedScroll(connection = nestedScrollConnection)) { - if (!state.isSwipeInProgress) - LaunchedEffect((state.loadState == REFRESHING || state.loadState == LOADING_MORE)) { - animate( - animationSpec = tween(durationMillis = 300), - initialValue = state.progress.offset, - targetValue = when (state.loadState) { - LOADING_MORE -> indicationHeightPx - REFRESHING -> indicationHeightPx - else -> 0f - } - ) { value, _ -> - if (!state.isSwipeInProgress) { - state.progress = state.progress.copy( - offset = value, - fraction = min(1f, value / refreshTriggerPx) - ) - } - } - } - - val offsetDp = with(LocalDensity.current) { - state.progress.offset.toDp() - } - content( - when (state.progress.location) { - TOP -> Modifier.padding(top = offsetDp) - BOTTOM -> Modifier.padding(bottom = offsetDp) - else -> Modifier - } - ) - if (state.progress.location != NONE) { - Box(modifier = Modifier - .fillMaxWidth() - .height(refreshTriggerDistance) - .graphicsLayer { - translationY = - if (state.progress.location == LOADING_MORE) constraints.maxHeight - state.progress.offset - else state.progress.offset - refreshTriggerPx - } - ) { - indicator( - Modifier.align(if (state.progress.location == TOP) Alignment.BottomStart else Alignment.TopStart), - state, - indicationHeight - ) - } - } - } -} - -@Composable -internal fun rememberSwipeRefreshState(state: Int) = remember { SwipeRefreshState(state) } \ No newline at end of file diff --git a/compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/complexlazylist/components/refresh/SwipeRefreshNestedScrollConnection.kt b/compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/complexlazylist/components/refresh/SwipeRefreshNestedScrollConnection.kt deleted file mode 100644 index 9ce64478ad945..0000000000000 --- a/compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/complexlazylist/components/refresh/SwipeRefreshNestedScrollConnection.kt +++ /dev/null @@ -1,134 +0,0 @@ -package benchmarks.complexlazylist.components.refresh - -import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.input.nestedscroll.NestedScrollConnection -import androidx.compose.ui.input.nestedscroll.NestedScrollSource -import androidx.compose.ui.unit.Velocity -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.launch -import kotlin.math.abs -import kotlin.math.absoluteValue -import kotlin.math.roundToInt - -internal class SwipeRefreshNestedScrollConnection( - private val state: SwipeRefreshState, - private val coroutineScope: CoroutineScope, - private val onRefresh: () -> Unit, - private val onLoadMore: () -> Unit -) : NestedScrollConnection { - var refreshEnabled: Boolean = false - var loadMoreEnabled: Boolean = false - var refreshTrigger: Float = 100f - var indicatorHeight: Float = 50f - - private var isTop = false - private var isBottom = false - - override fun onPreScroll( - available: Offset, - source: NestedScrollSource - ): Offset = when { - !refreshEnabled && !loadMoreEnabled -> Offset.Zero - state.loadState != NORMAL -> Offset.Zero - source == NestedScrollSource.Drag -> { - if (available.y > 0 && isBottom) { - onScroll(available) - } else if (available.y < 0 && isTop) { - onScroll(available) - } else { - Offset.Zero - } - } - else -> Offset.Zero - } - - override fun onPostScroll( - consumed: Offset, - available: Offset, - source: NestedScrollSource - ): Offset { - - if (!refreshEnabled && !loadMoreEnabled) { - return Offset.Zero - } - - else if (state.loadState != NORMAL) { - return Offset.Zero - } else if (source == NestedScrollSource.Drag) { - if (available.y < 0) { - if (!isBottom) { - isBottom = true - } - if (isBottom) { - return onScroll(available) - } - - } else if (available.y > 0) { - if (!isTop) { - isTop = true - } - if (isTop) { - return onScroll(available) - } - } - } - return Offset.Zero - } - - private fun onScroll(available: Offset): Offset { - if (!isBottom && !isTop) { - return Offset.Zero - } - if (available.y > 0 && isTop) { - state.isSwipeInProgress = true - } else if (available.y < 0 && isBottom) { - state.isSwipeInProgress = true - } else if (state.indicatorOffset.roundToInt() == 0) { - state.isSwipeInProgress = false - } - - val newOffset = (available.y + state.indicatorOffset).let { - if (isTop) it.coerceAtLeast(0.0F) else it.coerceAtMost(0.0F) - } - val dragConsumed = newOffset - state.indicatorOffset - - return if (dragConsumed.absoluteValue >= 0.5f) { - coroutineScope.launch { - state.dispatchScrollDelta( - dragConsumed, - if (isTop) TOP else BOTTOM, - refreshTrigger, - ) - } - // Return the consumed Y - Offset(x = 0f, y = dragConsumed) - } else { - Offset.Zero - } - } - - override suspend fun onPreFling(available: Velocity): Velocity { - // If we're dragging, not currently refreshing and scrolled - // past the trigger point, refresh! - if (state.loadState == NORMAL && abs(state.indicatorOffset) >= indicatorHeight) { - if (isTop) { - onRefresh() - } else if (isBottom) { - onLoadMore() - } - } - - // Reset the drag in progress state - state.isSwipeInProgress = false - - // Don't consume any velocity, to allow the scrolling layout to fling - return Velocity.Zero - } - - override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity { - return Velocity.Zero.also { - isTop = false - isBottom = false - } - } -} \ No newline at end of file diff --git a/compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/complexlazylist/components/refresh/SwipeRefreshState.kt b/compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/complexlazylist/components/refresh/SwipeRefreshState.kt deleted file mode 100644 index 5194d64a57000..0000000000000 --- a/compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/complexlazylist/components/refresh/SwipeRefreshState.kt +++ /dev/null @@ -1,79 +0,0 @@ -package benchmarks.complexlazylist.components.refresh - -import androidx.compose.animation.core.Animatable -import androidx.compose.foundation.MutatePriority -import androidx.compose.foundation.MutatorMutex -import androidx.compose.runtime.Stable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.setValue -import kotlin.math.abs -import kotlin.math.min - -internal const val NORMAL = 0 - - -internal const val REFRESHING = 1 - - -internal const val LOADING_MORE = 2 - -@Stable -internal class SwipeRefreshState( - loadState: Int, -) { - private val _indicatorOffset = Animatable(0f) - private val mutatorMutex = MutatorMutex() - - var loadState: Int by mutableStateOf(loadState) - - /** - * Whether a swipe/drag is currently in progress. - */ - var isSwipeInProgress: Boolean by mutableStateOf(false) - internal set - - var progress: SwipeProgress by mutableStateOf(SwipeProgress()) - internal set - - /** - * The current offset for the indicator, in pixels. - */ - internal val indicatorOffset: Float get() = _indicatorOffset.value - - internal suspend fun animateOffsetTo( - offset: Float, - ) { - mutatorMutex.mutate { - _indicatorOffset.animateTo(offset) - } - } - - /** - * Dispatch scroll delta in pixels from touch events. - */ - internal suspend fun dispatchScrollDelta( - delta: Float, - location: Int, - maxOffsetY: Float, - ) { - mutatorMutex.mutate(MutatePriority.UserInput) { - _indicatorOffset.snapTo((_indicatorOffset.value + delta).toFloat()) - updateProgress( - location = location, - maxOffsetY = maxOffsetY, - ) - } - } - - private fun updateProgress( - offsetY: Float = abs(indicatorOffset), - location: Int, - maxOffsetY: Float, - ) { - val offsetPercent = min(1f, offsetY / maxOffsetY) - - val offset = min(maxOffsetY, offsetY) - progress = SwipeProgress(location, offset, offsetPercent) - } -} diff --git a/compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/complexlazylist/models/FollowItem.kt b/compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/complexlazylist/models/FollowItem.kt deleted file mode 100644 index 04949ab8f24ab..0000000000000 --- a/compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/complexlazylist/models/FollowItem.kt +++ /dev/null @@ -1,16 +0,0 @@ -package benchmarks.complexlazylist.models - -class FollowItem() : IBaseViewModel { - override val blockId: String? - get() = TODO("Not yet implemented") - override val reportInfo: Map? - get() = TODO("Not yet implemented") - override val operations: Map? - get() = TODO("Not yet implemented") - override val flipInfos: Map? - get() = TODO("Not yet implemented") - override val extraData: Map? - get() = TODO("Not yet implemented") - override val data: Map? - get() = TODO("Not yet implemented") -} \ No newline at end of file diff --git a/compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/complexlazylist/models/HotItem.kt b/compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/complexlazylist/models/HotItem.kt deleted file mode 100644 index 8c79eb07354a6..0000000000000 --- a/compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/complexlazylist/models/HotItem.kt +++ /dev/null @@ -1,16 +0,0 @@ -package benchmarks.complexlazylist.models - -class HotItem() : IBaseViewModel { - override val blockId: String? - get() = TODO("Not yet implemented") - override val reportInfo: Map? - get() = TODO("Not yet implemented") - override val operations: Map? - get() = TODO("Not yet implemented") - override val flipInfos: Map? - get() = TODO("Not yet implemented") - override val extraData: Map? - get() = TODO("Not yet implemented") - override val data: Map? - get() = TODO("Not yet implemented") -} diff --git a/compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/complexlazylist/models/IBaseViewModel.kt b/compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/complexlazylist/models/IBaseViewModel.kt deleted file mode 100644 index 0c541f6be773c..0000000000000 --- a/compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/complexlazylist/models/IBaseViewModel.kt +++ /dev/null @@ -1,10 +0,0 @@ -package benchmarks.complexlazylist.models - -interface IBaseViewModel { - val blockId: String? - val reportInfo: Map? - val operations: Map? - val flipInfos: Map? - val extraData: Map? - val data: Map? -} \ No newline at end of file diff --git a/compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/complexlazylist/models/ICompositionModel.kt b/compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/complexlazylist/models/ICompositionModel.kt deleted file mode 100644 index 004bb51a0f2eb..0000000000000 --- a/compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/complexlazylist/models/ICompositionModel.kt +++ /dev/null @@ -1,21 +0,0 @@ -package benchmarks.complexlazylist.models - -interface ICompositionItem { - val bgColor: String? - val radius: String? - val alpha: String? - val shadowColor: String? - val textColor: String? - val text: String? -} - -interface ICompositionModel : IBaseViewModel { - val title: String? - val subtitle: String? - val overlyTopLeft: ICompositionItem? - val overlyView1: ICompositionItem? - val overlyView2: ICompositionItem? - val overlyView3: ICompositionItem? - val overlyTopRight: ICompositionItem? - val label: ICompositionItem? -} \ No newline at end of file diff --git a/compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/complexlazylist/models/ViewModelFactory.kt b/compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/complexlazylist/models/ViewModelFactory.kt deleted file mode 100644 index d1fdc8b78277c..0000000000000 --- a/compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/complexlazylist/models/ViewModelFactory.kt +++ /dev/null @@ -1,56 +0,0 @@ -package benchmarks.complexlazylist.models - - -val fakeItem2 = object : ICompositionItem { - override val bgColor: String? - get() = "" - override val radius: String? - get() = "0.5" - override val alpha: String? - get() = "0.5" - override val shadowColor: String? - get() = "" - override val textColor: String? - get() = "" - override val text: String? - get() = "yqhwjmjdsk" + - "fksghksjhgksjh" + - "gsgjlksgjlk" -} - -open class FakeItem : ICompositionModel { - override val title: String? - get() = "aghjagj" - override val subtitle: String? - get() = "ghjghjghj" - override val overlyTopLeft: ICompositionItem? - get() = fakeItem2 - override val overlyView1: ICompositionItem? - get() = fakeItem2 - override val overlyView2: ICompositionItem? - get() = fakeItem2 - override val overlyView3: ICompositionItem? - get() = fakeItem2 - override val overlyTopRight: ICompositionItem? - get() = fakeItem2 - override val label: ICompositionItem? - get() = fakeItem2 - override val blockId: String? - get() = "" - override val reportInfo: Map? - get() = TODO("Not yet implemented") - override val operations: Map? - get() = TODO("Not yet implemented") - override val flipInfos: Map? - get() = TODO("Not yet implemented") - override val extraData: Map? - get() = TODO("Not yet implemented") - override val data: Map? - get() = TODO("Not yet implemented") -} - -fun createFakeItem(): ICompositionModel = FakeItem() - -internal fun fetchCompositionModels(useJSON: Boolean, callback: (List) -> Unit) { - callback(List(250, { createFakeItem() })) -} diff --git a/compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/heavyshader/HeavyShader.kt b/compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/heavyshader/HeavyShader.kt deleted file mode 100644 index 633e6c7ba805c..0000000000000 --- a/compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/heavyshader/HeavyShader.kt +++ /dev/null @@ -1,135 +0,0 @@ -/* - * Copyright 2020-2026 JetBrains s.r.o. and respective authors and developers. - * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. - */ - -package benchmarks.heavyshader - -import androidx.compose.animation.core.LinearEasing -import androidx.compose.animation.core.RepeatMode -import androidx.compose.animation.core.animateFloat -import androidx.compose.animation.core.infiniteRepeatable -import androidx.compose.animation.core.rememberInfiniteTransition -import androidx.compose.animation.core.tween -import androidx.compose.foundation.Canvas -import androidx.compose.foundation.background -import androidx.compose.foundation.gestures.scrollBy -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.runtime.withFrameMillis -import androidx.compose.ui.Modifier -import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.graphics.BlendMode -import androidx.compose.ui.graphics.Brush -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.unit.dp -import kotlin.math.PI -import kotlin.math.cos -import kotlin.math.sin -import kotlinx.coroutines.isActive - -private const val ITEM_COUNT = 800 - -@Composable -fun HeavyShader() { - val listState = rememberLazyListState() - var scrollForward by remember { mutableStateOf(true) } - - LaunchedEffect(Unit) { - while (isActive) { - withFrameMillis { } - val currentItem = listState.firstVisibleItemIndex - if (currentItem == 0) scrollForward = true - if (currentItem > ITEM_COUNT - 100) scrollForward = false - listState.scrollBy(if (scrollForward) 33f else -33f) - } - } - - LazyColumn( - state = listState, - modifier = Modifier.fillMaxSize().background(Color.Black) - ) { - items(ITEM_COUNT) { - HeavyShaderItem() - } - } -} - -@Composable -private fun HeavyShaderItem() { - val time by rememberInfiniteTransition().animateFloat( - initialValue = 0f, - targetValue = 100f, - animationSpec = infiniteRepeatable( - animation = tween(5000, easing = LinearEasing), - repeatMode = RepeatMode.Restart - ) - ) - - Canvas( - modifier = Modifier - .fillMaxWidth() - .height(120.dp) - .padding(8.dp) - ) { - val layers = 60 - - repeat(layers) { layer -> - val offset = layer * 10f - val alpha = 1f - (layer.toFloat() / layers) - - drawRect( - brush = Brush.radialGradient( - colors = listOf( - Color(0xFF00FF00).copy(alpha = alpha), - Color(0xFF0000FF).copy(alpha = alpha * 0.7f), - Color(0xFFFF0000).copy(alpha = alpha * 0.5f), - Color(0xFFFFFF00).copy(alpha = alpha * 0.3f), - Color(0xFF00FFFF).copy(alpha = alpha * 0.1f) - ), - center = Offset( - size.width / 2 + cos(time + layer).toFloat() * 150f, - size.height / 2 + sin(time + layer).toFloat() * 150f - ), - radius = size.minDimension / 2 + offset - ), - blendMode = when (layer % 3) { - 0 -> BlendMode.Screen - 1 -> BlendMode.Overlay - else -> BlendMode.Multiply - } - ) - } - - repeat(60) { i -> - val angle = i * 7.2f + time * 10 - val distance = size.minDimension / 2 - val x = size.width / 2 + cos(angle * PI / 180.0).toFloat() * distance - val y = size.height / 2 + sin(angle * PI / 180.0).toFloat() * distance - - drawCircle( - brush = Brush.radialGradient( - colors = listOf( - Color.White.copy(alpha = 0.8f), - Color.Transparent - ), - center = Offset(x, y), - radius = 30f - ), - radius = 30f, - center = Offset(x, y), - blendMode = BlendMode.Plus - ) - } - } -} diff --git a/compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/lazygrid/LazyGrid.kt b/compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/lazygrid/LazyGrid.kt deleted file mode 100644 index a10fafe31b38e..0000000000000 --- a/compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/lazygrid/LazyGrid.kt +++ /dev/null @@ -1,97 +0,0 @@ -package benchmarks.lazygrid - -import androidx.compose.foundation.gestures.scrollBy -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.grid.GridCells -import androidx.compose.foundation.lazy.grid.LazyVerticalGrid -import androidx.compose.foundation.lazy.grid.items -import androidx.compose.foundation.lazy.grid.rememberLazyGridState -import androidx.compose.material.Card -import androidx.compose.material.MaterialTheme -import androidx.compose.material.Text -import androidx.compose.runtime.* -import androidx.compose.ui.Modifier -import androidx.compose.ui.semantics.contentDescription -import androidx.compose.ui.semantics.semantics -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp -import kotlinx.coroutines.isActive -import kotlin.coroutines.suspendCoroutine - -@Composable -fun LazyGrid(smoothScroll: Boolean = false, withLaunchedEffectInItem: Boolean = false) { - val itemCount = 12000 - val entries = remember {List(itemCount) { Entry("$it") }} - val state = rememberLazyGridState() - - MaterialTheme { - Column { - LazyVerticalGrid( - columns = GridCells.Fixed(4), - modifier = Modifier.fillMaxWidth().semantics { contentDescription = "IamLazy" }, - state = state - ) { - items(entries) { - ListCell(it, withLaunchedEffectInItem) - } - } - } - - } - - var curItem by remember { mutableStateOf(0) } - var direct by remember { mutableStateOf(true) } - if (smoothScroll) { - LaunchedEffect(Unit) { - while (isActive) { - withFrameMillis { } - curItem = state.firstVisibleItemIndex - if (curItem == 0) direct = true - if (curItem > itemCount - 100) direct = false - state.scrollBy(if (direct) 55f else -55f) - } - } - } else { - LaunchedEffect(Unit) { - while(isActive) { - withFrameMillis {} - curItem += if (direct) 50 else -50 - if (curItem >= itemCount) { - direct = false - curItem = itemCount - 1 - } else if (curItem <= 0) { - direct = true - curItem = 0 - } - state.scrollToItem(curItem) - } - } - } -} - -data class Entry(val contents: String) - -@Composable -private fun ListCell(entry: Entry, withLaunchedEffect: Boolean = false) { - Card( - modifier = Modifier - .fillMaxWidth() - .padding(8.dp) - ) { - Text( - text = entry.contents, - textAlign = TextAlign.Center, - style = MaterialTheme.typography.h5, - modifier = Modifier.padding(16.dp) - ) - - if (withLaunchedEffect) { - LaunchedEffect(Unit) { - // Never resumed to imitate some async task running in an item's scope - suspendCoroutine { } - } - } - } -} \ No newline at end of file diff --git a/compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/multipleComponents/Clickable.kt b/compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/multipleComponents/Clickable.kt deleted file mode 100644 index b8b364d6cc43f..0000000000000 --- a/compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/multipleComponents/Clickable.kt +++ /dev/null @@ -1,362 +0,0 @@ -/* - * Copyright 2021 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -// copy of https://github.com/JetBrains/compose-multiplatform-core/blob/d9e875b62e7bb4dd47b6b155d3a787251ff5bd38/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/Clickable.desktop.kt#L78 -// inefficient implementation of clickable for benchmarking purposes (Modifier.Node variant in AOSP is more optimized) - -package benchmarks.multipleComponents - -import androidx.compose.foundation.Indication -import androidx.compose.foundation.focusable -import androidx.compose.foundation.gestures.awaitEachGesture -import androidx.compose.foundation.hoverable -import androidx.compose.foundation.indication -import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.interaction.PressInteraction -import androidx.compose.runtime.Immutable -import androidx.compose.runtime.State -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.rememberUpdatedState -import androidx.compose.ui.Modifier -import androidx.compose.ui.composed -import androidx.compose.ui.focus.FocusRequester -import androidx.compose.ui.focus.focusRequester -import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.input.key.Key -import androidx.compose.ui.input.key.KeyEvent -import androidx.compose.ui.input.key.KeyEventType.Companion.KeyDown -import androidx.compose.ui.input.key.KeyEventType.Companion.KeyUp -import androidx.compose.ui.input.key.key -import androidx.compose.ui.input.key.onKeyEvent -import androidx.compose.ui.input.key.type -import androidx.compose.ui.input.pointer.AwaitPointerEventScope -import androidx.compose.ui.input.pointer.PointerButtons -import androidx.compose.ui.input.pointer.PointerEvent -import androidx.compose.ui.input.pointer.PointerEventPass -import androidx.compose.ui.input.pointer.PointerInputScope -import androidx.compose.ui.input.pointer.PointerKeyboardModifiers -import androidx.compose.ui.input.pointer.changedToDown -import androidx.compose.ui.input.pointer.changedToUp -import androidx.compose.ui.input.pointer.isOutOfBounds -import androidx.compose.ui.input.pointer.pointerInput -import androidx.compose.ui.input.pointer.positionChangeConsumed -import androidx.compose.ui.node.ModifierNodeElement -import androidx.compose.ui.node.SemanticsModifierNode -import androidx.compose.ui.platform.InspectorInfo -import androidx.compose.ui.platform.debugInspectorInfo -import androidx.compose.ui.semantics.Role -import androidx.compose.ui.semantics.SemanticsPropertyReceiver -import androidx.compose.ui.semantics.disabled -import androidx.compose.ui.semantics.onClick -import androidx.compose.ui.semantics.onLongClick -import androidx.compose.ui.unit.center -import androidx.compose.ui.unit.toOffset -import androidx.compose.ui.util.fastAll -import androidx.compose.ui.util.fastAny -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.launch - -@Immutable -class MouseClickScope( - val buttons: PointerButtons, - val keyboardModifiers: PointerKeyboardModifiers -) - -@Suppress("DEPRECATION") -internal val EmptyClickContext = MouseClickScope( - PointerButtons(0), PointerKeyboardModifiers(0) -) - -fun Modifier.mouseClickable( - enabled: Boolean = true, - onClickLabel: String? = null, - role: Role? = null, - @Suppress("DEPRECATION") onClick: MouseClickScope.() -> Unit -) = composed( - factory = { - val onClickState = rememberUpdatedState(onClick) - val centreOffset = remember { mutableStateOf(Offset.Zero) } - val currentKeyPressInteractions = remember { mutableMapOf() } - val focusRequester = remember { FocusRequester() } - val gesture = if (enabled) { - Modifier.pointerInput(Unit) { - centreOffset.value = size.center.toOffset() - detectTapWithContext( - onTap = { down, _ -> - focusRequester.requestFocus() - onClickState.value.invoke( - MouseClickScope( - down.buttons, - down.keyboardModifiers - ) - ) - } - ) - } - } else { - Modifier - } - Modifier - .focusRequester(focusRequester) - .genericClickableWithoutGesture( - interactionSource = remember { MutableInteractionSource() }, - indication = null, - indicationScope = rememberCoroutineScope(), - keyClickOffset = centreOffset, - enabled = enabled, - onClickLabel = onClickLabel, - currentKeyPressInteractions = currentKeyPressInteractions, - role = role, - onLongClickLabel = null, - onLongClick = null, - onClick = { onClick(EmptyClickContext) } - ) - .then(gesture) - }, - inspectorInfo = debugInspectorInfo { - name = "clickable" - properties["enabled"] = enabled - properties["onClickLabel"] = onClickLabel - properties["role"] = role - properties["onClick"] = onClick - } -) - -internal suspend fun PointerInputScope.detectTapWithContext( - onTap: ((PointerEvent, PointerEvent) -> Unit)? = null -) { - awaitEachGesture { - val down = awaitEventFirstDown().also { - it.changes.forEach { it.consume() } - } - - val up = waitForFirstInboundUpOrCancellation() - if (up != null) { - up.changes.forEach { it.consume() } - onTap?.invoke(down, up) - } - } -} - -private suspend fun AwaitPointerEventScope.awaitEventFirstDown(): PointerEvent { - var event: PointerEvent - do { - event = awaitPointerEvent() - } while ( - !event.changes.fastAll { it.changedToDown() } - ) - return event -} - -private suspend fun AwaitPointerEventScope.waitForFirstInboundUpOrCancellation(): PointerEvent? { - while (true) { - val event = awaitPointerEvent(PointerEventPass.Main) - if (event.changes.fastAll { it.changedToUp() }) { - // All pointers are up - return event - } - - if (event.changes.fastAny { - it.consumed.downChange || it.isOutOfBounds(size, extendedTouchPadding) - } - ) { - return null // Canceled - } - - // Check for cancel by position consumption. We can look on the Final pass of the - // existing pointer event because it comes after the Main pass we checked above. - val consumeCheck = awaitPointerEvent(PointerEventPass.Final) - if (consumeCheck.changes.fastAny { it.positionChangeConsumed() }) { - return null - } - } -} - -internal fun Modifier.genericClickableWithoutGesture( - interactionSource: MutableInteractionSource, - indication: Indication?, - indicationScope: CoroutineScope, - currentKeyPressInteractions: MutableMap, - keyClickOffset: State, - enabled: Boolean = true, - onClickLabel: String? = null, - role: Role? = null, - onLongClickLabel: String? = null, - onLongClick: (() -> Unit)? = null, - onClick: () -> Unit -): Modifier { - fun Modifier.detectPressAndClickFromKey() = this.onKeyEvent { keyEvent -> - when { - enabled && keyEvent.isPress -> { - // If the key already exists in the map, keyEvent is a repeat event. - // We ignore it as we only want to emit an interaction for the initial key press. - if (!currentKeyPressInteractions.containsKey(keyEvent.key)) { - val press = PressInteraction.Press(keyClickOffset.value) - currentKeyPressInteractions[keyEvent.key] = press - indicationScope.launch { interactionSource.emit(press) } - true - } else { - false - } - } - enabled && keyEvent.isClick -> { - currentKeyPressInteractions.remove(keyEvent.key)?.let { - indicationScope.launch { - interactionSource.emit(PressInteraction.Release(it)) - } - } - onClick() - true - } - else -> false - } - } - return this then - ClickableSemanticsElement( - enabled = enabled, - role = role, - onLongClickLabel = onLongClickLabel, - onLongClick = onLongClick, - onClickLabel = onClickLabel, - onClick = onClick - ) - .detectPressAndClickFromKey() - .indication(interactionSource, indication) - .hoverable(enabled = enabled, interactionSource = interactionSource) - .focusable(enabled = enabled, interactionSource = interactionSource) -} - -private class ClickableSemanticsElement( - private val enabled: Boolean, - private val role: Role?, - private val onLongClickLabel: String?, - private val onLongClick: (() -> Unit)?, - private val onClickLabel: String?, - private val onClick: () -> Unit -) : ModifierNodeElement() { - override fun create() = ClickableSemanticsNode( - enabled = enabled, - role = role, - onLongClickLabel = onLongClickLabel, - onLongClick = onLongClick, - onClickLabel = onClickLabel, - onClick = onClick - ) - - override fun update(node: ClickableSemanticsNode) { - node.update(enabled, onClickLabel, role, onClick, onLongClickLabel, onLongClick) - } - - override fun InspectorInfo.inspectableProperties() = Unit - - override fun hashCode(): Int { - var result = enabled.hashCode() - result = 31 * result + role.hashCode() - result = 31 * result + onLongClickLabel.hashCode() - result = 31 * result + onLongClick.hashCode() - result = 31 * result + onClickLabel.hashCode() - result = 31 * result + onClick.hashCode() - return result - } - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (other !is ClickableSemanticsElement) return false - - if (enabled != other.enabled) return false - if (role != other.role) return false - if (onLongClickLabel != other.onLongClickLabel) return false - if (onLongClick !== other.onLongClick) return false - if (onClickLabel != other.onClickLabel) return false - if (onClick !== other.onClick) return false - - return true - } -} - -internal class ClickableSemanticsNode( - private var enabled: Boolean, - private var onClickLabel: String?, - private var role: Role?, - private var onClick: () -> Unit, - private var onLongClickLabel: String?, - private var onLongClick: (() -> Unit)?, -) : SemanticsModifierNode, Modifier.Node() { - fun update( - enabled: Boolean, - onClickLabel: String?, - role: Role?, - onClick: () -> Unit, - onLongClickLabel: String?, - onLongClick: (() -> Unit)?, - ) { - this.enabled = enabled - this.onClickLabel = onClickLabel - this.role = role - this.onClick = onClick - this.onLongClickLabel = onLongClickLabel - this.onLongClick = onLongClick - } - - override val shouldMergeDescendantSemantics: Boolean - get() = true - - override fun SemanticsPropertyReceiver.applySemantics() { - if (this@ClickableSemanticsNode.role != null) { - role = this@ClickableSemanticsNode.role!! - } - onClick( - action = { onClick(); true }, - label = onClickLabel - ) - if (onLongClick != null) { - onLongClick( - action = { onLongClick?.invoke(); true }, - label = onLongClickLabel - ) - } - if (!enabled) { - disabled() - } - } -} - - -/** - * Whether the specified [KeyEvent] should trigger a press for a clickable component, i.e. whether - * it is associated with a press of an enter key or dpad centre. - */ -private val KeyEvent.isPress: Boolean - get() = type == KeyDown && isEnter - -/** - * Whether the specified [KeyEvent] should trigger a click for a clickable component, i.e. whether - * it is associated with a release of an enter key or dpad centre. - */ -private val KeyEvent.isClick: Boolean - get() = type == KeyUp && isEnter - -private val KeyEvent.isEnter: Boolean - get() = - when (key) { - Key.DirectionCenter, - Key.Enter, - Key.NumPadEnter, - Key.Spacebar -> true - else -> false - } diff --git a/compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/multipleComponents/MenuIcon.kt b/compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/multipleComponents/MenuIcon.kt deleted file mode 100644 index fba042afa95d3..0000000000000 --- a/compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/multipleComponents/MenuIcon.kt +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package benchmarks.multipleComponents - -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.SolidColor -import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.graphics.vector.path -import androidx.compose.ui.unit.dp - -internal val MenuIcon: ImageVector = ImageVector.Builder( - name = "Menu", - defaultWidth = 24.dp, - defaultHeight = 24.dp, - viewportWidth = 24f, - viewportHeight = 24f -).apply { - path(fill = SolidColor(Color.Black)) { - moveTo(3f, 18f) - horizontalLineTo(21f) - verticalLineTo(16f) - horizontalLineTo(3f) - close() - moveTo(3f, 13f) - horizontalLineTo(21f) - verticalLineTo(11f) - horizontalLineTo(3f) - close() - moveTo(3f, 6f) - verticalLineTo(8f) - horizontalLineTo(21f) - verticalLineTo(6f) - close() - } -}.build() diff --git a/compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/multipleComponents/MultipleComponents.kt b/compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/multipleComponents/MultipleComponents.kt deleted file mode 100644 index e5066ecd6c5bf..0000000000000 --- a/compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/multipleComponents/MultipleComponents.kt +++ /dev/null @@ -1,662 +0,0 @@ -/* - * Copyright 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -// copy of https://github.com/JetBrains/compose-multiplatform-core/blob/d9e875b62e7bb4dd47b6b155d3a787251ff5bd38/compose/desktop/desktop/samples/src/jvmMain/kotlin/androidx/compose/desktop/examples/example1/Main.jvm.kt#L17 - -package benchmarks.multipleComponents - -import androidx.compose.animation.animateColorAsState -import androidx.compose.animation.core.TweenSpec -import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.Image -import androidx.compose.foundation.ScrollState -import androidx.compose.foundation.background -import androidx.compose.foundation.border -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxHeight -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.offset -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.layout.wrapContentSize -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.foundation.text.InlineTextContent -import androidx.compose.foundation.text.appendInlineContent -import androidx.compose.foundation.text.selection.SelectionContainer -import androidx.compose.foundation.verticalScroll -import androidx.compose.material.BottomAppBar -import androidx.compose.material.Button -import androidx.compose.material.ButtonDefaults -import androidx.compose.material.Checkbox -import androidx.compose.material.CircularProgressIndicator -import androidx.compose.material.DropdownMenu -import androidx.compose.material.DropdownMenuItem -import androidx.compose.material.ExtendedFloatingActionButton -import androidx.compose.material.Icon -import androidx.compose.material.IconButton -import androidx.compose.material.MaterialTheme -import androidx.compose.material.Scaffold -import androidx.compose.material.Slider -import androidx.compose.material.Switch -import androidx.compose.material.Text -import androidx.compose.material.TextField -import androidx.compose.material.TopAppBar -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.produceState -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.ExperimentalComposeUiApi -import androidx.compose.ui.Modifier -import androidx.compose.ui.focus.FocusRequester -import androidx.compose.ui.focus.focusProperties -import androidx.compose.ui.focus.focusRequester -import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.graphics.Brush -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.Color.Companion.Black -import androidx.compose.ui.graphics.Color.Companion.Blue -import androidx.compose.ui.graphics.Color.Companion.Green -import androidx.compose.ui.graphics.Color.Companion.Red -import androidx.compose.ui.graphics.Color.Companion.Yellow -import androidx.compose.ui.graphics.ImageBitmap -import androidx.compose.ui.graphics.ImageShader -import androidx.compose.ui.graphics.ShaderBrush -import androidx.compose.ui.graphics.Shadow -import androidx.compose.ui.graphics.TileMode -import androidx.compose.ui.graphics.graphicsLayer -import androidx.compose.ui.input.key.Key -import androidx.compose.ui.input.key.isMetaPressed -import androidx.compose.ui.input.key.isShiftPressed -import androidx.compose.ui.input.key.key -import androidx.compose.ui.input.key.onPreviewKeyEvent -import androidx.compose.ui.input.pointer.PointerEventType -import androidx.compose.ui.input.pointer.PointerIcon -import androidx.compose.ui.input.pointer.isAltPressed -import androidx.compose.ui.input.pointer.isBackPressed -import androidx.compose.ui.input.pointer.isCtrlPressed -import androidx.compose.ui.input.pointer.isForwardPressed -import androidx.compose.ui.input.pointer.isMetaPressed -import androidx.compose.ui.input.pointer.isPrimaryPressed -import androidx.compose.ui.input.pointer.isSecondaryPressed -import androidx.compose.ui.input.pointer.isShiftPressed -import androidx.compose.ui.input.pointer.isTertiaryPressed -import androidx.compose.ui.input.pointer.pointerInput -import androidx.compose.ui.input.pointer.pointerHoverIcon -import androidx.compose.ui.platform.LocalUriHandler -import androidx.compose.ui.text.ExperimentalTextApi -import androidx.compose.ui.text.Placeholder -import androidx.compose.ui.text.PlaceholderVerticalAlign -import androidx.compose.ui.text.SpanStyle -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.buildAnnotatedString -import androidx.compose.ui.text.font.FontFamily -import androidx.compose.ui.text.font.FontStyle -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.text.style.TextDecoration -import androidx.compose.ui.text.style.TextDecoration.Companion.Underline -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.em -import androidx.compose.ui.unit.sp -import compose_benchmarks.benchmarks.generated.resources.Res -import compose_benchmarks.benchmarks.generated.resources.example1_cat -import compose_benchmarks.benchmarks.generated.resources.example1_ic_call_answer -import compose_benchmarks.benchmarks.generated.resources.example1_sailing -import compose_benchmarks.benchmarks.generated.resources.jetbrainsmono_italic -import compose_benchmarks.benchmarks.generated.resources.jetbrainsmono_regular -import org.jetbrains.compose.resources.ExperimentalResourceApi -import org.jetbrains.compose.resources.Font -import org.jetbrains.compose.resources.InternalResourceApi -import org.jetbrains.compose.resources.decodeToImageBitmap -import org.jetbrains.compose.resources.painterResource -import kotlin.random.Random - -@Composable -fun MultipleComponentsExample(isVectorGraphicsSupported: Boolean = true) { - val uriHandler = LocalUriHandler.current - MaterialTheme { - Scaffold( - topBar = { - TopAppBar( - title = { - Row(verticalAlignment = Alignment.CenterVertically) { - if (isVectorGraphicsSupported) { - Image( - painterResource(Res.drawable.example1_sailing), - contentDescription = "Star" - ) - } - Text("Desktop Compose Elements") - } - } - ) - }, - floatingActionButton = { - ExtendedFloatingActionButton( - text = { Text("Open URL") }, - onClick = { - uriHandler.openUri("https://google.com") - } - ) - }, - isFloatingActionButtonDocked = true, - bottomBar = { - BottomAppBar(cutoutShape = CircleShape) { - IconButton( - onClick = {} - ) { - Icon(MenuIcon, "Menu", Modifier.size(ButtonDefaults.IconSize)) - } - } - }, - content = { innerPadding -> - Row(Modifier.padding(innerPadding)) { - LeftColumn(Modifier.weight(1f)) - MiddleColumn(Modifier.width(500.dp), isVectorGraphicsSupported = isVectorGraphicsSupported) - RightColumn(Modifier.width(200.dp)) - } - } - ) - } -} - -@Composable -private fun LeftColumn(modifier: Modifier) = Box(modifier.fillMaxSize()) { - val state = rememberScrollState() - ScrollableContent(state) -} - -@OptIn(ExperimentalComposeUiApi::class) -@Composable -private fun ScrollableContent(scrollState: ScrollState) { - val italicFont = FontFamily( - Font(Res.font.jetbrainsmono_italic) - ) - - val dispatchedFonts = FontFamily( - Font(Res.font.jetbrainsmono_regular, style = FontStyle.Italic), - Font(Res.font.jetbrainsmono_italic, style = FontStyle.Normal) - ) - - val amount = remember { mutableStateOf(0f) } - val animation = remember { mutableStateOf(true) } - Column(Modifier.fillMaxSize().verticalScroll(scrollState)) { - Text( - text = "Привет! 你好! Desktop Compose use: ${amount.value}", - color = Color.Black, - modifier = Modifier - .background(Color.Blue) - .height(56.dp) - .wrapContentSize(Alignment.Center) - ) - - val inlineIndicatorId = "indicator" - - Text( - text = buildAnnotatedString { - append("The quick ") - if (animation.value) { - appendInlineContent(inlineIndicatorId) - } - pushStyle( - SpanStyle( - color = Color(0xff964B00), - shadow = Shadow(Color.Green, offset = Offset(1f, 1f)) - ) - ) - append("brown fox") - pop() - pushStyle(SpanStyle(background = Color.Yellow)) - append(" 🦊 ate a ") - pop() - pushStyle(SpanStyle(fontSize = 30.sp, textDecoration = Underline)) - append("zesty hamburgerfons") - pop() - append(" 🍔.\nThe 👩‍👩‍👧‍👧 laughed.") - addStyle(SpanStyle(color = Color.Green), 25, 35) - }, - color = Color.Black, - inlineContent = mapOf( - inlineIndicatorId to InlineTextContent( - Placeholder( - width = 1.em, - height = 1.em, - placeholderVerticalAlign = PlaceholderVerticalAlign.AboveBaseline - ) - ) { - CircularProgressIndicator(Modifier.padding(end = 3.dp)) - } - ) - ) - - val loremColors = listOf( - Color.Black, - Color.Yellow, - Color.Green, - Color.Blue - ) - var loremColor by remember { mutableStateOf(0) } - - val loremDecorations = listOf( - TextDecoration.None, - TextDecoration.Underline, - TextDecoration.LineThrough - ) - val lorem = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do" + - " eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad" + - " minim veniam, quis nostrud exercitation ullamco laboris nisi ut" + - " aliquipex ea commodo consequat. Duis aute irure dolor in reprehenderit" + - " in voluptate velit esse cillum dolore eu fugiat nulla pariatur." + - " Excepteur" + - " sint occaecat cupidatat non proident, sunt in culpa qui officia" + - " deserunt mollit anim id est laborum." - var loremDecoration by remember { mutableStateOf(0) } - Text( - text = lorem, - color = loremColors[loremColor], - textAlign = TextAlign.Center, - textDecoration = loremDecorations[loremDecoration], - modifier = Modifier.clickable { - if (loremColor < loremColors.size - 1) { - loremColor += 1 - } else { - loremColor = 0 - } - - if (loremDecoration < loremDecorations.size - 1) { - loremDecoration += 1 - } else { - loremDecoration = 0 - } - } - ) - - Text( - text = lorem, - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) - Row { - Box(modifier = Modifier.size(200.dp, 200.dp).border(1.dp, Black)) { - BrushTextGradient(lorem) - } - Box(modifier = Modifier.size(200.dp, 200.dp).border(1.dp, Black)) { - BrushTextImage(lorem) - } - } - - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceEvenly - ) { - Text( - "Default", - ) - - Text( - "SansSerif", - fontFamily = FontFamily.SansSerif - ) - - Text( - "Serif", - fontFamily = FontFamily.Serif - ) - - Text( - "Monospace", - fontFamily = FontFamily.Monospace - ) - - Text( - "Cursive", - fontFamily = FontFamily.Cursive - ) - } - - var overText by remember { mutableStateOf("Move mouse over text:") } - Text(overText, style = TextStyle(letterSpacing = 10.sp)) - - SelectionContainer { - Text( - text = "fun > List.quickSort(): List = when {\n" + - " size < 2 -> this\n" + - " else -> {\n" + - " val pivot = first()\n" + - " val (smaller, greater) = drop(1).partition { it <= pivot }\n" + - " smaller.quickSort() + pivot + greater.quickSort()\n" + - " }\n" + - "}", - fontFamily = italicFont, - modifier = Modifier - .padding(10.dp) - .pointerInput(Unit) { - awaitPointerEventScope { - while (true) { - val event = awaitPointerEvent() - when (event.type) { - PointerEventType.Move -> { - val position = event.changes.first().position - overText = "Move position: $position" - } - PointerEventType.Enter -> overText = "Over enter" - PointerEventType.Exit -> overText = "Over exit" - } - } - } - } - ) - } - Text( - text = buildAnnotatedString { - append("resolved: NotoSans-Regular.ttf ") - pushStyle( - SpanStyle( - fontStyle = FontStyle.Italic - ) - ) - append("NotoSans-italic.ttf.") - }, - fontFamily = dispatchedFonts, - ) - - Row(verticalAlignment = Alignment.CenterVertically) { - Button( - modifier = Modifier.padding(4.dp).pointerHoverIcon(PointerIcon.Hand), - onClick = { - amount.value++ - } - ) { - Text("Base") - } - - var clickableText by remember { mutableStateOf("Click me!") } - Text( - modifier = Modifier.mouseClickable( - onClick = { - clickableText = buildString { - append("Buttons pressed:\n") - append("primary: ${buttons.isPrimaryPressed}\t") - append("secondary: ${buttons.isSecondaryPressed}\t") - append("tertiary: ${buttons.isTertiaryPressed}\t") - append("primary: ${buttons.isPrimaryPressed}\t") - append("back: ${buttons.isBackPressed}\t") - append("forward: ${buttons.isForwardPressed}\t") - - append("\n\nKeyboard modifiers pressed:\n") - - append("alt: ${keyboardModifiers.isAltPressed}\t") - append("ctrl: ${keyboardModifiers.isCtrlPressed}\t") - append("meta: ${keyboardModifiers.isMetaPressed}\t") - append("shift: ${keyboardModifiers.isShiftPressed}\t") - } - } - ), - text = clickableText - ) - } - - Row( - modifier = Modifier.padding(vertical = 10.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Row { - Column { - Switch( - animation.value, - onCheckedChange = { - animation.value = it - } - ) - Row( - modifier = Modifier.padding(4.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Checkbox( - animation.value, - onCheckedChange = { - animation.value = it - } - ) - Text("Animation") - } - } - - Button( - modifier = Modifier.padding(4.dp), - onClick = { } - ) { - Text("Window") - } - } - - Animations(isCircularEnabled = animation.value) - } - - Slider( - value = amount.value / 100f, - onValueChange = { amount.value = (it * 100) } - ) - val dropDownMenuExpanded = remember { mutableStateOf(false) } - Button(onClick = { dropDownMenuExpanded.value = true }) { - Text("Expand Menu") - } - DropdownMenu( - expanded = dropDownMenuExpanded.value, - onDismissRequest = { - dropDownMenuExpanded.value = false - println("OnDismissRequest") - } - ) { - DropdownMenuItem(modifier = Modifier, onClick = { - println("Item 1 clicked") - }) { - Text("Item 1") - } - DropdownMenuItem(modifier = Modifier, onClick = { - println("Item 2 clicked") - }) { - Text("Item 2") - } - DropdownMenuItem(modifier = Modifier, onClick = { - println("Item 3 clicked") - }) { - Text("Item 3") - } - } - TextField( - value = amount.value.toString(), - onValueChange = { amount.value = it.toFloatOrNull() ?: 42f }, - label = { Text(text = "Input1") } - ) - - } -} - -@Composable -fun MiddleColumn(modifier: Modifier, isVectorGraphicsSupported: Boolean) = Column(modifier) { - val (focusItem1, focusItem2) = FocusRequester.createRefs() - val text = remember { - mutableStateOf("Hello \uD83E\uDDD1\uD83C\uDFFF\u200D\uD83E\uDDB0") - } - - TextField( - value = text.value, - onValueChange = { text.value = it }, - label = { Text(text = "Input2") }, - placeholder = { - Text(text = "Important input") - }, - maxLines = 1, - modifier = Modifier.onPreviewKeyEvent { - when { - (it.isMetaPressed && it.key == Key.Enter) -> { - if (it.isShiftPressed) { - text.value = "Cleared with shift!" - } else { - text.value = "Cleared!" - } - true - } - else -> false - } - }.focusRequester(focusItem1) - .focusProperties { - next = focusItem2 - } - ) - - var text2 by remember { - val initText = buildString { - (1..1000).forEach { - append("$it\n") - } - } - mutableStateOf(initText) - } - TextField( - text2, - modifier = Modifier - .height(200.dp) - .focusRequester(focusItem2) - .focusProperties { - previous = focusItem1 - }, - onValueChange = { text2 = it } - ) - - Row { - Image( - painterResource(Res.drawable.example1_cat), - "Localized description", - Modifier.size(200.dp) - ) - - if (isVectorGraphicsSupported) { - Icon( - painterResource(Res.drawable.example1_ic_call_answer), - "Localized description", - Modifier.size(100.dp).align(Alignment.CenterVertically), - tint = Color.Blue.copy(alpha = 0.5f) - ) - } - } - - Box( - modifier = Modifier.size(150.dp).background(Color.Gray).pointerHoverIcon( - PointerIcon.Hand - ) - ) { - Box( - modifier = Modifier.offset(20.dp, 20.dp).size(100.dp).background(Color.Blue).pointerHoverIcon( - PointerIcon.Crosshair - ) - ) { - Text("pointerHoverIcon test with Ctrl") - } - } -} - -@Composable -fun Animations(isCircularEnabled: Boolean) = Row { - if (isCircularEnabled) { - CircularProgressIndicator(Modifier.padding(10.dp)) - } - - val enabled = remember { mutableStateOf(true) } - val color by animateColorAsState( - if (enabled.value) Color.Green else Color.Red, - animationSpec = TweenSpec(durationMillis = 2000) - ) - - MaterialTheme { - Box( - Modifier - .size(70.dp) - .clickable { enabled.value = !enabled.value } - .background(color) - ) - } -} - -@OptIn(ExperimentalFoundationApi::class) -@Composable -private fun RightColumn(modifier: Modifier) = Box(modifier) { - val state = rememberLazyListState() - val itemCount = 100000 - val heights = remember { - val random = Random(24) - (0 until itemCount).map { random.nextFloat() } - } - - LazyColumn(Modifier.fillMaxSize().graphicsLayer(alpha = 0.5f), state = state) { - items((0 until itemCount).toList()) { i -> - val itemHeight = 20.dp + 20.dp * heights[i] - Text(i.toString(), Modifier.graphicsLayer(alpha = 0.5f).height(itemHeight)) - } - } - -} - -@OptIn(ExperimentalTextApi::class) -@Composable -fun BrushTextGradient(text: String) { - Text( - text = text, - style = TextStyle( - fontSize = 12.sp, - fontWeight = FontWeight.Bold, - brush = Brush.linearGradient( - colors = listOf(Red, Yellow, Green, Blue) - ) - ) - ) -} - -@OptIn(ExperimentalTextApi::class, ExperimentalResourceApi::class, InternalResourceApi::class) -@Composable -fun BrushTextImage(text: String) { - val image: ImageBitmap? by produceState(null) { - value = Res.readBytes("files/example1_compose-community-primary.png").decodeToImageBitmap() - } - if (image != null) { - Text( - text = text, - style = TextStyle( - fontSize = 12.sp, - fontWeight = FontWeight.Bold, - brush = ShaderBrush( - ImageShader(image!!, TileMode.Repeated, TileMode.Repeated) - ) - ) - ) - } -} \ No newline at end of file diff --git a/compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/textlayout/TextLayout.kt b/compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/textlayout/TextLayout.kt deleted file mode 100644 index c0e3efff09c92..0000000000000 --- a/compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/textlayout/TextLayout.kt +++ /dev/null @@ -1,117 +0,0 @@ -/* - * Copyright 2020-2026 JetBrains s.r.o. and respective authors and developers. - * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. - */ - -package benchmarks.textlayout - -import androidx.compose.foundation.background -import androidx.compose.foundation.gestures.scrollBy -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.runtime.withFrameMillis -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import kotlin.random.Random -import kotlinx.coroutines.isActive - -private const val ITEM_COUNT = 2000 - -@Composable -fun TextLayout() { - val listState = rememberLazyListState() - var frame by remember { mutableStateOf(0) } - var scrollForward by remember { mutableStateOf(true) } - - LaunchedEffect(Unit) { - while (isActive) { - withFrameMillis { } - val currentItem = listState.firstVisibleItemIndex - if (currentItem == 0) scrollForward = true - if (currentItem > ITEM_COUNT - 100) scrollForward = false - listState.scrollBy(if (scrollForward) 67f else -67f) - frame++ - } - } - - LazyColumn( - state = listState, - modifier = Modifier.fillMaxSize() - ) { - items(ITEM_COUNT) { index -> - TextLayoutItem(index, frame) - } - } -} - -@Composable -private fun TextLayoutItem(index: Int, frame: Int) { - Column( - modifier = Modifier - .fillMaxWidth() - .background(Color(0xFF2A2A2A)) - ) { - repeat(12) { row -> - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween - ) { - repeat(10) { col -> - Column( - modifier = Modifier - .weight(1f) - .background(Color(0xFF3A3A3A)) - ) { - Text( - "$frame R$row:C$col", - color = Color.White, - fontSize = 6.sp - ) - Row { - Box( - modifier = Modifier - .size(10.dp) - .background(Color(Random.nextInt(0xFFFFFF) or 0xFF000000.toInt())) - ) - Column { - Text("$frame #$index", color = Color.Gray, fontSize = 6.sp) - Text("Item", color = Color.Gray, fontSize = 6.sp) - } - } - Spacer(modifier = Modifier.height(4.dp)) - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween - ) { - repeat(10) { - Box( - modifier = Modifier - .size(10.dp) - .background(Color(0xFF4A4A4A)) - ) - } - } - } - } - } - } - } -} diff --git a/compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/visualeffects/HappyNY.kt b/compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/visualeffects/HappyNY.kt deleted file mode 100644 index ae07af509e08d..0000000000000 --- a/compose/mpp/benchmarks/src/commonMain/kotlin/benchmarks/visualeffects/HappyNY.kt +++ /dev/null @@ -1,321 +0,0 @@ -package benchmarks.visualeffects - -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.Surface -import androidx.compose.runtime.* -import androidx.compose.runtime.snapshots.SnapshotStateList -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.* -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp - -import kotlin.math.* -import kotlin.random.Random - -const val snowCount = 80 -const val starCount = 60 -const val rocketPartsCount = 30 - -data class SnowFlake( - var x: Dp, - var y: Dp, - val scale: Float, - var v: Double, - var alpha: Float, - var angle: Float, - var rotate: Int, - var phase: Double -) - -data class Star(val x: Dp, val y: Dp, val color: Color, val size: Dp) - -val random = Random(123) - -fun random(): Float = random.nextFloat() - -class DoubleRocket(val particle: Particle) { - private val STATE_ROCKET = 0 - private val STATE_SMALL_ROCKETS = 1 - var state = STATE_ROCKET - var rockets: Array = emptyArray() - private fun checkState(time: Long) { - if (particle.vy > -3.0 && state == STATE_ROCKET) { - explode(time) - } - if (state == STATE_SMALL_ROCKETS) { - var done = true - rockets.forEach { - if (!it.exploded) { - it.checkExplode(time) - } - if (!it.checkDone()) { - done = false - } - } - if (done) { - reset() - } - } - } - - private fun reset() { - state = STATE_ROCKET - particle.x = 0.0 - particle.y = 1000.0 - particle.vx = 2.1 - particle.vy = -12.5 - } - - private fun explode(time: Long) { - val colors = arrayOf(Color(0xff, 0, 0), Color(192, 255, 192), Color(192, 212, 255)) - rockets = Array(7) { - val v = 1.2f + 1.0 * random() - val angle = 2 * PI * random() - Rocket( - Particle( - particle.x, - particle.y, - v * sin(angle) + particle.vx, - v * cos(angle) + particle.vy - 0.5f, - colors[it % colors.size] - ), colors[it % colors.size], time - ) - } - state = STATE_SMALL_ROCKETS - } - - fun move(time: Long, prevTime: Long) { - if (rocket.state == rocket.STATE_ROCKET) { - rocket.particle.move(time, prevTime) - rocket.particle.gravity(time, prevTime) - } else { - rocket.rockets.forEach { - it.move(time, prevTime) - } - } - rocket.checkState(time) - } - - @Composable - fun draw() { - if (state == rocket.STATE_ROCKET) { - particle.draw() - } else { - rockets.forEach { - it.draw() - } - } - } - -} - -class Rocket(val particle: Particle, val color: Color, val startTime: Long = 0) { - var exploded = false - var parts: Array = emptyArray() - - fun checkExplode(time: Long) { - if (time - startTime > 1200000000) { - explode() - } - } - - private fun explode() { - parts = Array(rocketPartsCount) { - val v = 0.5f + 1.5 * random() - val angle = 2 * PI * random() - Particle(particle.x, particle.y, v * sin(angle) + particle.vx, v * cos(angle) + particle.vy, color, 1) - } - exploded = true - } - - fun checkDone(): Boolean { - if (!exploded) return false - parts.forEach { - if (it.y < 800) return false - } - return true - } - - fun move(time: Long, prevTime: Long) { - if (!exploded) { - particle.move(time, prevTime) - particle.gravity(time, prevTime) - checkExplode(time) - } else { - parts.forEach { - it.move(time, prevTime) - it.gravity(time, prevTime) - } - } - } - - @Composable - fun draw() { - if (!exploded) { - particle.draw() - } else { - parts.forEach { - it.draw() - } - } - } -} - -class Particle(var x: Double, var y: Double, var vx: Double, var vy: Double, val color: Color, val type: Int = 0) { - fun move(time: Long, prevTime: Long) { - x = (x + vx * (time - prevTime) / 30000000) - y = (y + vy * (time - prevTime) / 30000000) - } - - fun gravity(time: Long, prevTime: Long) { - vy = vy + 1.0f * (time - prevTime) / 300000000 - } - - @Composable - fun draw() { - val alphaFactor = if (type == 0) 1.0f else 1 / (1 + abs(vy / 5)).toFloat() - Box(Modifier.size(5.dp).offset(x.dp, y.dp).alpha(alphaFactor).clip(CircleShape).background(color)) - for (i in 1..5) { - Box( - Modifier.size(4.dp).offset((x - vx / 2 * i).dp, (y - vy / 2 * i).dp) - .alpha(alphaFactor * (1 - 0.18f * i)).clip(CircleShape).background(color) - ) - } - } -} - -val rocket = DoubleRocket(Particle(0.0, 1000.0, 2.1, -12.5, Color.White)) - -fun prepareStarsAndSnowFlakes(stars: SnapshotStateList, snowFlakes: SnapshotStateList, width: Int, height: Int) { - for (i in 0..snowCount) { - snowFlakes.add( - SnowFlake( - (50 + (width - 50) * random()).dp, - (height * random()).dp, - 0.1f + 0.2f * random(), - 1.5 + 3 * random(), - (0.4f + 0.4 * random()).toFloat(), - 60 * random(), - Random.nextInt(1, 5) - 3, - random() * 2 * PI - ) - ) - } - val colors = arrayOf(Color.Red, Color.Yellow, Color.Green, Color.Yellow, Color.Cyan, Color.Magenta, Color.White) - for (i in 0..starCount) { - stars.add( - Star( - (width * random()).dp, - (height * random()).dp, - colors[Random.nextInt(colors.size)], - (3 + 5 * random()).dp - ) - ) - } -} - -@Composable -fun NYContent(width: Int, height: Int) { - var time by remember { mutableStateOf(0L) } - val startTime = remember { 0L } - var prevTime by remember { mutableStateOf(0L) } - val snowFlakes = remember { mutableStateListOf() } - val stars = remember { mutableStateListOf() } - var flickering2 by remember { mutableStateOf(true) } - remember { prepareStarsAndSnowFlakes(stars, snowFlakes, width, height) } - - Surface( - modifier = Modifier.fillMaxSize().padding(5.dp).shadow(3.dp, RoundedCornerShape(20.dp)), - color = Color.Black, - shape = RoundedCornerShape(20.dp) - ) { - - LaunchedEffect(Unit) { - while (true) { - withFrameNanos { - prevTime = time - time = it - } - } - } - - if (flickering2) { - if (time - startTime > 15500000000) { //note, that startTime has been updated above - flickering2 = false - } - } - - rocket.move(time, prevTime) - - Box(Modifier.fillMaxSize()) { - - snow(time, prevTime, snowFlakes, startTime, height) - - starrySky(stars) - - rocket.draw() - - } - } -} - - -@Composable -fun starrySky(stars: SnapshotStateList) { - stars.forEach { - star(it.x, it.y, it.color, size = it.size) - } -} - -@Composable -fun star(x: Dp, y: Dp, color: Color = Color.White, size: Dp) { - Box(Modifier.offset(x, y).scale(1.0f, 0.2f).rotate(45f).size(size).background(color)) - Box(Modifier.offset(x, y).scale(0.2f, 1.0f).rotate(45f).size(size).background(color)) -} - -@Composable -fun snow(time: Long, prevTime: Long, snowFlakes: SnapshotStateList, startTime: Long, height: Int) { - val deltaAngle = (time - startTime) / 100000000 - with(LocalDensity.current) { - snowFlakes.forEach { - var y = it.y + ((it.v * (time - prevTime)) / 300000000).dp - if (y > (height + 20).dp) { - y = -20.dp - } - it.y = y - val x = it.x + (15 * sin(time.toDouble() / 3000000000 + it.phase)).dp - snowFlake(Modifier.offset(x, y).scale(it.scale).rotate(it.angle + deltaAngle * it.rotate), it.alpha) - } - } -} - -@Composable -fun snowFlake(modifier: Modifier, alpha: Float = 0.8f) { - Box(modifier) { - snowFlakeInt(0, 0f, 30.dp, 0.dp, alpha) - snowFlakeInt(0, 60f, 15.dp, 25.dp, alpha) - snowFlakeInt(0, 120f, -15.dp, 25.dp, alpha) - snowFlakeInt(0, 180f, -30.dp, 0.dp, alpha) - snowFlakeInt(0, 240f, -15.dp, -25.dp, alpha) - snowFlakeInt(0, 300f, 15.dp, -25.dp, alpha) - } - -} - -@Composable -fun snowFlakeInt(level: Int, angle: Float, shiftX: Dp, shiftY: Dp, alpha: Float) { - if (level > 3) return - Box( - Modifier.offset(shiftX, shiftY).rotate(angle).width(100.dp).height(10.dp).scale(0.6f).alpha(1f) - .background(Color.White.copy(alpha = alpha)) - ) { - snowFlakeInt(level + 1, 30f, 12.dp, 20.dp, alpha * 0.8f) - snowFlakeInt(level + 1, -30f, 12.dp, -20.dp, alpha * 0.8f) - } -} - diff --git a/compose/mpp/benchmarks/src/desktopMain/kotlin/BenchmarksSave.desktop.kt b/compose/mpp/benchmarks/src/desktopMain/kotlin/BenchmarksSave.desktop.kt deleted file mode 100644 index 1ac7b1bad0c47..0000000000000 --- a/compose/mpp/benchmarks/src/desktopMain/kotlin/BenchmarksSave.desktop.kt +++ /dev/null @@ -1,8 +0,0 @@ -/* - * Copyright 2020-2025 JetBrains s.r.o. and respective authors and developers. - * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. - */ - -actual fun saveBenchmarkStats(name: String, stats: BenchmarkStats) = saveBenchmarkStatsOnDisk(name, stats) - -actual val printJsonToOutput: Boolean = false diff --git a/compose/mpp/benchmarks/src/desktopMain/kotlin/BenchmarksSaveServer.kt b/compose/mpp/benchmarks/src/desktopMain/kotlin/BenchmarksSaveServer.kt deleted file mode 100644 index 4e0ac67c1b082..0000000000000 --- a/compose/mpp/benchmarks/src/desktopMain/kotlin/BenchmarksSaveServer.kt +++ /dev/null @@ -1,92 +0,0 @@ -/* - * Copyright 2020-2025 JetBrains s.r.o. and respective authors and developers. - * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. - */ - -import io.ktor.http.* -import io.ktor.serialization.kotlinx.json.* -import io.ktor.server.application.* -import io.ktor.server.engine.* -import io.ktor.server.netty.* -import io.ktor.server.plugins.contentnegotiation.* -import io.ktor.server.plugins.cors.routing.CORS -import io.ktor.server.request.* -import io.ktor.server.response.* -import io.ktor.server.routing.* -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import kotlinx.io.files.Path -import kotlinx.io.files.SystemFileSystem -import kotlinx.serialization.Serializable - -/** - * Data class for receiving benchmark results from client. - */ -@Serializable -data class BenchmarkResultFromClient( - val name: String, - val stats: String // JSON string of BenchmarkStats -) - -/** - * Starts a Ktor server to receive benchmark results from browsers - * and save them to disk in the same format as the direct disk saving mechanism. - */ -object BenchmarksSaveServer { - private var server: EmbeddedServer? = null - - fun start(port: Int = BENCHMARK_SERVER_PORT) { - if (server != null) { - println("Benchmark server is already running") - return - } - - server = embeddedServer(Netty, port = port) { - install(ContentNegotiation) { - json() - } - install(CORS) { - allowMethod(HttpMethod.Get) - allowMethod(HttpMethod.Post) - allowHeader(HttpHeaders.ContentType) - anyHost() - } - routing { - post("/benchmark") { - val result = call.receive() - if (result.name.isEmpty()) { - println("Stopping server! Received empty name from client") - call.respond(HttpStatusCode.OK, "Server stopped.") - stop() - return@post - } - println("Received benchmark result for: ${result.name}") - - withContext(Dispatchers.IO) { - if (Config.saveStatsToJSON) { - saveJson(result.name, result.stats) - } - - if (Config.saveStatsToCSV) { - // TODO: for CSV, we would need to convert JSON to the values - println("CSV results are not yet supported for the browser.") - } - } - - call.respond(HttpStatusCode.OK, "Benchmark result saved") - } - - get("/") { - call.respondText("Benchmark server is running", ContentType.Text.Plain) - } - } - }.start(wait = true) - } - - fun stop() { - server?.stop(1000, 2000) - server = null - println("Benchmark server stopped") - System.exit(0) - } -} diff --git a/compose/mpp/benchmarks/src/desktopMain/kotlin/GraphicsContext.desktop.kt b/compose/mpp/benchmarks/src/desktopMain/kotlin/GraphicsContext.desktop.kt deleted file mode 100644 index df9eb367aba24..0000000000000 --- a/compose/mpp/benchmarks/src/desktopMain/kotlin/GraphicsContext.desktop.kt +++ /dev/null @@ -1,43 +0,0 @@ -import org.jetbrains.skia.ColorSpace -import org.jetbrains.skia.PixelGeometry -import org.jetbrains.skia.Surface -import org.jetbrains.skia.SurfaceColorFormat -import org.jetbrains.skia.SurfaceOrigin -import org.jetbrains.skia.SurfaceProps -import org.jetbrains.skiko.ExperimentalSkikoApi -import org.jetbrains.skiko.graphicapi.DirectXOffscreenContext -import org.jetbrains.skiko.hostOs - -@OptIn(ExperimentalSkikoApi::class) -fun graphicsContext(): GraphicsContext? = when { - hostOs.isWindows -> DirectXGraphicsContext() - else -> { - println("Unsupported desktop host OS: $hostOs. Using non-GPU graphic context") - null - } -} - -@OptIn(ExperimentalSkikoApi::class) -class DirectXGraphicsContext() : GraphicsContext { - // Note: we don't close `context` and `texture` after using, - // because it is created once in the main function - private val context = DirectXOffscreenContext() - private var texture: DirectXOffscreenContext.Texture? = null - - override fun surface(width: Int, height: Int): Surface { - texture?.close() - texture = context.Texture(width, height) - return Surface.makeFromBackendRenderTarget( - context.directContext, - texture!!.backendRenderTarget, - SurfaceOrigin.TOP_LEFT, - SurfaceColorFormat.BGRA_8888, - ColorSpace.sRGB, - SurfaceProps(pixelGeometry = PixelGeometry.UNKNOWN) - ) ?: throw IllegalStateException("Can't create Surface") - } - - override suspend fun awaitGPUCompletion() { - texture?.waitForCompletion() - } -} diff --git a/compose/mpp/benchmarks/src/desktopMain/kotlin/main.desktop.kt b/compose/mpp/benchmarks/src/desktopMain/kotlin/main.desktop.kt deleted file mode 100644 index 53da7526ca278..0000000000000 --- a/compose/mpp/benchmarks/src/desktopMain/kotlin/main.desktop.kt +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright 2020-2021 JetBrains s.r.o. and respective authors and developers. - * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. - */ - -import androidx.compose.ui.unit.dp -import androidx.compose.ui.window.Window -import androidx.compose.ui.window.application -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 val isSvgSupported: Boolean = true - -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) - - if (Config.runServer) { - // Start the benchmark server to receive results from browsers - BenchmarksSaveServer.start() - } else if (Config.isModeEnabled(Mode.REAL) || Config.isModeEnabled(Mode.STARTUP)) { - val device = GraphicsEnvironment.getLocalGraphicsEnvironment().defaultScreenDevice - val frameRate = device.displayMode.refreshRate.takeIf { it > 0 } ?: 120 - - application { - Window( - onCloseRequest = ::exitApplication, - alwaysOnTop = true, - state = rememberWindowState( - width = 1920.dp, height = 1080.dp - ) - ) { - BenchmarkRunner(getBenchmarks(), frameRate, { System.exit(0) }) - } - } - } else { - runBlocking(Dispatchers.Main) { runBenchmarks(graphicsContext = graphicsContext()) } - } -} diff --git a/compose/mpp/benchmarks/src/desktopMain/kotlin/runGC.jvm.kt b/compose/mpp/benchmarks/src/desktopMain/kotlin/runGC.jvm.kt deleted file mode 100644 index 3095a116b1ca0..0000000000000 --- a/compose/mpp/benchmarks/src/desktopMain/kotlin/runGC.jvm.kt +++ /dev/null @@ -1,3 +0,0 @@ -actual fun runGC() { - System.gc() -} \ No newline at end of file diff --git a/compose/mpp/benchmarks/src/iosMain/kotlin/Platform.ios.kt b/compose/mpp/benchmarks/src/iosMain/kotlin/Platform.ios.kt deleted file mode 100644 index 3f7ddd75eda31..0000000000000 --- a/compose/mpp/benchmarks/src/iosMain/kotlin/Platform.ios.kt +++ /dev/null @@ -1,12 +0,0 @@ -/* - * Copyright 2020-2021 JetBrains s.r.o. and respective authors and developers. - * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. - */ - -import kotlin.time.TimeSource - -actual val mainTime: TimeSource.Monotonic.ValueTimeMark = TimeSource.Monotonic.markNow() - -actual val printJsonToOutput: Boolean = true - -actual val isSvgSupported: Boolean = true diff --git a/compose/mpp/benchmarks/src/iosMain/kotlin/main.ios.kt b/compose/mpp/benchmarks/src/iosMain/kotlin/main.ios.kt deleted file mode 100644 index 8c4f5623fa6b6..0000000000000 --- a/compose/mpp/benchmarks/src/iosMain/kotlin/main.ios.kt +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright 2020-2021 JetBrains s.r.o. and respective authors and developers. - * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. - */ - -import androidx.compose.ui.ExperimentalComposeUiApi -import androidx.compose.ui.window.ComposeUIViewController -import kotlinx.coroutines.MainScope -import kotlinx.coroutines.launch -import platform.UIKit.UIApplication -import platform.UIKit.UIScreen -import platform.UIKit.UIViewController -import kotlin.system.exitProcess - -fun setGlobalFromArgs(args : List) { - Config.setGlobalFromArgs(args.toTypedArray()) -} - -fun runReal() = Config.isModeEnabled(Mode.REAL) || Config.isModeEnabled(Mode.STARTUP) - -fun runBenchmarks() { - UIApplication.sharedApplication.setIdleTimerDisabled(true) - MainScope().launch { - runBenchmarks(graphicsContext = graphicsContext()) - println("Completed!") - exitProcess(0) - } -} - -@OptIn(ExperimentalComposeUiApi::class) -fun MainViewController(): UIViewController { - return ComposeUIViewController(configure = { parallelRendering = Config.parallelRendering }) { - BenchmarkRunner(getBenchmarks(), UIScreen.mainScreen.maximumFramesPerSecond.toInt(), { exitProcess(0) }) - } -} diff --git a/compose/mpp/benchmarks/src/jsMain/kotlin/Main.js.kt b/compose/mpp/benchmarks/src/jsMain/kotlin/Main.js.kt deleted file mode 100644 index 4e3892edd68a5..0000000000000 --- a/compose/mpp/benchmarks/src/jsMain/kotlin/Main.js.kt +++ /dev/null @@ -1,4 +0,0 @@ - -fun main() { - mainBrowser() -} \ No newline at end of file diff --git a/compose/mpp/benchmarks/src/macosMain/kotlin/main.macos.kt b/compose/mpp/benchmarks/src/macosMain/kotlin/main.macos.kt deleted file mode 100644 index 2099537f9b1e4..0000000000000 --- a/compose/mpp/benchmarks/src/macosMain/kotlin/main.macos.kt +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright 2020-2021 JetBrains s.r.o. and respective authors and developers. - * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. - */ -import androidx.compose.ui.unit.DpSize -import androidx.compose.ui.unit.dp -import androidx.compose.ui.window.Window -import kotlinx.coroutines.runBlocking -import platform.AppKit.NSApp -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 printJsonToOutput: Boolean = false - -actual val isSvgSupported: Boolean = true - -fun main(args : Array) { - Config.setGlobalFromArgs(args) - if (Config.isModeEnabled(Mode.REAL) || Config.isModeEnabled(Mode.STARTUP)) { - NSApplication.sharedApplication() - val frameRate = (NSScreen.mainScreen?.maximumFramesPerSecond?.toInt()) ?: 120 - Window( - "Benchmarks", - DpSize( - width = 1920.dp, height = 1080.dp - ) - ) { - BenchmarkRunner(getBenchmarks(), frameRate, { exitProcess(0) }) - } - NSApp?.run() - } else { - runBlocking { runBenchmarks(graphicsContext = graphicsContext()) } - } -} \ No newline at end of file diff --git a/compose/mpp/benchmarks/src/wasmJsMain/kotlin/Main.wasmJs.kt b/compose/mpp/benchmarks/src/wasmJsMain/kotlin/Main.wasmJs.kt deleted file mode 100644 index c3d7a73c2afdb..0000000000000 --- a/compose/mpp/benchmarks/src/wasmJsMain/kotlin/Main.wasmJs.kt +++ /dev/null @@ -1,64 +0,0 @@ -import kotlinx.coroutines.MainScope -import kotlinx.coroutines.promise -import kotlin.js.Promise - -val jsOne = 1.toJsNumber() - -fun main(args: Array) { - if (isD8env().toBoolean()) { - mainD8(args) - } else { - mainBrowser() - } -} - -// Currently, the initialization can't be adjusted to avoid calling the fun main, but -// we don't want use the default fun main, because Jetstream3 requires running the workloads separately / independently of each other. -// Also, they require that a benchmark completes before the function exists, which is not possible with if they just call fun main. -// Therefore, they'll rely on fun customLaunch, which returns a Promise (can be awaited for). -fun mainD8(args: Array) { - println("mainD8 is intentionally doing nothing. Read the comments in main.wasmJs.kt") -} - -private val basicConfigForD8 = Config( - // Using only SIMPLE mode, because VSYNC_EMULATION calls delay(...), - // which is implemented via setTimeout on web targets. - // setTimeout implementation is simplified in D8, making - // the VSYNC_EMULATION mode meaningless when running in D8 - modes = setOf(Mode.SIMPLE), - // MultipleComponents is unsupported, because it uses vector icons. D8 doesn't provide XML parsing APIs. - // But there is an alternative workload called 'MultipleComponents-NoVectorGraphics' - disabledBenchmarks = setOf("MultipleComponents"), -) - -@JsExport -fun customLaunch(benchmarkName: String, frameCount: Int): Promise { - val config = basicConfigForD8.copy( - benchmarks = mapOf(benchmarkName.uppercase() to frameCount) - ) - Config.setGlobal(config) - - return MainScope().promise { - runBenchmarks(warmupCount = 0) - jsOne - } -} - -@JsExport -fun d8BenchmarksRunner(args: String): Promise { - val config = Args.parseArgs(args.split(" ").toTypedArray()) - .copy( - modes = basicConfigForD8.modes, - disabledBenchmarks = basicConfigForD8.disabledBenchmarks - ) - - Config.setGlobal(config) - - return MainScope().promise { - runBenchmarks() - jsOne - } -} - -private fun isD8env(): JsBoolean = - js("typeof isD8 !== 'undefined'") \ No newline at end of file diff --git a/compose/mpp/benchmarks/src/webMain/kotlin/BenchmarksSave.web.kt b/compose/mpp/benchmarks/src/webMain/kotlin/BenchmarksSave.web.kt deleted file mode 100644 index 34489ac9400f7..0000000000000 --- a/compose/mpp/benchmarks/src/webMain/kotlin/BenchmarksSave.web.kt +++ /dev/null @@ -1,116 +0,0 @@ -/* - * Copyright 2020-2025 JetBrains s.r.o. and respective authors and developers. - * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. - */ - -import io.ktor.client.HttpClient -import io.ktor.client.plugins.contentnegotiation.ContentNegotiation -import io.ktor.client.request.get -import io.ktor.client.request.post -import io.ktor.client.request.setBody -import io.ktor.http.ContentType -import io.ktor.http.HttpStatusCode -import io.ktor.http.contentType -import io.ktor.serialization.kotlinx.json.json -import kotlinx.browser.window -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch -import kotlinx.coroutines.yield -import kotlinx.serialization.Serializable - -/** - * Browser implementation for saving benchmark results. - * Instead of trying to save directly to disk (which would fail with UnsupportedOperationException), - * this implementation sends the results to a server via HTTP. - */ -actual fun saveBenchmarkStats(name: String, stats: BenchmarkStats) { - GlobalScope.launch { - BenchmarksSaveServerClient.sendBenchmarkResult(name, stats) - } -} - -actual val printJsonToOutput: Boolean = false - -/** - * Client for sending benchmark results to the server - */ -object BenchmarksSaveServerClient { - private val client = HttpClient { - install(ContentNegotiation) { - json() - } - } - - private var resultsSendingInProgress = 0 - - private fun serverUrlRoot(): String { - val protocol = window.location.protocol - val hostname = window.location.hostname - return "$protocol//$hostname:$BENCHMARK_SERVER_PORT" - } - - private fun serverUrl(): String { - return "${serverUrlRoot()}/benchmark" - } - - /** - * Sends benchmark results to the server - */ - suspend fun sendBenchmarkResult(name: String, stats: BenchmarkStats) { - resultsSendingInProgress++ - println("Sending results: $name") - sendBenchmarkResult(name, stats.toJsonString()) - resultsSendingInProgress-- - println("Benchmark result sent to server: ${serverUrl()}") - } - - private suspend fun sendBenchmarkResult(name: String, stats: String) { - try { - - val result = BenchmarkResultToServer( - name = name, - stats = stats - ) - - client.post(serverUrl()) { - contentType(ContentType.Application.Json) - setBody(result) - } - } catch (e: Throwable) { - println("Error sending benchmark result to server: ${e.message}") - } - } - - suspend fun stopServer() { - while (resultsSendingInProgress > 0) { - yield() - } - sendBenchmarkResult("", "") - } - - suspend fun isServerAlive(): Boolean { - // waiting for the server to start for 2 seconds - val TIMEOUT = 2000 - val DELTA = 100L - var delayed = 0L - while (delayed < TIMEOUT) { - try { - return client.get(serverUrlRoot()).status == HttpStatusCode.OK - } catch (_: Throwable) { - delayed += DELTA - delay(DELTA) - } - } - return false - } -} - -/** - * Data class for sending benchmark results to the server - */ -@Serializable -data class BenchmarkResultToServer( - val name: String, - val stats: String // JSON string of BenchmarkStats -) \ No newline at end of file diff --git a/compose/mpp/benchmarks/src/webMain/kotlin/Main.web.kt b/compose/mpp/benchmarks/src/webMain/kotlin/Main.web.kt deleted file mode 100644 index 4d733e63434aa..0000000000000 --- a/compose/mpp/benchmarks/src/webMain/kotlin/Main.web.kt +++ /dev/null @@ -1,67 +0,0 @@ -import androidx.compose.ui.ExperimentalComposeUiApi -import androidx.compose.ui.window.ComposeViewport -import kotlinx.browser.document -import kotlinx.browser.window -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() - -actual val isSvgSupported: Boolean = true - -@OptIn(ExperimentalComposeUiApi::class, ExperimentalWasmJsInterop::class) -fun mainBrowser() = MainScope().launch { - awaitSkikoWasm() - - val urlParams = URLSearchParams(window.location.search.toJsString()) - var i = 0 - val args = generateSequence { urlParams.get("arg${i++}") }.toList().toTypedArray() - - Config.setGlobalFromArgs(args) - - val composeRoot = document.getElementById("root") ?: error("No root element found") - 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() - GlobalScope.launch { - if (BenchmarksSaveServerClient.isServerAlive()) { - BenchmarksSaveServerClient.stopServer() - } - } - }) - } - } else { - composeRoot.remove() - println("Wait for the benchmarks to complete...\n") - MainScope().launch { - if (Config.saveStats() && !BenchmarksSaveServerClient.isServerAlive()) { - println("No benchmark server found.") - return@launch - } - - runBenchmarks() - println("Completed!") - if (BenchmarksSaveServerClient.isServerAlive()) { - GlobalScope.launch { - BenchmarksSaveServerClient.stopServer() - } - } - } - } -} - -private suspend fun awaitSkikoWasm() { - suspendCancellableCoroutine { c -> - @Suppress("INVISIBLE_REFERENCE") - org.jetbrains.skiko.wasm.onWasmReady { - c.resumeWith(Result.success(Unit)) - } - } -} diff --git a/compose/mpp/benchmarks/src/webMain/kotlin/runGC.web.kt b/compose/mpp/benchmarks/src/webMain/kotlin/runGC.web.kt deleted file mode 100644 index 461e709899404..0000000000000 --- a/compose/mpp/benchmarks/src/webMain/kotlin/runGC.web.kt +++ /dev/null @@ -1,3 +0,0 @@ -actual fun runGC() { - js("(typeof gc === 'function')? gc() : console.log('Manual GC is not available. Ensure that the browser was started with the appropriate flags.')") -} \ No newline at end of file diff --git a/compose/mpp/benchmarks/src/webMain/resources/index.html b/compose/mpp/benchmarks/src/webMain/resources/index.html deleted file mode 100644 index 196358702e835..0000000000000 --- a/compose/mpp/benchmarks/src/webMain/resources/index.html +++ /dev/null @@ -1,22 +0,0 @@ - - - - - Benchmarks Compose + K/Wasm - - - - - -

- - \ No newline at end of file diff --git a/compose/mpp/benchmarks/src/webMain/resources/launcher.mjs b/compose/mpp/benchmarks/src/webMain/resources/launcher.mjs deleted file mode 100644 index d6ec6a0fef898..0000000000000 --- a/compose/mpp/benchmarks/src/webMain/resources/launcher.mjs +++ /dev/null @@ -1,5 +0,0 @@ -import './polyfills.mjs'; -import { d8BenchmarksRunner } from './mpp-benchmarks.mjs'; - -await d8BenchmarksRunner(Array.from(arguments).join(' ')); -console.log('Finished'); diff --git a/compose/mpp/benchmarks/src/webMain/resources/launcher_jetstream3.mjs b/compose/mpp/benchmarks/src/webMain/resources/launcher_jetstream3.mjs deleted file mode 100644 index bfbdb1a69ff98..0000000000000 --- a/compose/mpp/benchmarks/src/webMain/resources/launcher_jetstream3.mjs +++ /dev/null @@ -1,16 +0,0 @@ -import './polyfills.mjs'; -import { customLaunch } from './mpp-benchmarks.mjs'; - -/* - AnimatedVisibility, - LazyGrid, - LazyGrid-ItemLaunchedEffect, - LazyGrid-SmoothScroll, - LazyGrid-SmoothScroll-ItemLaunchedEffect, - VisualEffects, - MultipleComponents-NoVectorGraphics - */ -let name = arguments[0] ? arguments[0] : 'AnimatedVisibility'; -let frameCount = arguments[1] ? parseInt(arguments[1]) : 1000; -await customLaunch(name, frameCount); -console.log('Finished'); diff --git a/compose/mpp/benchmarks/src/webMain/resources/polyfills.mjs b/compose/mpp/benchmarks/src/webMain/resources/polyfills.mjs deleted file mode 100644 index eb73783e4fa7b..0000000000000 --- a/compose/mpp/benchmarks/src/webMain/resources/polyfills.mjs +++ /dev/null @@ -1,83 +0,0 @@ -// polyfills.mjs -globalThis.isD8 = true; - -if (typeof globalThis.window === 'undefined') { - globalThis.window = globalThis; -} -if (typeof globalThis.navigator === 'undefined') { - globalThis.navigator = {}; -} -if (!globalThis.navigator.languages) { - globalThis.navigator.languages = ['en-US', 'en']; - globalThis.navigator.userAgent = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36'; - globalThis.navigator.platform = "MacIntel"; -} - -// Compose reads `window.isSecureContext` in its Clipboard feature: -globalThis.isSecureContext = false; - -if (!globalThis.gc) { - // No GC control in D8 - globalThis.gc = () => { - // console.log('gc called'); - }; -} - -if (typeof globalThis.AbortController === 'undefined') { - globalThis.AbortController = class { - constructor() { - this.signal = {}; - } - abort() {} - }; -} - -// Minimal Blob polyfill -class BlobPolyfill { - constructor(uint8, type = '') { - this._uint8 = uint8; - this._type = type; - } - get size() { - return this._uint8.byteLength; - } - get type() { return this._type; } - async arrayBuffer() { - console.log('arrayBuffer called'); - return this._uint8.buffer; - } - slice(start = 0, end = this.size, type = this.type) { - return new BlobPolyfill(this._uint8.slice(start, end), type); - } -} - -const runtimeDirectory = String(import.meta.url) - .replace(/^file:\/\//, '') - .replace(/\/[^/]+$/, ''); - -globalThis.fetch = async (request) => { - const resourcePath = String(request) - .replace(/^file:\/\//, '') - .replace(/^\.\//, ''); - const filePath = resourcePath.startsWith('/') - ? resourcePath - : `${runtimeDirectory}/${resourcePath}`; - console.log('fetch', resourcePath); - let data; - try { - data = read(filePath, 'binary'); - } catch (error) { - console.error(`Failed to read D8 resource ${filePath}:`, error); - throw error; - } - - const uint8 = new Uint8Array(data); - - return { - ok: true, - status: 200, - async blob() { - return new BlobPolyfill(uint8, 'application/xml'); - }, - }; -}; diff --git a/compose/mpp/benchmarks/src/webMain/resources/styles.css b/compose/mpp/benchmarks/src/webMain/resources/styles.css deleted file mode 100644 index 85a9c38ec952b..0000000000000 --- a/compose/mpp/benchmarks/src/webMain/resources/styles.css +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - - -html, body { - width: 100%; - height: 100%; - margin: 0; - padding: 0; -} - -#root { - width: 100%; - height: 100%; -} From d4c7c124213d5e9bb589c18c2b0ae2865d53133c Mon Sep 17 00:00:00 2001 From: Konstantin Date: Wed, 11 Nov 2026 10:49:45 +0100 Subject: [PATCH 21/61] Setup included build on compose-core for benchmarks project --- compose/mpp/benchmarks/README.md | 81 ++++++ .../benchmarks-app/build.gradle.kts | 25 +- .../benchmarks-app/gradle.properties | 0 .../benchmarks-app/proguard-rules.pro | 0 .../benchmarks/app/StartupTiming.apple.kt | 0 .../drawable/compose-multiplatform.png | Bin .../drawable/example1_cat.jpg | Bin .../drawable/example1_ic_call_answer.xml | 0 .../drawable/example1_sailing.svg | 0 .../example1_compose-community-primary.png | Bin .../font/jetbrainsmono_italic.ttf | Bin .../font/jetbrainsmono_regular.ttf | Bin .../jetbrains/compose/benchmarks/app/App.kt | 0 .../compose/benchmarks/app/ArgumentsParser.kt | 0 .../benchmarks/app/RenderBenchmarks.kt | 0 .../compose/benchmarks/app/StartupTiming.kt | 0 .../animation/AnimatedVisibility.kt | 0 .../benchmarks/canvasdrawing/CanvasDrawing.kt | 0 .../complexlazylist/components/MainUI.kt | 0 .../components/MultiLevelCell.kt | 0 .../components/refresh/LoadingIndicator.kt | 0 .../components/refresh/SwipeProgress.kt | 0 .../components/refresh/SwipeRefreshLayout.kt | 0 .../SwipeRefreshNestedScrollConnection.kt | 0 .../components/refresh/SwipeRefreshState.kt | 0 .../complexlazylist/models/FollowItem.kt | 0 .../complexlazylist/models/HotItem.kt | 0 .../complexlazylist/models/IBaseViewModel.kt | 0 .../models/ICompositionModel.kt | 0 .../models/ViewModelFactory.kt | 0 .../app/benchmarks/heavyshader/HeavyShader.kt | 0 .../app/benchmarks/lazygrid/LazyGrid.kt | 0 .../multipleComponents/Clickable.kt | 0 .../benchmarks/multipleComponents/MenuIcon.kt | 0 .../multipleComponents/MultipleComponents.kt | 0 .../app/benchmarks/textlayout/TextLayout.kt | 0 .../app/benchmarks/visualeffects/HappyNY.kt | 0 .../compose/benchmarks/app/Main.ios.kt | 0 .../compose/benchmarks/app/Main.jvm.kt | 0 .../benchmarks/app/StartupTiming.jvm.kt | 0 .../compose/benchmarks/app/Main.macos.kt | 0 .../compose/benchmarks/app/Main.web.kt | 0 .../benchmarks/app/StartupTiming.web.kt | 0 .../src/webMain/resources/index.html | 0 .../benchmarks-collector/build.gradle.kts | 10 +- .../compose/benchmarks/collector/Main.kt | 0 .../project.pbxproj | 8 +- .../contents.xcworkspacedata | 0 .../xcschemes/xcschememanagement.plist | 5 + .../xcschemes/BenchmarksIosApp.xcscheme | 0 .../xcschemes/xcschememanagement.plist | 5 + .../AccentColor.colorset/Contents.json | 0 .../AppIcon.appiconset/Contents.json | 0 .../Assets.xcassets/Contents.json | 0 .../BenchmarksIosApp/BenchmarksIosApp.swift | 0 .../BenchmarksIosApp/Info.plist | 0 .../Preview Assets.xcassets/Contents.json | 0 .../benchmarks-ios-app/README.md | 8 +- .../benchmarks-report/build.gradle.kts | 4 +- .../benchmarks-report/gradle.properties | 0 .../benchmarks/report/BenchmarkReports.kt | 0 .../benchmarks-runtime/build.gradle.kts | 22 +- .../benchmarks-runtime/gradle.properties | 0 .../compose/benchmarks/Platform.apple.kt | 0 .../compose/benchmarks/AppMeasurement.kt | 0 .../benchmarks/BenchmarkComposeScene.kt | 0 .../compose/benchmarks/BenchmarkRunner.kt | 0 .../compose/benchmarks/ContentMeasurement.kt | 0 .../compose/benchmarks/GraphicsContext.kt | 0 .../compose/benchmarks/HttpClient.kt | 0 .../jetbrains/compose/benchmarks/Platform.kt | 0 .../compose/benchmarks/Platform.jvm.kt | 0 .../compose/benchmarks/Platform.web.kt | 0 compose/mpp/benchmarks/build.gradle.kts | 7 + compose/mpp/benchmarks/gradle.properties | 21 ++ .../mpp/benchmarks/gradle/libs.versions.toml | 33 +++ .../gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 45633 bytes .../gradle/wrapper/gradle-wrapper.properties | 8 + compose/mpp/benchmarks/gradlew | 248 ++++++++++++++++++ compose/mpp/benchmarks/gradlew.bat | 93 +++++++ .../launch-scripts/README.md | 30 ++- .../launch-scripts/run_benchmarks.py | 30 ++- compose/mpp/benchmarks/settings.gradle.kts | 64 +++++ settings-fork.gradle | 4 - 84 files changed, 638 insertions(+), 68 deletions(-) create mode 100644 compose/mpp/benchmarks/README.md rename compose/mpp/{newbenchmarks => benchmarks}/benchmarks-app/build.gradle.kts (69%) rename compose/mpp/{newbenchmarks => benchmarks}/benchmarks-app/gradle.properties (100%) rename compose/mpp/{newbenchmarks => benchmarks}/benchmarks-app/proguard-rules.pro (100%) rename compose/mpp/{newbenchmarks => benchmarks}/benchmarks-app/src/appleMain/kotlin/org/jetbrains/compose/benchmarks/app/StartupTiming.apple.kt (100%) rename compose/mpp/{newbenchmarks => benchmarks}/benchmarks-app/src/commonMain/composeResources/drawable/compose-multiplatform.png (100%) rename compose/mpp/{newbenchmarks => benchmarks}/benchmarks-app/src/commonMain/composeResources/drawable/example1_cat.jpg (100%) rename compose/mpp/{newbenchmarks => benchmarks}/benchmarks-app/src/commonMain/composeResources/drawable/example1_ic_call_answer.xml (100%) rename compose/mpp/{newbenchmarks => benchmarks}/benchmarks-app/src/commonMain/composeResources/drawable/example1_sailing.svg (100%) rename compose/mpp/{newbenchmarks => benchmarks}/benchmarks-app/src/commonMain/composeResources/files/example1_compose-community-primary.png (100%) rename compose/mpp/{newbenchmarks => benchmarks}/benchmarks-app/src/commonMain/composeResources/font/jetbrainsmono_italic.ttf (100%) rename compose/mpp/{newbenchmarks => benchmarks}/benchmarks-app/src/commonMain/composeResources/font/jetbrainsmono_regular.ttf (100%) rename compose/mpp/{newbenchmarks => benchmarks}/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/App.kt (100%) rename compose/mpp/{newbenchmarks => benchmarks}/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/ArgumentsParser.kt (100%) rename compose/mpp/{newbenchmarks => benchmarks}/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/RenderBenchmarks.kt (100%) rename compose/mpp/{newbenchmarks => benchmarks}/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/StartupTiming.kt (100%) rename compose/mpp/{newbenchmarks => benchmarks}/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/animation/AnimatedVisibility.kt (100%) rename compose/mpp/{newbenchmarks => benchmarks}/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/canvasdrawing/CanvasDrawing.kt (100%) rename compose/mpp/{newbenchmarks => benchmarks}/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/complexlazylist/components/MainUI.kt (100%) rename compose/mpp/{newbenchmarks => benchmarks}/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/complexlazylist/components/MultiLevelCell.kt (100%) rename compose/mpp/{newbenchmarks => benchmarks}/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/complexlazylist/components/refresh/LoadingIndicator.kt (100%) rename compose/mpp/{newbenchmarks => benchmarks}/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/complexlazylist/components/refresh/SwipeProgress.kt (100%) rename compose/mpp/{newbenchmarks => benchmarks}/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/complexlazylist/components/refresh/SwipeRefreshLayout.kt (100%) rename compose/mpp/{newbenchmarks => benchmarks}/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/complexlazylist/components/refresh/SwipeRefreshNestedScrollConnection.kt (100%) rename compose/mpp/{newbenchmarks => benchmarks}/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/complexlazylist/components/refresh/SwipeRefreshState.kt (100%) rename compose/mpp/{newbenchmarks => benchmarks}/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/complexlazylist/models/FollowItem.kt (100%) rename compose/mpp/{newbenchmarks => benchmarks}/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/complexlazylist/models/HotItem.kt (100%) rename compose/mpp/{newbenchmarks => benchmarks}/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/complexlazylist/models/IBaseViewModel.kt (100%) rename compose/mpp/{newbenchmarks => benchmarks}/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/complexlazylist/models/ICompositionModel.kt (100%) rename compose/mpp/{newbenchmarks => benchmarks}/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/complexlazylist/models/ViewModelFactory.kt (100%) rename compose/mpp/{newbenchmarks => benchmarks}/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/heavyshader/HeavyShader.kt (100%) rename compose/mpp/{newbenchmarks => benchmarks}/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/lazygrid/LazyGrid.kt (100%) rename compose/mpp/{newbenchmarks => benchmarks}/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/multipleComponents/Clickable.kt (100%) rename compose/mpp/{newbenchmarks => benchmarks}/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/multipleComponents/MenuIcon.kt (100%) rename compose/mpp/{newbenchmarks => benchmarks}/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/multipleComponents/MultipleComponents.kt (100%) rename compose/mpp/{newbenchmarks => benchmarks}/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/textlayout/TextLayout.kt (100%) rename compose/mpp/{newbenchmarks => benchmarks}/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/visualeffects/HappyNY.kt (100%) rename compose/mpp/{newbenchmarks => benchmarks}/benchmarks-app/src/iosMain/kotlin/org/jetbrains/compose/benchmarks/app/Main.ios.kt (100%) rename compose/mpp/{newbenchmarks => benchmarks}/benchmarks-app/src/jvmMain/kotlin/org/jetbrains/compose/benchmarks/app/Main.jvm.kt (100%) rename compose/mpp/{newbenchmarks => benchmarks}/benchmarks-app/src/jvmMain/kotlin/org/jetbrains/compose/benchmarks/app/StartupTiming.jvm.kt (100%) rename compose/mpp/{newbenchmarks => benchmarks}/benchmarks-app/src/macosMain/kotlin/org/jetbrains/compose/benchmarks/app/Main.macos.kt (100%) rename compose/mpp/{newbenchmarks => benchmarks}/benchmarks-app/src/webMain/kotlin/org/jetbrains/compose/benchmarks/app/Main.web.kt (100%) rename compose/mpp/{newbenchmarks => benchmarks}/benchmarks-app/src/webMain/kotlin/org/jetbrains/compose/benchmarks/app/StartupTiming.web.kt (100%) rename compose/mpp/{newbenchmarks => benchmarks}/benchmarks-app/src/webMain/resources/index.html (100%) rename compose/mpp/{newbenchmarks => benchmarks}/benchmarks-collector/build.gradle.kts (74%) rename compose/mpp/{newbenchmarks => benchmarks}/benchmarks-collector/src/main/kotlin/org/jetbrains/compose/benchmarks/collector/Main.kt (100%) rename compose/mpp/{newbenchmarks => benchmarks}/benchmarks-ios-app/BenchmarksIosApp.xcodeproj/project.pbxproj (96%) rename compose/mpp/{newbenchmarks => benchmarks}/benchmarks-ios-app/BenchmarksIosApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata (100%) create mode 100644 compose/mpp/benchmarks/benchmarks-ios-app/BenchmarksIosApp.xcodeproj/project.xcworkspace/xcuserdata/konstantin.tskhovrebov.xcuserdatad/xcschemes/xcschememanagement.plist rename compose/mpp/{newbenchmarks => benchmarks}/benchmarks-ios-app/BenchmarksIosApp.xcodeproj/xcshareddata/xcschemes/BenchmarksIosApp.xcscheme (100%) create mode 100644 compose/mpp/benchmarks/benchmarks-ios-app/BenchmarksIosApp.xcodeproj/xcuserdata/konstantin.tskhovrebov.xcuserdatad/xcschemes/xcschememanagement.plist rename compose/mpp/{newbenchmarks => benchmarks}/benchmarks-ios-app/BenchmarksIosApp/Assets.xcassets/AccentColor.colorset/Contents.json (100%) rename compose/mpp/{newbenchmarks => benchmarks}/benchmarks-ios-app/BenchmarksIosApp/Assets.xcassets/AppIcon.appiconset/Contents.json (100%) rename compose/mpp/{newbenchmarks => benchmarks}/benchmarks-ios-app/BenchmarksIosApp/Assets.xcassets/Contents.json (100%) rename compose/mpp/{newbenchmarks => benchmarks}/benchmarks-ios-app/BenchmarksIosApp/BenchmarksIosApp.swift (100%) rename compose/mpp/{newbenchmarks => benchmarks}/benchmarks-ios-app/BenchmarksIosApp/Info.plist (100%) rename compose/mpp/{newbenchmarks => benchmarks}/benchmarks-ios-app/BenchmarksIosApp/Preview Content/Preview Assets.xcassets/Contents.json (100%) rename compose/mpp/{newbenchmarks => benchmarks}/benchmarks-ios-app/README.md (80%) rename compose/mpp/{newbenchmarks => benchmarks}/benchmarks-report/build.gradle.kts (88%) rename compose/mpp/{newbenchmarks => benchmarks}/benchmarks-report/gradle.properties (100%) rename compose/mpp/{newbenchmarks => benchmarks}/benchmarks-report/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/report/BenchmarkReports.kt (100%) rename compose/mpp/{newbenchmarks => benchmarks}/benchmarks-runtime/build.gradle.kts (59%) rename compose/mpp/{newbenchmarks => benchmarks}/benchmarks-runtime/gradle.properties (100%) rename compose/mpp/{newbenchmarks => benchmarks}/benchmarks-runtime/src/appleMain/kotlin/org/jetbrains/compose/benchmarks/Platform.apple.kt (100%) rename compose/mpp/{newbenchmarks => benchmarks}/benchmarks-runtime/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/AppMeasurement.kt (100%) rename compose/mpp/{newbenchmarks => benchmarks}/benchmarks-runtime/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/BenchmarkComposeScene.kt (100%) rename compose/mpp/{newbenchmarks => benchmarks}/benchmarks-runtime/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/BenchmarkRunner.kt (100%) rename compose/mpp/{newbenchmarks => benchmarks}/benchmarks-runtime/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/ContentMeasurement.kt (100%) rename compose/mpp/{newbenchmarks => benchmarks}/benchmarks-runtime/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/GraphicsContext.kt (100%) rename compose/mpp/{newbenchmarks => benchmarks}/benchmarks-runtime/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/HttpClient.kt (100%) rename compose/mpp/{newbenchmarks => benchmarks}/benchmarks-runtime/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/Platform.kt (100%) rename compose/mpp/{newbenchmarks => benchmarks}/benchmarks-runtime/src/jvmMain/kotlin/org/jetbrains/compose/benchmarks/Platform.jvm.kt (100%) rename compose/mpp/{newbenchmarks => benchmarks}/benchmarks-runtime/src/webMain/kotlin/org/jetbrains/compose/benchmarks/Platform.web.kt (100%) create mode 100644 compose/mpp/benchmarks/build.gradle.kts create mode 100644 compose/mpp/benchmarks/gradle.properties create mode 100644 compose/mpp/benchmarks/gradle/libs.versions.toml create mode 100644 compose/mpp/benchmarks/gradle/wrapper/gradle-wrapper.jar create mode 100644 compose/mpp/benchmarks/gradle/wrapper/gradle-wrapper.properties create mode 100755 compose/mpp/benchmarks/gradlew create mode 100644 compose/mpp/benchmarks/gradlew.bat rename compose/mpp/{newbenchmarks => benchmarks}/launch-scripts/README.md (78%) rename compose/mpp/{newbenchmarks => benchmarks}/launch-scripts/run_benchmarks.py (95%) create mode 100644 compose/mpp/benchmarks/settings.gradle.kts diff --git a/compose/mpp/benchmarks/README.md b/compose/mpp/benchmarks/README.md new file mode 100644 index 0000000000000..e792963de3f3d --- /dev/null +++ b/compose/mpp/benchmarks/README.md @@ -0,0 +1,81 @@ +# Benchmarks + +This is a standalone Gradle project. It is not included in the core build. +Run the commands below from `compose/mpp/benchmarks`. + +## Versions and settings + +`gradle/libs.versions.toml` defines Kotlin `2.4.21`, the matching Compose Compiler plugin, +and Compose Multiplatform `1.13.0-beta02`. Change the catalog to compare other compatible versions. +Ktor and the supporting Kotlin libraries also have their own catalog entries. + +Repositories and Gradle/Kotlin properties follow the Demo project. The wrapper uses Gradle `9.5.0` +to match core: core's buildSrc currently fails with Gradle `9.7.1` deprecation warnings. +Configuration-cache problems are warnings, as in core, because its included build starts `xcrun` +during configuration. No Android target is configured for the benchmarks. + +## Published libraries + +By default, Compose libraries come from Maven repositories: + +```bash +./gradlew :benchmarks-app:compileKotlinJvm :benchmarks-collector:installDist --console=plain +python3 launch-scripts/run_benchmarks.py --target jvm --tests LazyGrid --modes averageFrame startup +``` + +The launcher builds release/production artifacts once, starts the collector, runs tests sequentially, +and closes each test after its report or timeout. See [launcher options](launch-scripts/README.md) +for browser, native macOS, and iOS Simulator runs. + +## Core sources with an included build + +Pass a core checkout path to replace Compose Maven dependencies with core projects: + +```bash +./gradlew :benchmarks-app:compileKotlinJvm -PcomposeCorePath=../../.. --console=plain +python3 launch-scripts/run_benchmarks.py \ + --target jvm --compose-core-path ../../.. --tests LazyGrid --modes averageFrame startup +``` + +Relative Gradle property paths are resolved from this project. Relative launcher paths are resolved +from the caller's working directory. An absolute path can select another checkout or worktree. +The launcher exports `ORG_GRADLE_PROJECT_composeCorePath` so Xcode's Gradle invocation receives it too. +For manual Xcode builds, set `composeCorePath` in this project's `gradle.properties` or export that +environment variable before opening Xcode. Omit the property to use published libraries. + +Substitution covers the core Compose runtime, UI, foundation, animation, material, and desktop modules. +Compose resources still use the published artifact: they are not built in the core repository. +You can inspect the JVM dependency graph with: + +```bash +./gradlew :benchmarks-app:dependencyInsight --configuration jvmRuntimeClasspath \ + --dependency org.jetbrains.compose.ui:ui -PcomposeCorePath=../../.. --console=plain +``` + +Kotlin in the benchmark project and Kotlin in core are configured independently. This project's +catalog does not override core's compiler or dependencies. Use a compatible core checkout and Gradle +version; rebuild the app when switching versions or switching between Maven and included-build modes. +Do not use `--skip-build` for that switch. + +## Manual collector and JVM app + +Start the collector in one terminal: + +```bash +./gradlew :benchmarks-collector:run --args="8090 $PWD/out/reports" --console=plain +``` + +Run a case in another terminal: + +```bash +./gradlew :benchmarks-app:run \ + --args="http://127.0.0.1:8090/reports LazyGrid averageFrame run-001" --console=plain +``` + +Application arguments are ` `. +Modes: `averageFrame`, `fps`, `realFrames`, `startup`. Use a unique ID for every run. +The collector listens on `127.0.0.1` and allows JSON POST requests from any origin. +Browser requests go directly to the collector, without a proxy. Loopback works for local browsers +and iOS Simulator, but not for a physical device connected to another machine. + +The [iOS app](benchmarks-ios-app/README.md) builds its framework from this standalone project. diff --git a/compose/mpp/newbenchmarks/benchmarks-app/build.gradle.kts b/compose/mpp/benchmarks/benchmarks-app/build.gradle.kts similarity index 69% rename from compose/mpp/newbenchmarks/benchmarks-app/build.gradle.kts rename to compose/mpp/benchmarks/benchmarks-app/build.gradle.kts index 967566b5f25a1..369bd9e83f9af 100644 --- a/compose/mpp/newbenchmarks/benchmarks-app/build.gradle.kts +++ b/compose/mpp/benchmarks/benchmarks-app/build.gradle.kts @@ -19,9 +19,9 @@ import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl plugins { - id("org.jetbrains.kotlin.multiplatform") - id("org.jetbrains.kotlin.plugin.compose") version libs.versions.kotlin.get() - id("org.jetbrains.compose") version "1.13.0-beta02" + alias(libs.plugins.kotlin.multiplatform) + alias(libs.plugins.compose.compiler) + alias(libs.plugins.compose.multiplatform) } kotlin { @@ -59,19 +59,16 @@ kotlin { sourceSets { commonMain.dependencies { - implementation(project(":compose:mpp:newbenchmarks:benchmarks-runtime")) - implementation(project(":compose:mpp:newbenchmarks:benchmarks-report")) - implementation(project(":compose:runtime:runtime")) - implementation(project(":compose:ui:ui")) - implementation(project(":compose:foundation:foundation")) - implementation(project(":compose:material:material")) - implementation("org.jetbrains.compose.components:components-resources:1.13.0-beta02") { - exclude(group = "org.jetbrains.compose.runtime") - exclude(group = "org.jetbrains.compose.ui") - } + implementation(project(":benchmarks-runtime")) + implementation(project(":benchmarks-report")) + implementation(libs.compose.runtime) + implementation(libs.compose.ui) + implementation(libs.compose.foundation) + implementation(libs.compose.material) + implementation(libs.compose.resources) } jvmMain.dependencies { - implementation(libs.skikoAwtRuntime) + implementation(compose.desktop.currentOs) implementation(libs.kotlinCoroutinesSwing) } webMain.dependencies { diff --git a/compose/mpp/newbenchmarks/benchmarks-app/gradle.properties b/compose/mpp/benchmarks/benchmarks-app/gradle.properties similarity index 100% rename from compose/mpp/newbenchmarks/benchmarks-app/gradle.properties rename to compose/mpp/benchmarks/benchmarks-app/gradle.properties diff --git a/compose/mpp/newbenchmarks/benchmarks-app/proguard-rules.pro b/compose/mpp/benchmarks/benchmarks-app/proguard-rules.pro similarity index 100% rename from compose/mpp/newbenchmarks/benchmarks-app/proguard-rules.pro rename to compose/mpp/benchmarks/benchmarks-app/proguard-rules.pro diff --git a/compose/mpp/newbenchmarks/benchmarks-app/src/appleMain/kotlin/org/jetbrains/compose/benchmarks/app/StartupTiming.apple.kt b/compose/mpp/benchmarks/benchmarks-app/src/appleMain/kotlin/org/jetbrains/compose/benchmarks/app/StartupTiming.apple.kt similarity index 100% rename from compose/mpp/newbenchmarks/benchmarks-app/src/appleMain/kotlin/org/jetbrains/compose/benchmarks/app/StartupTiming.apple.kt rename to compose/mpp/benchmarks/benchmarks-app/src/appleMain/kotlin/org/jetbrains/compose/benchmarks/app/StartupTiming.apple.kt diff --git a/compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/composeResources/drawable/compose-multiplatform.png b/compose/mpp/benchmarks/benchmarks-app/src/commonMain/composeResources/drawable/compose-multiplatform.png similarity index 100% rename from compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/composeResources/drawable/compose-multiplatform.png rename to compose/mpp/benchmarks/benchmarks-app/src/commonMain/composeResources/drawable/compose-multiplatform.png diff --git a/compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/composeResources/drawable/example1_cat.jpg b/compose/mpp/benchmarks/benchmarks-app/src/commonMain/composeResources/drawable/example1_cat.jpg similarity index 100% rename from compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/composeResources/drawable/example1_cat.jpg rename to compose/mpp/benchmarks/benchmarks-app/src/commonMain/composeResources/drawable/example1_cat.jpg diff --git a/compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/composeResources/drawable/example1_ic_call_answer.xml b/compose/mpp/benchmarks/benchmarks-app/src/commonMain/composeResources/drawable/example1_ic_call_answer.xml similarity index 100% rename from compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/composeResources/drawable/example1_ic_call_answer.xml rename to compose/mpp/benchmarks/benchmarks-app/src/commonMain/composeResources/drawable/example1_ic_call_answer.xml diff --git a/compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/composeResources/drawable/example1_sailing.svg b/compose/mpp/benchmarks/benchmarks-app/src/commonMain/composeResources/drawable/example1_sailing.svg similarity index 100% rename from compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/composeResources/drawable/example1_sailing.svg rename to compose/mpp/benchmarks/benchmarks-app/src/commonMain/composeResources/drawable/example1_sailing.svg diff --git a/compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/composeResources/files/example1_compose-community-primary.png b/compose/mpp/benchmarks/benchmarks-app/src/commonMain/composeResources/files/example1_compose-community-primary.png similarity index 100% rename from compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/composeResources/files/example1_compose-community-primary.png rename to compose/mpp/benchmarks/benchmarks-app/src/commonMain/composeResources/files/example1_compose-community-primary.png diff --git a/compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/composeResources/font/jetbrainsmono_italic.ttf b/compose/mpp/benchmarks/benchmarks-app/src/commonMain/composeResources/font/jetbrainsmono_italic.ttf similarity index 100% rename from compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/composeResources/font/jetbrainsmono_italic.ttf rename to compose/mpp/benchmarks/benchmarks-app/src/commonMain/composeResources/font/jetbrainsmono_italic.ttf diff --git a/compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/composeResources/font/jetbrainsmono_regular.ttf b/compose/mpp/benchmarks/benchmarks-app/src/commonMain/composeResources/font/jetbrainsmono_regular.ttf similarity index 100% rename from compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/composeResources/font/jetbrainsmono_regular.ttf rename to compose/mpp/benchmarks/benchmarks-app/src/commonMain/composeResources/font/jetbrainsmono_regular.ttf diff --git a/compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/App.kt b/compose/mpp/benchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/App.kt similarity index 100% rename from compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/App.kt rename to compose/mpp/benchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/App.kt diff --git a/compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/ArgumentsParser.kt b/compose/mpp/benchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/ArgumentsParser.kt similarity index 100% rename from compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/ArgumentsParser.kt rename to compose/mpp/benchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/ArgumentsParser.kt diff --git a/compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/RenderBenchmarks.kt b/compose/mpp/benchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/RenderBenchmarks.kt similarity index 100% rename from compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/RenderBenchmarks.kt rename to compose/mpp/benchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/RenderBenchmarks.kt diff --git a/compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/StartupTiming.kt b/compose/mpp/benchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/StartupTiming.kt similarity index 100% rename from compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/StartupTiming.kt rename to compose/mpp/benchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/StartupTiming.kt diff --git a/compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/animation/AnimatedVisibility.kt b/compose/mpp/benchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/animation/AnimatedVisibility.kt similarity index 100% rename from compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/animation/AnimatedVisibility.kt rename to compose/mpp/benchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/animation/AnimatedVisibility.kt diff --git a/compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/canvasdrawing/CanvasDrawing.kt b/compose/mpp/benchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/canvasdrawing/CanvasDrawing.kt similarity index 100% rename from compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/canvasdrawing/CanvasDrawing.kt rename to compose/mpp/benchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/canvasdrawing/CanvasDrawing.kt diff --git a/compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/complexlazylist/components/MainUI.kt b/compose/mpp/benchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/complexlazylist/components/MainUI.kt similarity index 100% rename from compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/complexlazylist/components/MainUI.kt rename to compose/mpp/benchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/complexlazylist/components/MainUI.kt diff --git a/compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/complexlazylist/components/MultiLevelCell.kt b/compose/mpp/benchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/complexlazylist/components/MultiLevelCell.kt similarity index 100% rename from compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/complexlazylist/components/MultiLevelCell.kt rename to compose/mpp/benchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/complexlazylist/components/MultiLevelCell.kt diff --git a/compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/complexlazylist/components/refresh/LoadingIndicator.kt b/compose/mpp/benchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/complexlazylist/components/refresh/LoadingIndicator.kt similarity index 100% rename from compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/complexlazylist/components/refresh/LoadingIndicator.kt rename to compose/mpp/benchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/complexlazylist/components/refresh/LoadingIndicator.kt diff --git a/compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/complexlazylist/components/refresh/SwipeProgress.kt b/compose/mpp/benchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/complexlazylist/components/refresh/SwipeProgress.kt similarity index 100% rename from compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/complexlazylist/components/refresh/SwipeProgress.kt rename to compose/mpp/benchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/complexlazylist/components/refresh/SwipeProgress.kt diff --git a/compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/complexlazylist/components/refresh/SwipeRefreshLayout.kt b/compose/mpp/benchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/complexlazylist/components/refresh/SwipeRefreshLayout.kt similarity index 100% rename from compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/complexlazylist/components/refresh/SwipeRefreshLayout.kt rename to compose/mpp/benchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/complexlazylist/components/refresh/SwipeRefreshLayout.kt diff --git a/compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/complexlazylist/components/refresh/SwipeRefreshNestedScrollConnection.kt b/compose/mpp/benchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/complexlazylist/components/refresh/SwipeRefreshNestedScrollConnection.kt similarity index 100% rename from compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/complexlazylist/components/refresh/SwipeRefreshNestedScrollConnection.kt rename to compose/mpp/benchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/complexlazylist/components/refresh/SwipeRefreshNestedScrollConnection.kt diff --git a/compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/complexlazylist/components/refresh/SwipeRefreshState.kt b/compose/mpp/benchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/complexlazylist/components/refresh/SwipeRefreshState.kt similarity index 100% rename from compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/complexlazylist/components/refresh/SwipeRefreshState.kt rename to compose/mpp/benchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/complexlazylist/components/refresh/SwipeRefreshState.kt diff --git a/compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/complexlazylist/models/FollowItem.kt b/compose/mpp/benchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/complexlazylist/models/FollowItem.kt similarity index 100% rename from compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/complexlazylist/models/FollowItem.kt rename to compose/mpp/benchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/complexlazylist/models/FollowItem.kt diff --git a/compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/complexlazylist/models/HotItem.kt b/compose/mpp/benchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/complexlazylist/models/HotItem.kt similarity index 100% rename from compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/complexlazylist/models/HotItem.kt rename to compose/mpp/benchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/complexlazylist/models/HotItem.kt diff --git a/compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/complexlazylist/models/IBaseViewModel.kt b/compose/mpp/benchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/complexlazylist/models/IBaseViewModel.kt similarity index 100% rename from compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/complexlazylist/models/IBaseViewModel.kt rename to compose/mpp/benchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/complexlazylist/models/IBaseViewModel.kt diff --git a/compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/complexlazylist/models/ICompositionModel.kt b/compose/mpp/benchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/complexlazylist/models/ICompositionModel.kt similarity index 100% rename from compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/complexlazylist/models/ICompositionModel.kt rename to compose/mpp/benchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/complexlazylist/models/ICompositionModel.kt diff --git a/compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/complexlazylist/models/ViewModelFactory.kt b/compose/mpp/benchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/complexlazylist/models/ViewModelFactory.kt similarity index 100% rename from compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/complexlazylist/models/ViewModelFactory.kt rename to compose/mpp/benchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/complexlazylist/models/ViewModelFactory.kt diff --git a/compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/heavyshader/HeavyShader.kt b/compose/mpp/benchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/heavyshader/HeavyShader.kt similarity index 100% rename from compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/heavyshader/HeavyShader.kt rename to compose/mpp/benchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/heavyshader/HeavyShader.kt diff --git a/compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/lazygrid/LazyGrid.kt b/compose/mpp/benchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/lazygrid/LazyGrid.kt similarity index 100% rename from compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/lazygrid/LazyGrid.kt rename to compose/mpp/benchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/lazygrid/LazyGrid.kt diff --git a/compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/multipleComponents/Clickable.kt b/compose/mpp/benchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/multipleComponents/Clickable.kt similarity index 100% rename from compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/multipleComponents/Clickable.kt rename to compose/mpp/benchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/multipleComponents/Clickable.kt diff --git a/compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/multipleComponents/MenuIcon.kt b/compose/mpp/benchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/multipleComponents/MenuIcon.kt similarity index 100% rename from compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/multipleComponents/MenuIcon.kt rename to compose/mpp/benchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/multipleComponents/MenuIcon.kt diff --git a/compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/multipleComponents/MultipleComponents.kt b/compose/mpp/benchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/multipleComponents/MultipleComponents.kt similarity index 100% rename from compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/multipleComponents/MultipleComponents.kt rename to compose/mpp/benchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/multipleComponents/MultipleComponents.kt diff --git a/compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/textlayout/TextLayout.kt b/compose/mpp/benchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/textlayout/TextLayout.kt similarity index 100% rename from compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/textlayout/TextLayout.kt rename to compose/mpp/benchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/textlayout/TextLayout.kt diff --git a/compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/visualeffects/HappyNY.kt b/compose/mpp/benchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/visualeffects/HappyNY.kt similarity index 100% rename from compose/mpp/newbenchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/visualeffects/HappyNY.kt rename to compose/mpp/benchmarks/benchmarks-app/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/app/benchmarks/visualeffects/HappyNY.kt diff --git a/compose/mpp/newbenchmarks/benchmarks-app/src/iosMain/kotlin/org/jetbrains/compose/benchmarks/app/Main.ios.kt b/compose/mpp/benchmarks/benchmarks-app/src/iosMain/kotlin/org/jetbrains/compose/benchmarks/app/Main.ios.kt similarity index 100% rename from compose/mpp/newbenchmarks/benchmarks-app/src/iosMain/kotlin/org/jetbrains/compose/benchmarks/app/Main.ios.kt rename to compose/mpp/benchmarks/benchmarks-app/src/iosMain/kotlin/org/jetbrains/compose/benchmarks/app/Main.ios.kt diff --git a/compose/mpp/newbenchmarks/benchmarks-app/src/jvmMain/kotlin/org/jetbrains/compose/benchmarks/app/Main.jvm.kt b/compose/mpp/benchmarks/benchmarks-app/src/jvmMain/kotlin/org/jetbrains/compose/benchmarks/app/Main.jvm.kt similarity index 100% rename from compose/mpp/newbenchmarks/benchmarks-app/src/jvmMain/kotlin/org/jetbrains/compose/benchmarks/app/Main.jvm.kt rename to compose/mpp/benchmarks/benchmarks-app/src/jvmMain/kotlin/org/jetbrains/compose/benchmarks/app/Main.jvm.kt diff --git a/compose/mpp/newbenchmarks/benchmarks-app/src/jvmMain/kotlin/org/jetbrains/compose/benchmarks/app/StartupTiming.jvm.kt b/compose/mpp/benchmarks/benchmarks-app/src/jvmMain/kotlin/org/jetbrains/compose/benchmarks/app/StartupTiming.jvm.kt similarity index 100% rename from compose/mpp/newbenchmarks/benchmarks-app/src/jvmMain/kotlin/org/jetbrains/compose/benchmarks/app/StartupTiming.jvm.kt rename to compose/mpp/benchmarks/benchmarks-app/src/jvmMain/kotlin/org/jetbrains/compose/benchmarks/app/StartupTiming.jvm.kt diff --git a/compose/mpp/newbenchmarks/benchmarks-app/src/macosMain/kotlin/org/jetbrains/compose/benchmarks/app/Main.macos.kt b/compose/mpp/benchmarks/benchmarks-app/src/macosMain/kotlin/org/jetbrains/compose/benchmarks/app/Main.macos.kt similarity index 100% rename from compose/mpp/newbenchmarks/benchmarks-app/src/macosMain/kotlin/org/jetbrains/compose/benchmarks/app/Main.macos.kt rename to compose/mpp/benchmarks/benchmarks-app/src/macosMain/kotlin/org/jetbrains/compose/benchmarks/app/Main.macos.kt diff --git a/compose/mpp/newbenchmarks/benchmarks-app/src/webMain/kotlin/org/jetbrains/compose/benchmarks/app/Main.web.kt b/compose/mpp/benchmarks/benchmarks-app/src/webMain/kotlin/org/jetbrains/compose/benchmarks/app/Main.web.kt similarity index 100% rename from compose/mpp/newbenchmarks/benchmarks-app/src/webMain/kotlin/org/jetbrains/compose/benchmarks/app/Main.web.kt rename to compose/mpp/benchmarks/benchmarks-app/src/webMain/kotlin/org/jetbrains/compose/benchmarks/app/Main.web.kt diff --git a/compose/mpp/newbenchmarks/benchmarks-app/src/webMain/kotlin/org/jetbrains/compose/benchmarks/app/StartupTiming.web.kt b/compose/mpp/benchmarks/benchmarks-app/src/webMain/kotlin/org/jetbrains/compose/benchmarks/app/StartupTiming.web.kt similarity index 100% rename from compose/mpp/newbenchmarks/benchmarks-app/src/webMain/kotlin/org/jetbrains/compose/benchmarks/app/StartupTiming.web.kt rename to compose/mpp/benchmarks/benchmarks-app/src/webMain/kotlin/org/jetbrains/compose/benchmarks/app/StartupTiming.web.kt diff --git a/compose/mpp/newbenchmarks/benchmarks-app/src/webMain/resources/index.html b/compose/mpp/benchmarks/benchmarks-app/src/webMain/resources/index.html similarity index 100% rename from compose/mpp/newbenchmarks/benchmarks-app/src/webMain/resources/index.html rename to compose/mpp/benchmarks/benchmarks-app/src/webMain/resources/index.html diff --git a/compose/mpp/newbenchmarks/benchmarks-collector/build.gradle.kts b/compose/mpp/benchmarks/benchmarks-collector/build.gradle.kts similarity index 74% rename from compose/mpp/newbenchmarks/benchmarks-collector/build.gradle.kts rename to compose/mpp/benchmarks/benchmarks-collector/build.gradle.kts index f2549c025dc0e..92e44aa8c2dab 100644 --- a/compose/mpp/newbenchmarks/benchmarks-collector/build.gradle.kts +++ b/compose/mpp/benchmarks/benchmarks-collector/build.gradle.kts @@ -15,15 +15,15 @@ */ plugins { - id("org.jetbrains.kotlin.jvm") + alias(libs.plugins.kotlin.jvm) application } dependencies { - implementation(project(":compose:mpp:newbenchmarks:benchmarks-report")) - implementation("io.ktor:ktor-server-core:3.6.1") - implementation("io.ktor:ktor-server-cio:3.6.1") - implementation("io.ktor:ktor-server-cors:3.6.1") + implementation(project(":benchmarks-report")) + implementation(libs.ktor.server.core) + implementation(libs.ktor.server.cio) + implementation(libs.ktor.server.cors) implementation(libs.kotlinSerializationJson) } diff --git a/compose/mpp/newbenchmarks/benchmarks-collector/src/main/kotlin/org/jetbrains/compose/benchmarks/collector/Main.kt b/compose/mpp/benchmarks/benchmarks-collector/src/main/kotlin/org/jetbrains/compose/benchmarks/collector/Main.kt similarity index 100% rename from compose/mpp/newbenchmarks/benchmarks-collector/src/main/kotlin/org/jetbrains/compose/benchmarks/collector/Main.kt rename to compose/mpp/benchmarks/benchmarks-collector/src/main/kotlin/org/jetbrains/compose/benchmarks/collector/Main.kt diff --git a/compose/mpp/newbenchmarks/benchmarks-ios-app/BenchmarksIosApp.xcodeproj/project.pbxproj b/compose/mpp/benchmarks/benchmarks-ios-app/BenchmarksIosApp.xcodeproj/project.pbxproj similarity index 96% rename from compose/mpp/newbenchmarks/benchmarks-ios-app/BenchmarksIosApp.xcodeproj/project.pbxproj rename to compose/mpp/benchmarks/benchmarks-ios-app/BenchmarksIosApp.xcodeproj/project.pbxproj index 018b0e4093559..01ce55d77d740 100644 --- a/compose/mpp/newbenchmarks/benchmarks-ios-app/BenchmarksIosApp.xcodeproj/project.pbxproj +++ b/compose/mpp/benchmarks/benchmarks-ios-app/BenchmarksIosApp.xcodeproj/project.pbxproj @@ -154,7 +154,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "cd \"$SRCROOT/../../../..\"\n./gradlew :compose:mpp:newbenchmarks:benchmarks-app:embedAndSignAppleFrameworkForXcode --console=plain\n"; + shellScript = "cd \"$SRCROOT/..\"\n./gradlew :benchmarks-app:embedAndSignAppleFrameworkForXcode --console=plain\n"; }; /* End PBXShellScriptBuildPhase section */ @@ -291,11 +291,10 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Automatic; - CMP_OUT_DIR = "$(SRCROOT)/../../../../out"; ENABLE_USER_SCRIPT_SANDBOXING = NO; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", - "$(CMP_OUT_DIR)/compose-multiplatform-core/compose/mpp/newbenchmarks/benchmarks-app/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)", + "$(SRCROOT)/../benchmarks-app/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)", ); OTHER_LDFLAGS = ( "$(inherited)", @@ -328,11 +327,10 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Automatic; - CMP_OUT_DIR = "$(SRCROOT)/../../../../out"; ENABLE_USER_SCRIPT_SANDBOXING = NO; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", - "$(CMP_OUT_DIR)/compose-multiplatform-core/compose/mpp/newbenchmarks/benchmarks-app/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)", + "$(SRCROOT)/../benchmarks-app/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)", ); OTHER_LDFLAGS = ( "$(inherited)", diff --git a/compose/mpp/newbenchmarks/benchmarks-ios-app/BenchmarksIosApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/compose/mpp/benchmarks/benchmarks-ios-app/BenchmarksIosApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata similarity index 100% rename from compose/mpp/newbenchmarks/benchmarks-ios-app/BenchmarksIosApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata rename to compose/mpp/benchmarks/benchmarks-ios-app/BenchmarksIosApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata diff --git a/compose/mpp/benchmarks/benchmarks-ios-app/BenchmarksIosApp.xcodeproj/project.xcworkspace/xcuserdata/konstantin.tskhovrebov.xcuserdatad/xcschemes/xcschememanagement.plist b/compose/mpp/benchmarks/benchmarks-ios-app/BenchmarksIosApp.xcodeproj/project.xcworkspace/xcuserdata/konstantin.tskhovrebov.xcuserdatad/xcschemes/xcschememanagement.plist new file mode 100644 index 0000000000000..ee3458dd76776 --- /dev/null +++ b/compose/mpp/benchmarks/benchmarks-ios-app/BenchmarksIosApp.xcodeproj/project.xcworkspace/xcuserdata/konstantin.tskhovrebov.xcuserdatad/xcschemes/xcschememanagement.plist @@ -0,0 +1,5 @@ + + + + + diff --git a/compose/mpp/newbenchmarks/benchmarks-ios-app/BenchmarksIosApp.xcodeproj/xcshareddata/xcschemes/BenchmarksIosApp.xcscheme b/compose/mpp/benchmarks/benchmarks-ios-app/BenchmarksIosApp.xcodeproj/xcshareddata/xcschemes/BenchmarksIosApp.xcscheme similarity index 100% rename from compose/mpp/newbenchmarks/benchmarks-ios-app/BenchmarksIosApp.xcodeproj/xcshareddata/xcschemes/BenchmarksIosApp.xcscheme rename to compose/mpp/benchmarks/benchmarks-ios-app/BenchmarksIosApp.xcodeproj/xcshareddata/xcschemes/BenchmarksIosApp.xcscheme diff --git a/compose/mpp/benchmarks/benchmarks-ios-app/BenchmarksIosApp.xcodeproj/xcuserdata/konstantin.tskhovrebov.xcuserdatad/xcschemes/xcschememanagement.plist b/compose/mpp/benchmarks/benchmarks-ios-app/BenchmarksIosApp.xcodeproj/xcuserdata/konstantin.tskhovrebov.xcuserdatad/xcschemes/xcschememanagement.plist new file mode 100644 index 0000000000000..ee3458dd76776 --- /dev/null +++ b/compose/mpp/benchmarks/benchmarks-ios-app/BenchmarksIosApp.xcodeproj/xcuserdata/konstantin.tskhovrebov.xcuserdatad/xcschemes/xcschememanagement.plist @@ -0,0 +1,5 @@ + + + + + diff --git a/compose/mpp/newbenchmarks/benchmarks-ios-app/BenchmarksIosApp/Assets.xcassets/AccentColor.colorset/Contents.json b/compose/mpp/benchmarks/benchmarks-ios-app/BenchmarksIosApp/Assets.xcassets/AccentColor.colorset/Contents.json similarity index 100% rename from compose/mpp/newbenchmarks/benchmarks-ios-app/BenchmarksIosApp/Assets.xcassets/AccentColor.colorset/Contents.json rename to compose/mpp/benchmarks/benchmarks-ios-app/BenchmarksIosApp/Assets.xcassets/AccentColor.colorset/Contents.json diff --git a/compose/mpp/newbenchmarks/benchmarks-ios-app/BenchmarksIosApp/Assets.xcassets/AppIcon.appiconset/Contents.json b/compose/mpp/benchmarks/benchmarks-ios-app/BenchmarksIosApp/Assets.xcassets/AppIcon.appiconset/Contents.json similarity index 100% rename from compose/mpp/newbenchmarks/benchmarks-ios-app/BenchmarksIosApp/Assets.xcassets/AppIcon.appiconset/Contents.json rename to compose/mpp/benchmarks/benchmarks-ios-app/BenchmarksIosApp/Assets.xcassets/AppIcon.appiconset/Contents.json diff --git a/compose/mpp/newbenchmarks/benchmarks-ios-app/BenchmarksIosApp/Assets.xcassets/Contents.json b/compose/mpp/benchmarks/benchmarks-ios-app/BenchmarksIosApp/Assets.xcassets/Contents.json similarity index 100% rename from compose/mpp/newbenchmarks/benchmarks-ios-app/BenchmarksIosApp/Assets.xcassets/Contents.json rename to compose/mpp/benchmarks/benchmarks-ios-app/BenchmarksIosApp/Assets.xcassets/Contents.json diff --git a/compose/mpp/newbenchmarks/benchmarks-ios-app/BenchmarksIosApp/BenchmarksIosApp.swift b/compose/mpp/benchmarks/benchmarks-ios-app/BenchmarksIosApp/BenchmarksIosApp.swift similarity index 100% rename from compose/mpp/newbenchmarks/benchmarks-ios-app/BenchmarksIosApp/BenchmarksIosApp.swift rename to compose/mpp/benchmarks/benchmarks-ios-app/BenchmarksIosApp/BenchmarksIosApp.swift diff --git a/compose/mpp/newbenchmarks/benchmarks-ios-app/BenchmarksIosApp/Info.plist b/compose/mpp/benchmarks/benchmarks-ios-app/BenchmarksIosApp/Info.plist similarity index 100% rename from compose/mpp/newbenchmarks/benchmarks-ios-app/BenchmarksIosApp/Info.plist rename to compose/mpp/benchmarks/benchmarks-ios-app/BenchmarksIosApp/Info.plist diff --git a/compose/mpp/newbenchmarks/benchmarks-ios-app/BenchmarksIosApp/Preview Content/Preview Assets.xcassets/Contents.json b/compose/mpp/benchmarks/benchmarks-ios-app/BenchmarksIosApp/Preview Content/Preview Assets.xcassets/Contents.json similarity index 100% rename from compose/mpp/newbenchmarks/benchmarks-ios-app/BenchmarksIosApp/Preview Content/Preview Assets.xcassets/Contents.json rename to compose/mpp/benchmarks/benchmarks-ios-app/BenchmarksIosApp/Preview Content/Preview Assets.xcassets/Contents.json diff --git a/compose/mpp/newbenchmarks/benchmarks-ios-app/README.md b/compose/mpp/benchmarks/benchmarks-ios-app/README.md similarity index 80% rename from compose/mpp/newbenchmarks/benchmarks-ios-app/README.md rename to compose/mpp/benchmarks/benchmarks-ios-app/README.md index 3061ba00f04f6..45d0fc4c549f2 100644 --- a/compose/mpp/newbenchmarks/benchmarks-ios-app/README.md +++ b/compose/mpp/benchmarks/benchmarks-ios-app/README.md @@ -28,8 +28,10 @@ See the [main README](../README.md) for collector setup and network limits. - Supported architectures: ARM64 device and ARM64 simulator, matching `benchmarks-app`. - For a physical device, select your development team under **Signing & Capabilities**. - User Script Sandboxing is disabled so Gradle can build the framework. -- Framework search paths use the repository's default `out` layout. If using a custom Gradle `OUT_DIR`, - override the Xcode `CMP_OUT_DIR` build setting to the same directory. +- Framework search paths use `../benchmarks-app/build/xcode-frameworks`, relative to this Xcode project. +- The Run Script invokes the standalone `benchmarks/gradlew`, not the core wrapper. +- To use core sources, set `composeCorePath` in `benchmarks/gradle.properties`, or export + `ORG_GRADLE_PROJECT_composeCorePath` before starting Xcode. Otherwise, libraries come from Maven. There is no separate Gradle module for this Xcode application; its Kotlin framework is built by -`:compose:mpp:newbenchmarks:benchmarks-app`. +`:benchmarks-app` in the standalone `benchmarks` Gradle build. diff --git a/compose/mpp/newbenchmarks/benchmarks-report/build.gradle.kts b/compose/mpp/benchmarks/benchmarks-report/build.gradle.kts similarity index 88% rename from compose/mpp/newbenchmarks/benchmarks-report/build.gradle.kts rename to compose/mpp/benchmarks/benchmarks-report/build.gradle.kts index e261490b3de7f..91bbd42dae2c1 100644 --- a/compose/mpp/newbenchmarks/benchmarks-report/build.gradle.kts +++ b/compose/mpp/benchmarks/benchmarks-report/build.gradle.kts @@ -19,8 +19,8 @@ import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl plugins { - id("org.jetbrains.kotlin.multiplatform") - id("org.jetbrains.kotlin.plugin.serialization") version libs.versions.kotlin.get() + alias(libs.plugins.kotlin.multiplatform) + alias(libs.plugins.kotlin.serialization) } kotlin { diff --git a/compose/mpp/newbenchmarks/benchmarks-report/gradle.properties b/compose/mpp/benchmarks/benchmarks-report/gradle.properties similarity index 100% rename from compose/mpp/newbenchmarks/benchmarks-report/gradle.properties rename to compose/mpp/benchmarks/benchmarks-report/gradle.properties diff --git a/compose/mpp/newbenchmarks/benchmarks-report/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/report/BenchmarkReports.kt b/compose/mpp/benchmarks/benchmarks-report/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/report/BenchmarkReports.kt similarity index 100% rename from compose/mpp/newbenchmarks/benchmarks-report/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/report/BenchmarkReports.kt rename to compose/mpp/benchmarks/benchmarks-report/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/report/BenchmarkReports.kt diff --git a/compose/mpp/newbenchmarks/benchmarks-runtime/build.gradle.kts b/compose/mpp/benchmarks/benchmarks-runtime/build.gradle.kts similarity index 59% rename from compose/mpp/newbenchmarks/benchmarks-runtime/build.gradle.kts rename to compose/mpp/benchmarks/benchmarks-runtime/build.gradle.kts index 6539eb619755e..510564890497a 100644 --- a/compose/mpp/newbenchmarks/benchmarks-runtime/build.gradle.kts +++ b/compose/mpp/benchmarks/benchmarks-runtime/build.gradle.kts @@ -19,9 +19,9 @@ import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl plugins { - id("org.jetbrains.kotlin.multiplatform") - id("org.jetbrains.kotlin.plugin.compose") version libs.versions.kotlin.get() - id("org.jetbrains.kotlin.plugin.serialization") version libs.versions.kotlin.get() + alias(libs.plugins.kotlin.multiplatform) + alias(libs.plugins.compose.compiler) + alias(libs.plugins.kotlin.serialization) } kotlin { @@ -38,20 +38,20 @@ kotlin { sourceSets { commonMain.dependencies { - compileOnly(project(":compose:ui:ui")) - implementation(project(":compose:mpp:newbenchmarks:benchmarks-report")) - implementation("io.ktor:ktor-client-core:3.6.1") - implementation("io.ktor:ktor-client-content-negotiation:3.6.1") - implementation("io.ktor:ktor-serialization-kotlinx-json:3.6.1") + compileOnly(libs.compose.ui) + implementation(project(":benchmarks-report")) + implementation(libs.ktor.client.core) + implementation(libs.ktor.client.content.negotiation) + implementation(libs.ktor.serialization.kotlinx.json) } jvmMain.dependencies { - implementation("io.ktor:ktor-client-cio:3.6.1") + implementation(libs.ktor.client.cio) } appleMain.dependencies { - implementation("io.ktor:ktor-client-darwin:3.6.1") + implementation(libs.ktor.client.darwin) } webMain.dependencies { - implementation("io.ktor:ktor-client-js:3.6.1") + implementation(libs.ktor.client.js) } } } diff --git a/compose/mpp/newbenchmarks/benchmarks-runtime/gradle.properties b/compose/mpp/benchmarks/benchmarks-runtime/gradle.properties similarity index 100% rename from compose/mpp/newbenchmarks/benchmarks-runtime/gradle.properties rename to compose/mpp/benchmarks/benchmarks-runtime/gradle.properties diff --git a/compose/mpp/newbenchmarks/benchmarks-runtime/src/appleMain/kotlin/org/jetbrains/compose/benchmarks/Platform.apple.kt b/compose/mpp/benchmarks/benchmarks-runtime/src/appleMain/kotlin/org/jetbrains/compose/benchmarks/Platform.apple.kt similarity index 100% rename from compose/mpp/newbenchmarks/benchmarks-runtime/src/appleMain/kotlin/org/jetbrains/compose/benchmarks/Platform.apple.kt rename to compose/mpp/benchmarks/benchmarks-runtime/src/appleMain/kotlin/org/jetbrains/compose/benchmarks/Platform.apple.kt diff --git a/compose/mpp/newbenchmarks/benchmarks-runtime/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/AppMeasurement.kt b/compose/mpp/benchmarks/benchmarks-runtime/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/AppMeasurement.kt similarity index 100% rename from compose/mpp/newbenchmarks/benchmarks-runtime/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/AppMeasurement.kt rename to compose/mpp/benchmarks/benchmarks-runtime/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/AppMeasurement.kt diff --git a/compose/mpp/newbenchmarks/benchmarks-runtime/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/BenchmarkComposeScene.kt b/compose/mpp/benchmarks/benchmarks-runtime/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/BenchmarkComposeScene.kt similarity index 100% rename from compose/mpp/newbenchmarks/benchmarks-runtime/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/BenchmarkComposeScene.kt rename to compose/mpp/benchmarks/benchmarks-runtime/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/BenchmarkComposeScene.kt diff --git a/compose/mpp/newbenchmarks/benchmarks-runtime/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/BenchmarkRunner.kt b/compose/mpp/benchmarks/benchmarks-runtime/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/BenchmarkRunner.kt similarity index 100% rename from compose/mpp/newbenchmarks/benchmarks-runtime/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/BenchmarkRunner.kt rename to compose/mpp/benchmarks/benchmarks-runtime/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/BenchmarkRunner.kt diff --git a/compose/mpp/newbenchmarks/benchmarks-runtime/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/ContentMeasurement.kt b/compose/mpp/benchmarks/benchmarks-runtime/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/ContentMeasurement.kt similarity index 100% rename from compose/mpp/newbenchmarks/benchmarks-runtime/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/ContentMeasurement.kt rename to compose/mpp/benchmarks/benchmarks-runtime/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/ContentMeasurement.kt diff --git a/compose/mpp/newbenchmarks/benchmarks-runtime/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/GraphicsContext.kt b/compose/mpp/benchmarks/benchmarks-runtime/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/GraphicsContext.kt similarity index 100% rename from compose/mpp/newbenchmarks/benchmarks-runtime/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/GraphicsContext.kt rename to compose/mpp/benchmarks/benchmarks-runtime/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/GraphicsContext.kt diff --git a/compose/mpp/newbenchmarks/benchmarks-runtime/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/HttpClient.kt b/compose/mpp/benchmarks/benchmarks-runtime/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/HttpClient.kt similarity index 100% rename from compose/mpp/newbenchmarks/benchmarks-runtime/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/HttpClient.kt rename to compose/mpp/benchmarks/benchmarks-runtime/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/HttpClient.kt diff --git a/compose/mpp/newbenchmarks/benchmarks-runtime/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/Platform.kt b/compose/mpp/benchmarks/benchmarks-runtime/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/Platform.kt similarity index 100% rename from compose/mpp/newbenchmarks/benchmarks-runtime/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/Platform.kt rename to compose/mpp/benchmarks/benchmarks-runtime/src/commonMain/kotlin/org/jetbrains/compose/benchmarks/Platform.kt diff --git a/compose/mpp/newbenchmarks/benchmarks-runtime/src/jvmMain/kotlin/org/jetbrains/compose/benchmarks/Platform.jvm.kt b/compose/mpp/benchmarks/benchmarks-runtime/src/jvmMain/kotlin/org/jetbrains/compose/benchmarks/Platform.jvm.kt similarity index 100% rename from compose/mpp/newbenchmarks/benchmarks-runtime/src/jvmMain/kotlin/org/jetbrains/compose/benchmarks/Platform.jvm.kt rename to compose/mpp/benchmarks/benchmarks-runtime/src/jvmMain/kotlin/org/jetbrains/compose/benchmarks/Platform.jvm.kt diff --git a/compose/mpp/newbenchmarks/benchmarks-runtime/src/webMain/kotlin/org/jetbrains/compose/benchmarks/Platform.web.kt b/compose/mpp/benchmarks/benchmarks-runtime/src/webMain/kotlin/org/jetbrains/compose/benchmarks/Platform.web.kt similarity index 100% rename from compose/mpp/newbenchmarks/benchmarks-runtime/src/webMain/kotlin/org/jetbrains/compose/benchmarks/Platform.web.kt rename to compose/mpp/benchmarks/benchmarks-runtime/src/webMain/kotlin/org/jetbrains/compose/benchmarks/Platform.web.kt diff --git a/compose/mpp/benchmarks/build.gradle.kts b/compose/mpp/benchmarks/build.gradle.kts new file mode 100644 index 0000000000000..a42513de51ff8 --- /dev/null +++ b/compose/mpp/benchmarks/build.gradle.kts @@ -0,0 +1,7 @@ +plugins { + alias(libs.plugins.kotlin.multiplatform).apply(false) + alias(libs.plugins.kotlin.jvm).apply(false) + alias(libs.plugins.kotlin.serialization).apply(false) + alias(libs.plugins.compose.compiler).apply(false) + alias(libs.plugins.compose.multiplatform).apply(false) +} diff --git a/compose/mpp/benchmarks/gradle.properties b/compose/mpp/benchmarks/gradle.properties new file mode 100644 index 0000000000000..e09d6b87b0db9 --- /dev/null +++ b/compose/mpp/benchmarks/gradle.properties @@ -0,0 +1,21 @@ +#Gradle +org.gradle.jvmargs=-Xmx4G +org.gradle.caching=true +org.gradle.configuration-cache=true +# Core currently starts xcrun during configuration when used as an included build. +org.gradle.configuration-cache.problems=warn +org.gradle.daemon=true +org.gradle.parallel=true + +#Kotlin +kotlin.daemon.jvmargs=-Xmx4G +kotlin.native.binary.gc=cms + +#Compose +org.jetbrains.compose.experimental.macos.enabled=true + +#Android +android.useAndroidX=true + +# Enabled parallel sync for Gradle 9.4+ +org.gradle.tooling.parallel=true diff --git a/compose/mpp/benchmarks/gradle/libs.versions.toml b/compose/mpp/benchmarks/gradle/libs.versions.toml new file mode 100644 index 0000000000000..a906d9c58fc6f --- /dev/null +++ b/compose/mpp/benchmarks/gradle/libs.versions.toml @@ -0,0 +1,33 @@ +[versions] +kotlin = "2.4.21" +compose-multiplatform = "1.13.0-beta02" +coroutines = "1.9.0" +serialization = "1.8.0" +ktor = "3.6.1" + +[libraries] +compose-runtime = { module = "org.jetbrains.compose.runtime:runtime", version.ref = "compose-multiplatform" } +compose-ui = { module = "org.jetbrains.compose.ui:ui", version.ref = "compose-multiplatform" } +compose-foundation = { module = "org.jetbrains.compose.foundation:foundation", version.ref = "compose-multiplatform" } +compose-material = { module = "org.jetbrains.compose.material:material", version.ref = "compose-multiplatform" } +compose-resources = { module = "org.jetbrains.compose.components:components-resources", version.ref = "compose-multiplatform" } +kotlinCoroutinesSwing = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-swing", version.ref = "coroutines" } +kotlinSerializationCore = { module = "org.jetbrains.kotlinx:kotlinx-serialization-core", version.ref = "serialization" } +kotlinSerializationJson = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "serialization" } +kotlinXw3c = { module = "org.jetbrains.kotlinx:kotlinx-browser", version = "0.5.0" } +ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" } +ktor-client-content-negotiation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktor" } +ktor-serialization-kotlinx-json = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor" } +ktor-client-cio = { module = "io.ktor:ktor-client-cio", version.ref = "ktor" } +ktor-client-darwin = { module = "io.ktor:ktor-client-darwin", version.ref = "ktor" } +ktor-client-js = { module = "io.ktor:ktor-client-js", version.ref = "ktor" } +ktor-server-core = { module = "io.ktor:ktor-server-core", version.ref = "ktor" } +ktor-server-cio = { module = "io.ktor:ktor-server-cio", version.ref = "ktor" } +ktor-server-cors = { module = "io.ktor:ktor-server-cors", version.ref = "ktor" } + +[plugins] +kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } +kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } +kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } +compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } +compose-multiplatform = { id = "org.jetbrains.compose", version.ref = "compose-multiplatform" } diff --git a/compose/mpp/benchmarks/gradle/wrapper/gradle-wrapper.jar b/compose/mpp/benchmarks/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..f8e1ee3125fe0768e9a76ee977ac089eb657005e GIT binary patch literal 45633 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�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 literal 0 HcmV?d00001 diff --git a/compose/mpp/benchmarks/gradle/wrapper/gradle-wrapper.properties b/compose/mpp/benchmarks/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000000000..a04cb0ade7ce9 --- /dev/null +++ b/compose/mpp/benchmarks/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,8 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.0-bin.zip +distributionSha256Sum=553c78f50dafcd54d65b9a444649057857469edf836431389695608536d6b746 +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/compose/mpp/benchmarks/gradlew b/compose/mpp/benchmarks/gradlew new file mode 100755 index 0000000000000..c54dc4036c186 --- /dev/null +++ b/compose/mpp/benchmarks/gradlew @@ -0,0 +1,248 @@ +#!/bin/sh + +# +# Copyright © 2015 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/compose/mpp/benchmarks/gradlew.bat b/compose/mpp/benchmarks/gradlew.bat new file mode 100644 index 0000000000000..c4bdd3ab8e3cc --- /dev/null +++ b/compose/mpp/benchmarks/gradlew.bat @@ -0,0 +1,93 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/compose/mpp/newbenchmarks/launch-scripts/README.md b/compose/mpp/benchmarks/launch-scripts/README.md similarity index 78% rename from compose/mpp/newbenchmarks/launch-scripts/README.md rename to compose/mpp/benchmarks/launch-scripts/README.md index f6e657ca516a3..227edd28659bc 100644 --- a/compose/mpp/newbenchmarks/launch-scripts/README.md +++ b/compose/mpp/benchmarks/launch-scripts/README.md @@ -3,7 +3,9 @@ `run_benchmarks.py` uses Python 3 and its standard library. No Playwright or Python packages are needed. It runs on macOS and Linux. Apple targets require macOS and Xcode. -Run these examples from the repository root. The script also works from another working directory. +Run these examples from the core repository root. The script also works from another working directory. +It invokes the standalone wrapper in `compose/mpp/benchmarks`, not the core build. +From the standalone project root, use `python3 launch-scripts/run_benchmarks.py` instead. ## Defaults @@ -13,6 +15,8 @@ Run these examples from the repository root. The script also works from another - `--repeat 1`, `--timeout 30` seconds per test, collector `--port 8090`. - Build the collector and selected app once before the series. Apps use release/production artifacts. Build time is not part of the test timeout. Use `--skip-build` to reuse existing artifacts. +- Compose libraries come from Maven by default. Use `--compose-core-path /path/to/core` for an included + build of core sources. Rebuild when changing versions or switching dependency modes. - Continue after failed tests and timeouts. Exit with code 1 if any test fails; Ctrl+C exits with code 130. - Reports go directly into one shared `reports` folder. The script waits for a matching report ID, name, and type, then closes the test process before starting the next one. @@ -27,13 +31,13 @@ Do not run two launchers against the same simulator application at the same time Run everything: ```bash -python3 compose/mpp/newbenchmarks/launch-scripts/run_benchmarks.py --target jvm +python3 compose/mpp/benchmarks/launch-scripts/run_benchmarks.py --target jvm ``` Select cases and modes, with three repetitions of each combination: ```bash -python3 compose/mpp/newbenchmarks/launch-scripts/run_benchmarks.py \ +python3 compose/mpp/benchmarks/launch-scripts/run_benchmarks.py \ --target jvm --tests LazyGrid TextLayout --modes averageFrame startup --repeat 3 ``` @@ -43,7 +47,7 @@ The JVM app is launched directly from its release uber-JAR, not through a Gradle ## Native macOS (ARM64) ```bash -python3 compose/mpp/newbenchmarks/launch-scripts/run_benchmarks.py \ +python3 compose/mpp/benchmarks/launch-scripts/run_benchmarks.py \ --target macos --tests LazyGrid --modes averageFrame fps ``` @@ -58,7 +62,7 @@ closed after a report, crash, or timeout. Browser launches are visible by defaul Chromium: ```bash -python3 compose/mpp/newbenchmarks/launch-scripts/run_benchmarks.py \ +python3 compose/mpp/benchmarks/launch-scripts/run_benchmarks.py \ --target wasmJs --browser "/Applications/Google Chrome.app" \ --tests LazyGrid --modes startup averageFrame ``` @@ -66,7 +70,7 @@ python3 compose/mpp/newbenchmarks/launch-scripts/run_benchmarks.py \ Firefox: ```bash -python3 compose/mpp/newbenchmarks/launch-scripts/run_benchmarks.py \ +python3 compose/mpp/benchmarks/launch-scripts/run_benchmarks.py \ --target js --browser "/Applications/Firefox.app" \ --tests LazyGrid --modes averageFrame ``` @@ -74,7 +78,7 @@ python3 compose/mpp/newbenchmarks/launch-scripts/run_benchmarks.py \ Safari (macOS only): ```bash -python3 compose/mpp/newbenchmarks/launch-scripts/run_benchmarks.py \ +python3 compose/mpp/benchmarks/launch-scripts/run_benchmarks.py \ --target js --browser "/Applications/Safari.app" \ --tests LazyGrid --modes startup --repeat 3 ``` @@ -99,14 +103,14 @@ The browser receives all application parameters in the page URL. Without `--simulator`, exactly one available simulator must already be booted: ```bash -python3 compose/mpp/newbenchmarks/launch-scripts/run_benchmarks.py \ +python3 compose/mpp/benchmarks/launch-scripts/run_benchmarks.py \ --target ios --tests LazyGrid --modes startup ``` Or select a simulator by UUID (recommended) or a unique name. The launcher boots it if necessary: ```bash -python3 compose/mpp/newbenchmarks/launch-scripts/run_benchmarks.py \ +python3 compose/mpp/benchmarks/launch-scripts/run_benchmarks.py \ --target ios --simulator YOUR_SIMULATOR_UUID --tests LazyGrid --modes startup --repeat 3 ``` @@ -120,15 +124,17 @@ Physical iOS devices are not supported by this launcher. By default, each series creates a new directory: ```text -out/benchmark-reports/--/ +compose/mpp/benchmarks/out/benchmark-reports/--/ reports/ # All collected benchmark JSON files, prefixed with the run UUID logs/ # Build logs, collector log, and a separate log for each run UUID summary.json # Parameters, statuses, timings, report paths, and errors ``` Set `--output-dir /path/to/new-directory` to choose the series directory. It must not already exist. -Use `--port 18090` if port 8090 is occupied. `OUT_DIR` is respected for Gradle artifacts and default output. -The default iOS DerivedData path is `$OUT_DIR/benchmarks-ios-derived` (or `out/benchmarks-ios-derived`). +Use `--port 18090` if port 8090 is occupied. `OUT_DIR` selects the default reports and iOS DerivedData +root; its default is the standalone project's `out` directory. Relative `OUT_DIR` values are resolved +from that project. Gradle artifacts use the normal `/build` layout, independent of `OUT_DIR`. +The default iOS DerivedData path is `$OUT_DIR/benchmarks-ios-derived` (or the project's `out/benchmarks-ios-derived`). A test timeout limits launch/report waiting; process cleanup may take a few extra seconds. Reports already collected are kept after failures or interruption. Inspect `summary.json` and the diff --git a/compose/mpp/newbenchmarks/launch-scripts/run_benchmarks.py b/compose/mpp/benchmarks/launch-scripts/run_benchmarks.py similarity index 95% rename from compose/mpp/newbenchmarks/launch-scripts/run_benchmarks.py rename to compose/mpp/benchmarks/launch-scripts/run_benchmarks.py index 7626e2fafaf0f..0fcf846db25c1 100755 --- a/compose/mpp/newbenchmarks/launch-scripts/run_benchmarks.py +++ b/compose/mpp/benchmarks/launch-scripts/run_benchmarks.py @@ -37,10 +37,9 @@ import uuid -REPO = Path(__file__).resolve().parents[4] -MODULES = REPO / "compose/mpp/newbenchmarks" -APP_TASK = ":compose:mpp:newbenchmarks:benchmarks-app:" -COLLECTOR_TASK = ":compose:mpp:newbenchmarks:benchmarks-collector:" +ROOT = Path(__file__).resolve().parents[1] +APP_TASK = ":benchmarks-app:" +COLLECTOR_TASK = ":benchmarks-collector:" BUNDLE_ID = "org.jetbrains.compose.benchmarks.app.BenchmarksIosApp" TESTS = ( "AnimatedVisibility", "LazyGrid", "LazyGrid-ItemLaunchedEffect", "LazyGrid-SmoothScroll", @@ -60,6 +59,7 @@ def parse_args(): parser.add_argument("--timeout", type=float, default=30, help="seconds per test (default: 30)") parser.add_argument("--output-dir", type=Path, help="new directory for this series") parser.add_argument("--skip-build", action="store_true", help="reuse existing release artifacts") + parser.add_argument("--compose-core-path", type=Path, help="include a core checkout instead of published Compose libraries") parser.add_argument("--browser", help="Chromium/Firefox/Safari executable, command name, or macOS .app") parser.add_argument("--browser-arg", action="append", default=[], help="extra browser flag; use --browser-arg=--flag") parser.add_argument("--web-port", type=int, default=0, help="static file server port (default: automatic)") @@ -116,7 +116,7 @@ def managed_process(command, log_path): with log_path.open("ab", buffering=0) as log: log.write(("$ " + repr([str(arg) for arg in command]) + "\n").encode()) process = subprocess.Popen( - [str(arg) for arg in command], cwd=REPO, stdout=log, + [str(arg) for arg in command], cwd=ROOT, stdout=log, stderr=subprocess.STDOUT, start_new_session=True, ) try: @@ -244,13 +244,13 @@ def build(args, out, logs, simulator): "wasmJs": [APP_TASK + "wasmJsBrowserDistribution"], "ios": [], }[args.target] - run_command([REPO / "gradlew", *tasks, "--console=plain"], logs / "gradle-build.log") + run_command([ROOT / "gradlew", *tasks, "--console=plain"], logs / "gradle-build.log") if args.target == "ios": run_command([ - "xcodebuild", "-project", MODULES / "benchmarks-ios-app/BenchmarksIosApp.xcodeproj", + "xcodebuild", "-project", ROOT / "benchmarks-ios-app/BenchmarksIosApp.xcodeproj", "-scheme", "BenchmarksIosApp", "-configuration", "Release", "-sdk", "iphonesimulator", "-destination", f"id={simulator['udid']}", "-derivedDataPath", out / "benchmarks-ios-derived", - "ARCHS=arm64", "CODE_SIGNING_ALLOWED=NO", f"CMP_OUT_DIR={out}", "build", + "ARCHS=arm64", "CODE_SIGNING_ALLOWED=NO", "build", ], logs / "xcode-build.log") @@ -379,8 +379,8 @@ def run_series(args, out, directory, summary): logs, reports = directory / "logs", directory / "reports" logs.mkdir() reports.mkdir() - app_build = out / "compose-multiplatform-core/compose/mpp/newbenchmarks/benchmarks-app/build" - collector_build = out / "compose-multiplatform-core/compose/mpp/newbenchmarks/benchmarks-collector/build" + app_build = ROOT / "benchmarks-app/build" + collector_build = ROOT / "benchmarks-collector/build" simulator = select_simulator(args.simulator) if args.target == "ios" else None browser = executable(args.browser) if args.target in ("js", "wasmJs") else None if browser: @@ -454,15 +454,21 @@ def handle_termination(signum, frame): def main(): args = parse_args() signal.signal(signal.SIGTERM, handle_termination) - out = Path(os.environ.get("OUT_DIR", REPO / "out")).expanduser() + if args.compose_core_path: + args.compose_core_path = args.compose_core_path.expanduser().resolve() + # Also reaches the Gradle invocation in the Xcode framework build phase. + os.environ["ORG_GRADLE_PROJECT_composeCorePath"] = str(args.compose_core_path) + out = Path(os.environ.get("OUT_DIR", ROOT / "out")).expanduser() if not out.is_absolute(): - out = REPO / out + out = ROOT / out out = out.resolve() stamp = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ") directory = (args.output_dir or out / "benchmark-reports" / f"{stamp}-{args.target}-{uuid.uuid4().hex[:8]}").expanduser().resolve() directory.mkdir(parents=True, exist_ok=False) summary = {"target": args.target, "tests": args.tests, "modes": args.modes, "repeat": args.repeat, "timeoutSeconds": args.timeout, "runs": []} + if args.compose_core_path: + summary["composeCorePath"] = str(args.compose_core_path) print(f"Results: {directory}", flush=True) exit_code = 0 try: diff --git a/compose/mpp/benchmarks/settings.gradle.kts b/compose/mpp/benchmarks/settings.gradle.kts new file mode 100644 index 0000000000000..bd9aa0a427c6c --- /dev/null +++ b/compose/mpp/benchmarks/settings.gradle.kts @@ -0,0 +1,64 @@ +pluginManagement { + repositories { + google { + content { + includeGroupByRegex("com\\.android.*") + includeGroupByRegex("com\\.google.*") + includeGroupByRegex("androidx.*") + includeGroupByRegex("android.*") + } + } + gradlePluginPortal() + mavenCentral() + maven("https://packages.jetbrains.team/maven/p/cmp/dev") + maven("https://packages.jetbrains.team/maven/p/kt/dev") + } +} + +dependencyResolutionManagement { + repositories { + google { + content { + includeGroupByRegex("com\\.android.*") + includeGroupByRegex("com\\.google.*") + includeGroupByRegex("androidx.*") + includeGroupByRegex("android.*") + } + } + mavenCentral() + maven("https://packages.jetbrains.team/maven/p/cmp/dev") + maven("https://packages.jetbrains.team/maven/p/kt/dev") + } +} + +include(":benchmarks-runtime") +include(":benchmarks-report") +include(":benchmarks-collector") +include(":benchmarks-app") + +// Omit this property to benchmark published Compose libraries. +val composeCorePath = providers.gradleProperty("composeCorePath").orNull +if (composeCorePath != null) { + includeBuild(composeCorePath) { + dependencySubstitution { + // Explicit mappings are needed because core's project groups differ from Maven groups. + val components = mapOf( + "animation" to listOf("animation", "animation-core", "animation-graphics"), + "foundation" to listOf("foundation", "foundation-layout"), + "material" to listOf("material", "material-navigation", "material-ripple"), + "runtime" to listOf("runtime", "runtime-saveable"), + "ui" to listOf( + "ui", "ui-geometry", "ui-backhandler", "ui-graphics", "ui-skiko", "ui-text", + "ui-tooling", "ui-tooling-data", "ui-tooling-preview", "ui-uikit", "ui-unit", "ui-util", + ), + "desktop" to listOf("desktop"), + ) + components.forEach { (group, names) -> + names.forEach { artifact -> + substitute(module("org.jetbrains.compose.$group:$artifact")) + .using(project(":compose:$group:$artifact")) + } + } + } + } +} diff --git a/settings-fork.gradle b/settings-fork.gradle index 35a200417624f..06f192a2c2101 100644 --- a/settings-fork.gradle +++ b/settings-fork.gradle @@ -338,10 +338,6 @@ includeProject(":compose:animation:animation-graphics") includeProject(":compose:desktop:desktop") includeProject(":compose:desktop:desktop:desktop-samples", "compose/desktop/desktop/samples") includeProject(":compose:mpp") -includeProject(":compose:mpp:newbenchmarks:benchmarks-runtime") -includeProject(":compose:mpp:newbenchmarks:benchmarks-report") -includeProject(":compose:mpp:newbenchmarks:benchmarks-collector") -includeProject(":compose:mpp:newbenchmarks:benchmarks-app") includeProject(":compose:mpp:demo") includeProject(":compose:mpp:demo-swiftui") includeProject(":compose:foundation:foundation") From b0729c625be749b61234a445cc1af87239d82c49 Mon Sep 17 00:00:00 2001 From: Konstantin Date: Wed, 11 Nov 2026 11:16:16 +0100 Subject: [PATCH 22/61] Implement benchmark launcher --- compose/mpp/benchmarks/README.md | 81 --- .../project.pbxproj | 2 +- ...nchmarks-ios-app.BenchmarksIosApp.xcscheme | 32 ++ .../benchmarks-launcher/build.gradle.kts | 33 ++ .../benchmarks/launcher/Applications.kt | 237 +++++++++ .../compose/benchmarks/launcher/Main.kt | 434 +++++++++++++++ .../compose/benchmarks/launcher/Processes.kt | 141 +++++ .../compose/benchmarks/launcher/WebServer.kt | 75 +++ .../mpp/benchmarks/gradle/libs.versions.toml | 2 + .../mpp/benchmarks/launch-scripts/README.md | 141 ----- .../launch-scripts/run_benchmarks.py | 492 ------------------ compose/mpp/benchmarks/settings.gradle.kts | 1 + 12 files changed, 956 insertions(+), 715 deletions(-) delete mode 100644 compose/mpp/benchmarks/README.md create mode 100644 compose/mpp/benchmarks/benchmarks-ios-app/BenchmarksIosApp.xcodeproj/xcuserdata/konstantin.tskhovrebov.xcuserdatad/xcschemes/benchmarks-ios-app.BenchmarksIosApp.xcscheme create mode 100644 compose/mpp/benchmarks/benchmarks-launcher/build.gradle.kts create mode 100644 compose/mpp/benchmarks/benchmarks-launcher/src/main/kotlin/org/jetbrains/compose/benchmarks/launcher/Applications.kt create mode 100644 compose/mpp/benchmarks/benchmarks-launcher/src/main/kotlin/org/jetbrains/compose/benchmarks/launcher/Main.kt create mode 100644 compose/mpp/benchmarks/benchmarks-launcher/src/main/kotlin/org/jetbrains/compose/benchmarks/launcher/Processes.kt create mode 100644 compose/mpp/benchmarks/benchmarks-launcher/src/main/kotlin/org/jetbrains/compose/benchmarks/launcher/WebServer.kt delete mode 100644 compose/mpp/benchmarks/launch-scripts/README.md delete mode 100755 compose/mpp/benchmarks/launch-scripts/run_benchmarks.py diff --git a/compose/mpp/benchmarks/README.md b/compose/mpp/benchmarks/README.md deleted file mode 100644 index e792963de3f3d..0000000000000 --- a/compose/mpp/benchmarks/README.md +++ /dev/null @@ -1,81 +0,0 @@ -# Benchmarks - -This is a standalone Gradle project. It is not included in the core build. -Run the commands below from `compose/mpp/benchmarks`. - -## Versions and settings - -`gradle/libs.versions.toml` defines Kotlin `2.4.21`, the matching Compose Compiler plugin, -and Compose Multiplatform `1.13.0-beta02`. Change the catalog to compare other compatible versions. -Ktor and the supporting Kotlin libraries also have their own catalog entries. - -Repositories and Gradle/Kotlin properties follow the Demo project. The wrapper uses Gradle `9.5.0` -to match core: core's buildSrc currently fails with Gradle `9.7.1` deprecation warnings. -Configuration-cache problems are warnings, as in core, because its included build starts `xcrun` -during configuration. No Android target is configured for the benchmarks. - -## Published libraries - -By default, Compose libraries come from Maven repositories: - -```bash -./gradlew :benchmarks-app:compileKotlinJvm :benchmarks-collector:installDist --console=plain -python3 launch-scripts/run_benchmarks.py --target jvm --tests LazyGrid --modes averageFrame startup -``` - -The launcher builds release/production artifacts once, starts the collector, runs tests sequentially, -and closes each test after its report or timeout. See [launcher options](launch-scripts/README.md) -for browser, native macOS, and iOS Simulator runs. - -## Core sources with an included build - -Pass a core checkout path to replace Compose Maven dependencies with core projects: - -```bash -./gradlew :benchmarks-app:compileKotlinJvm -PcomposeCorePath=../../.. --console=plain -python3 launch-scripts/run_benchmarks.py \ - --target jvm --compose-core-path ../../.. --tests LazyGrid --modes averageFrame startup -``` - -Relative Gradle property paths are resolved from this project. Relative launcher paths are resolved -from the caller's working directory. An absolute path can select another checkout or worktree. -The launcher exports `ORG_GRADLE_PROJECT_composeCorePath` so Xcode's Gradle invocation receives it too. -For manual Xcode builds, set `composeCorePath` in this project's `gradle.properties` or export that -environment variable before opening Xcode. Omit the property to use published libraries. - -Substitution covers the core Compose runtime, UI, foundation, animation, material, and desktop modules. -Compose resources still use the published artifact: they are not built in the core repository. -You can inspect the JVM dependency graph with: - -```bash -./gradlew :benchmarks-app:dependencyInsight --configuration jvmRuntimeClasspath \ - --dependency org.jetbrains.compose.ui:ui -PcomposeCorePath=../../.. --console=plain -``` - -Kotlin in the benchmark project and Kotlin in core are configured independently. This project's -catalog does not override core's compiler or dependencies. Use a compatible core checkout and Gradle -version; rebuild the app when switching versions or switching between Maven and included-build modes. -Do not use `--skip-build` for that switch. - -## Manual collector and JVM app - -Start the collector in one terminal: - -```bash -./gradlew :benchmarks-collector:run --args="8090 $PWD/out/reports" --console=plain -``` - -Run a case in another terminal: - -```bash -./gradlew :benchmarks-app:run \ - --args="http://127.0.0.1:8090/reports LazyGrid averageFrame run-001" --console=plain -``` - -Application arguments are ` `. -Modes: `averageFrame`, `fps`, `realFrames`, `startup`. Use a unique ID for every run. -The collector listens on `127.0.0.1` and allows JSON POST requests from any origin. -Browser requests go directly to the collector, without a proxy. Loopback works for local browsers -and iOS Simulator, but not for a physical device connected to another machine. - -The [iOS app](benchmarks-ios-app/README.md) builds its framework from this standalone project. diff --git a/compose/mpp/benchmarks/benchmarks-ios-app/BenchmarksIosApp.xcodeproj/project.pbxproj b/compose/mpp/benchmarks/benchmarks-ios-app/BenchmarksIosApp.xcodeproj/project.pbxproj index 01ce55d77d740..0bd2ca248926c 100644 --- a/compose/mpp/benchmarks/benchmarks-ios-app/BenchmarksIosApp.xcodeproj/project.pbxproj +++ b/compose/mpp/benchmarks/benchmarks-ios-app/BenchmarksIosApp.xcodeproj/project.pbxproj @@ -154,7 +154,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "cd \"$SRCROOT/..\"\n./gradlew :benchmarks-app:embedAndSignAppleFrameworkForXcode --console=plain\n"; + shellScript = "cd \"$SRCROOT/..\"\n./gradlew --project-cache-dir \"$SRCROOT/../.gradle/launcher\" :benchmarks-app:embedAndSignAppleFrameworkForXcode --console=plain\n"; }; /* End PBXShellScriptBuildPhase section */ diff --git a/compose/mpp/benchmarks/benchmarks-ios-app/BenchmarksIosApp.xcodeproj/xcuserdata/konstantin.tskhovrebov.xcuserdatad/xcschemes/benchmarks-ios-app.BenchmarksIosApp.xcscheme b/compose/mpp/benchmarks/benchmarks-ios-app/BenchmarksIosApp.xcodeproj/xcuserdata/konstantin.tskhovrebov.xcuserdatad/xcschemes/benchmarks-ios-app.BenchmarksIosApp.xcscheme new file mode 100644 index 0000000000000..664fbb1cc625a --- /dev/null +++ b/compose/mpp/benchmarks/benchmarks-ios-app/BenchmarksIosApp.xcodeproj/xcuserdata/konstantin.tskhovrebov.xcuserdatad/xcschemes/benchmarks-ios-app.BenchmarksIosApp.xcscheme @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + diff --git a/compose/mpp/benchmarks/benchmarks-launcher/build.gradle.kts b/compose/mpp/benchmarks/benchmarks-launcher/build.gradle.kts new file mode 100644 index 0000000000000..c53d1cfe8b35d --- /dev/null +++ b/compose/mpp/benchmarks/benchmarks-launcher/build.gradle.kts @@ -0,0 +1,33 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + +plugins { + alias(libs.plugins.kotlin.jvm) + alias(libs.plugins.kotlin.serialization) + application +} + +kotlin { + compilerOptions { + jvmTarget.set(JvmTarget.JVM_17) + freeCompilerArgs.add("-Xjdk-release=17") + } +} + +java { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 +} + +dependencies { + implementation(libs.clikt) + implementation(libs.kotlinSerializationJson) +} + +application { + mainClass.set("org.jetbrains.compose.benchmarks.launcher.MainKt") + applicationDefaultJvmArgs = listOf("--enable-native-access=ALL-UNNAMED") +} + +tasks.named("run") { + workingDir = rootProject.projectDir +} diff --git a/compose/mpp/benchmarks/benchmarks-launcher/src/main/kotlin/org/jetbrains/compose/benchmarks/launcher/Applications.kt b/compose/mpp/benchmarks/benchmarks-launcher/src/main/kotlin/org/jetbrains/compose/benchmarks/launcher/Applications.kt new file mode 100644 index 0000000000000..59c41b4e5d98d --- /dev/null +++ b/compose/mpp/benchmarks/benchmarks-launcher/src/main/kotlin/org/jetbrains/compose/benchmarks/launcher/Applications.kt @@ -0,0 +1,237 @@ +/* + * Copyright 2026 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jetbrains.compose.benchmarks.launcher + +import java.net.URLEncoder +import java.nio.file.Files +import java.nio.file.Path +import java.util.concurrent.TimeUnit +import kotlin.io.path.name +import kotlin.io.path.writeText +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.booleanOrNull +import kotlinx.serialization.json.jsonArray +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive + +internal const val bundleId = "org.jetbrains.compose.benchmarks.app.BenchmarksIosApp" +internal val isMac = System.getProperty("os.name").startsWith("Mac") + +internal fun path(value: String): Path = Path.of( + when { + value == "~" -> System.getProperty("user.home") + value.startsWith("~/") -> System.getProperty("user.home") + value.drop(1) + else -> value + } +) + +internal fun executable(value: String, processes: Processes): Path { + var candidate = path(value) + if (candidate.name.endsWith(".app") && Files.isDirectory(candidate)) { + val name = processes.capture( + listOf( + "/usr/libexec/PlistBuddy", "-c", "Print :CFBundleExecutable", + candidate.resolve("Contents/Info.plist").toAbsolutePath().toString(), + ) + ).trim() + candidate = candidate.resolve("Contents/MacOS/$name") + } + val found = if (Files.isRegularFile(candidate)) candidate else { + System.getenv("PATH").orEmpty().split(":").map { Path.of(it).resolve(value) } + .firstOrNull { Files.isRegularFile(it) && Files.isExecutable(it) } + } + check(found != null && Files.isExecutable(found)) { "executable not found: $value" } + return found.toRealPath() +} + +internal fun browserKind(browser: Path): String { + val name = browser.name.lowercase() + return when { + name in listOf("safari", "safari technology preview") -> "safari" + "firefox" in name -> "firefox" + listOf( + "chrome", + "chromium", + "brave", + "edge", + "vivaldi" + ).any { it in name } || name == "arc" -> "chromium" + + else -> error("cannot determine browser family from executable name: $browser") + } +} + +internal data class Simulator(val id: String, val state: String) + +internal fun selectSimulator(selection: String?, processes: Processes): Simulator { + val data = Json.parseToJsonElement( + processes.capture( + listOf( + "xcrun", "simctl", "list", "devices", "available", "--json", + ) + ) + ).jsonObject.getValue("devices").jsonObject + val devices = data.values.flatMap { it.jsonArray }.map { it.jsonObject } + .filter { it["isAvailable"]?.jsonPrimitive?.booleanOrNull == true } + val matches = devices.filter { + if (selection == null) it.text("state") == "Booted" + else selection in listOf(it.text("udid"), it.text("name")) + } + check(matches.size == 1) { "specify --simulator UUID, or boot exactly one simulator before running" } + return Simulator(matches.single().text("udid"), matches.single().text("state")) +} + +internal fun JsonObject.text(key: String): String = getValue(key).jsonPrimitive.content + +private fun safariBundle(browser: Path): Path = generateSequence(browser.parent) { it.parent } + .firstOrNull { it.name.endsWith(".app") } + ?: error("Safari executable must be inside its .app bundle") + +private fun safariPids(browser: Path, processes: Processes): List = + processes.capture(listOf("ps", "-ww", "-axo", "pid=,comm=")).lineSequence().mapNotNull { line -> + val fields = line.trim().split(Regex("\\s+"), limit = 2) + if (fields.size != 2) return@mapNotNull null + if (runCatching { + Path.of(fields[1]).toRealPath() + }.getOrNull() == browser) fields[0].toLong() else null + }.toList() + +internal fun requireSafariClosed(browser: Path, processes: Processes) { + check(safariPids(browser, processes).isEmpty()) { + "close Safari before starting the series; existing Safari sessions will not be closed" + } +} + +private fun quitSafari(browser: Path, processes: Processes, log: Path) { + val owned = safariPids(browser, processes).mapNotNull { ProcessHandle.of(it).orElse(null) } + if (owned.isEmpty()) return + var interrupted = Thread.interrupted() + try { + try { + processes.run( + listOf( + "osascript", + "-e", + "on run argv\n tell application (item 1 of argv) to quit\nend run", + safariBundle(browser).toString(), + ), log, timeoutSeconds = 5, checkExit = false + ) + } catch (_: InterruptedException) { + interrupted = true + } catch (error: Exception) { + appendLog(log, "Safari quit failed: ${error.message}\n") + } + val deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(3) + while (!interrupted && owned.any { it.isAlive } && System.nanoTime() < deadline) { + try { + Thread.sleep(100) + } catch (_: InterruptedException) { + interrupted = true + } + } + owned.filter { it.isAlive }.forEach { it.destroyForcibly() } + check(owned.all { + !it.isAlive || runCatching { + it.onExit().get(3, TimeUnit.SECONDS); true + }.getOrDefault(false) + }) { + "Safari did not stop; refusing to start the next test" + } + } finally { + if (interrupted) Thread.currentThread().interrupt() + } +} + +internal fun runApplication( + processes: Processes, + target: String, + app: Path, + browser: Path?, + kind: String?, + simulator: Simulator?, + parameters: List, + webUrl: String?, + browserArgs: List, + log: Path, + waitForReport: (Process) -> Unit, +) { + if (target == "ios") { + val device = requireNotNull(simulator).id + try { + processes.start( + listOf( + "xcrun", "simctl", "launch", "--console", "--terminate-running-process", + device, bundleId + ) + parameters, log + ).use { waitForReport(it.process) } + } finally { + processes.run( + listOf("xcrun", "simctl", "terminate", device, bundleId), + log, + 10, + checkExit = false + ) + } + return + } + if (browser == null) { + val command = if (target == "jvm") { + val java = System.getenv("JAVA_HOME")?.let { path(it).resolve("bin/java").toString() } + ?: "java" + listOf(executable(java, processes).toString(), "-jar", app.toString()) + parameters + } else listOf(app.toString()) + parameters + processes.start(command, log).use { waitForReport(it.process) } + return + } + val query = listOf("benchmarkCollectorUrl", "test", "type", "id").zip(parameters) + .joinToString("&") { (key, value) -> "$key=${URLEncoder.encode(value, Charsets.UTF_8)}" } + val url = "$webUrl/?$query" + if (kind == "safari") { + requireSafariClosed(browser, processes) + processes.start(listOf("open", "-W", "-a", safariBundle(browser).toString(), url), log) + .use { + try { + waitForReport(it.process) + } finally { + quitSafari(browser, processes, log) + } + } + return + } + val profile = Files.createTempDirectory("benchmark-browser-") + try { + val arguments = if (kind == "chromium") { + listOf( + "--user-data-dir=$profile", + "--no-first-run", + "--no-default-browser-check", + "--new-window" + ) + } else { + profile.resolve("user.js").writeText( + "user_pref(\"browser.shell.checkDefaultBrowser\", false);\n" + + "user_pref(\"browser.startup.homepage_override.mstone\", \"ignore\");\n" + ) + listOf("--no-remote", "--profile", profile.toString(), "--new-window") + } + processes.start(listOf(browser.toString()) + arguments + browserArgs + url, log) + .use { waitForReport(it.process) } + } finally { + deleteTree(profile) + } +} diff --git a/compose/mpp/benchmarks/benchmarks-launcher/src/main/kotlin/org/jetbrains/compose/benchmarks/launcher/Main.kt b/compose/mpp/benchmarks/benchmarks-launcher/src/main/kotlin/org/jetbrains/compose/benchmarks/launcher/Main.kt new file mode 100644 index 0000000000000..b72b754b4b03f --- /dev/null +++ b/compose/mpp/benchmarks/benchmarks-launcher/src/main/kotlin/org/jetbrains/compose/benchmarks/launcher/Main.kt @@ -0,0 +1,434 @@ +/* + * Copyright 2026 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jetbrains.compose.benchmarks.launcher + +import com.github.ajalt.clikt.core.CliktCommand +import com.github.ajalt.clikt.core.main +import com.github.ajalt.clikt.parameters.options.convert +import com.github.ajalt.clikt.parameters.options.default +import com.github.ajalt.clikt.parameters.options.flag +import com.github.ajalt.clikt.parameters.options.multiple +import com.github.ajalt.clikt.parameters.options.option +import com.github.ajalt.clikt.parameters.options.required +import com.github.ajalt.clikt.parameters.options.varargValues +import com.github.ajalt.clikt.parameters.types.choice +import com.github.ajalt.clikt.parameters.types.double +import com.github.ajalt.clikt.parameters.types.int +import java.net.InetSocketAddress +import java.net.ServerSocket +import java.net.Socket +import java.nio.file.Files +import java.nio.file.Path +import java.time.Instant +import java.time.ZoneOffset +import java.time.format.DateTimeFormatter +import java.util.UUID +import java.util.concurrent.atomic.AtomicBoolean +import kotlin.io.path.exists +import kotlin.io.path.getLastModifiedTime +import kotlin.io.path.isDirectory +import kotlin.io.path.listDirectoryEntries +import kotlin.io.path.readText +import kotlin.io.path.writeText +import kotlin.math.round +import kotlin.system.exitProcess +import kotlinx.serialization.Serializable +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.jsonObject + +private val cases = listOf( + "AnimatedVisibility", "LazyGrid", "LazyGrid-ItemLaunchedEffect", "LazyGrid-SmoothScroll", + "LazyGrid-SmoothScroll-ItemLaunchedEffect", "VisualEffects", "LazyList", "MultipleComponents", + "MultipleComponents-NoVectorGraphics", "TextLayout", "CanvasDrawing", "HeavyShader", +) +private val modes = listOf("averageFrame", "fps", "realFrames", "startup") +private val json = Json { prettyPrint = true; encodeDefaults = true; explicitNulls = false } + +fun main(args: Array) = Launcher().main(args) + +private class Launcher : CliktCommand(name = "benchmarks-launcher") { + private val target by option(help = "Target to build and run") + .choice("jvm", "macos", "js", "wasmJs", "ios").required() + private val port by option(help = "Collector port").int().default(8090) + private val tests by option(help = "Case names, space/comma separated, or all").varargValues() + .default(listOf("all")) + private val selectedModes by option("--modes", help = "Modes, space/comma separated, or all") + .varargValues().default(listOf("all")) + private val repeat by option(help = "Repetitions of each case/mode").int().default(1) + private val timeout by option(help = "Seconds per test, excluding build time").double() + .default(30.0) + private val outputDir by option( + "--output-dir", + help = "New directory for this series" + ).convert { absolutePath(it) } + private val projectDir by option( + "--project-dir", + help = "Standalone benchmarks project, detected by default" + ) + .convert { absolutePath(it) } + private val skipBuild by option( + "--skip-build", + help = "Reuse existing release artifacts" + ).flag() + private val corePath by option( + "--compose-core-path", + help = "Include a core checkout instead of Maven libraries" + ) + .convert { absolutePath(it) } + private val browser by option(help = "Chromium/Firefox/Safari executable, command name, or macOS .app") + private val browserArgs by option( + "--browser-arg", + help = "Extra browser flag; use --browser-arg=--flag" + ).multiple() + private val webPort by option( + "--web-port", + help = "Static file port, 0 selects a free port" + ).int().default(0) + private val simulator by option(help = "iOS Simulator UUID or unique name; default: one booted device") + + override fun run() { + try { + require( + !System.getProperty("os.name").startsWith("Windows") + ) { "this launcher requires macOS or Linux" } + require(target !in listOf("ios", "macos") || isMac) { "Apple targets require macOS" } + require(port in 1..65535 && webPort in 0..65535) { "invalid port" } + require(repeat > 0 && timeout.isFinite() && timeout > 0) { "repeat and timeout must be positive" } + require( + target !in listOf( + "js", + "wasmJs" + ) || browser != null + ) { "web targets require --browser" } + launchSeries() + } catch (error: Exception) { + echo("Error: ${error.message}", err = true) + exitProcess(1) + } + } + + private fun launchSeries() { + val root = findRoot(projectDir) + val out = path(System.getenv("OUT_DIR") ?: root.resolve("out").toString()).let { + (if (it.isAbsolute) it else root.resolve(it)).toAbsolutePath().normalize() + } + val stamp = DateTimeFormatter.ofPattern("yyyyMMdd'T'HHmmss'Z'").withZone(ZoneOffset.UTC) + .format(Instant.now()) + val directory = outputDir ?: out.resolve( + "benchmark-reports/$stamp-$target-${ + UUID.randomUUID().toString().take(8) + }" + ) + val summary = Summary( + target, select(tests, cases), select(selectedModes, modes), repeat, timeout, + composeCorePath = corePath?.toString() + ) + Files.createDirectories(directory.parent) + Files.createDirectory(directory) + val logs = Files.createDirectory(directory.resolve("logs")) + val reports = Files.createDirectory(directory.resolve("reports")) + val environment = System.getenv().toMutableMap() + corePath?.let { environment["ORG_GRADLE_PROJECT_composeCorePath"] = it.toString() } + val processes = Processes(root, environment, logs.resolve("commands.log")) + val shuttingDown = AtomicBoolean() + val mainThread = Thread.currentThread() + val hook = Thread({ + shuttingDown.set(true) + mainThread.interrupt() + mainThread.join(20_000) + processes.close() + }, "benchmark-shutdown") + Runtime.getRuntime().addShutdownHook(hook) + println("Results: $directory") + try { + summary.save(directory) + val device = if (target == "ios") selectSimulator(simulator, processes) else null + val browserPath = if (target in listOf("js", "wasmJs")) executable( + requireNotNull(browser), + processes + ) else null + val kind = browserPath?.let { browserKind(it) } + if (browserPath != null) { + summary.browser = Browser(kind!!, browserPath.toString()) + println("Browser: $kind ($browserPath)") + if (kind == "safari") { + require(isMac) { "Safari requires macOS" } + require(browserArgs.isEmpty()) { "--browser-arg is not supported for Safari" } + requireSafariClosed(browserPath, processes) + } + } + ensureFreePort(port) + if (!skipBuild) build(root, out, logs, device, processes) + val app = artifact(root.resolve("benchmarks-app/build"), out) + if (device != null) { + if (device.state != "Booted") processes.run( + listOf( + "xcrun", + "simctl", + "boot", + device.id + ), logs.resolve("simulator.log") + ) + processes.run( + listOf("xcrun", "simctl", "bootstatus", device.id, "-b"), + logs.resolve("simulator.log") + ) + processes.run( + listOf("xcrun", "simctl", "install", device.id, app.toString()), + logs.resolve("simulator.log") + ) + } + ensureFreePort(port) + val collectorCommand = listOf( + root.resolve( + "benchmarks-collector/build/install/benchmarks-collector/bin/benchmarks-collector" + ).toString(), port.toString(), reports.toString() + ) + processes.start(collectorCommand, logs.resolve("collector.log")).use { collector -> + waitForCollector(collector.process, port) + val server = if (browserPath != null) WebServer( + app, + webPort, + logs.resolve("web-server.log") + ) else null + server.use { + val total = summary.tests.size * summary.modes.size * repeat + for (name in summary.tests) for (mode in summary.modes) for (iteration in 1..repeat) { + if (Thread.interrupted()) throw InterruptedException() + check(collector.process.isAlive) { "collector exited; see logs/collector.log" } + if (kind == "safari") requireSafariClosed( + requireNotNull(browserPath), + processes + ) + val id = UUID.randomUUID().toString() + val log = logs.resolve("$id.log") + val result = RunResult(id, name, mode, iteration, log = "logs/$id.log") + summary.runs.add(result) + summary.save(directory) + val started = System.nanoTime() + println("[${summary.runs.size}/$total] $name / $mode / $id") + try { + runApplication( + processes, + target, + app, + browserPath, + kind, + device, + listOf("http://127.0.0.1:$port/reports", name, mode, id), + server?.url, + browserArgs, + log + ) { process -> + val report = waitForReport( + reports, + id, + name, + mode, + started, + timeout, + process, + collector.process + ) + result.status = "passed" + result.report = directory.relativize(report).toString() + } + } catch (error: InterruptedException) { + result.status = "interrupted" + throw error + } catch (error: Exception) { + result.status = "failed" + result.error = error.message ?: error.toString() + appendLog(log, "\nLauncher: ${error.stackTraceToString()}\n") + } finally { + result.elapsedSeconds = + round((System.nanoTime() - started) / 1_000_000.0) / 1000 + summary.save(directory) + } + println(" ${result.status}: ${result.error ?: result.report.orEmpty()}") + } + } + } + } catch (_: InterruptedException) { + summary.error = "interrupted" + } catch (error: Exception) { + summary.error = error.message ?: error.toString() + System.err.println("Error: ${summary.error}") + } finally { + Thread.interrupted() // File writes and cleanup must complete after cancellation. + try { + processes.close() + } finally { + Thread.interrupted() + if (shuttingDown.get()) summary.error = "interrupted" + try { + summary.save(directory) + } finally { + runCatching { Runtime.getRuntime().removeShutdownHook(hook) } + } + } + } + println("Finished: ${summary.runs.count { it.status == "passed" }}/${summary.runs.size} passed. Results: $directory") + if (!shuttingDown.get()) { + val code = if (summary.error == "interrupted") 130 + else if (summary.error != null || summary.runs.any { it.status != "passed" }) 1 else 0 + if (code != 0) exitProcess(code) + } + } + + private fun build(root: Path, out: Path, logs: Path, device: Simulator?, processes: Processes) { + val appTask = when (target) { + "jvm" -> "packageReleaseUberJarForCurrentOS" + "macos" -> "linkReleaseExecutableMacosArm64" + "js" -> "jsBrowserDistribution" + "wasmJs" -> "wasmJsBrowserDistribution" + else -> null + } + // A separate project cache avoids locking the outer build when launched through Gradle run. + val command = listOf( + root.resolve("gradlew").toString(), "--project-cache-dir", + root.resolve(".gradle/launcher").toString(), ":benchmarks-collector:installDist" + ) + + listOfNotNull(appTask?.let { ":benchmarks-app:$it" }) + "--console=plain" + processes.run(command, logs.resolve("gradle-build.log")) + if (target == "ios") { + processes.run( + listOf( + "xcodebuild", + "-project", + root.resolve("benchmarks-ios-app/BenchmarksIosApp.xcodeproj").toString(), + "-scheme", + "BenchmarksIosApp", + "-configuration", + "Release", + "-sdk", + "iphonesimulator", + "-destination", + "id=${requireNotNull(device).id}", + "-derivedDataPath", + out.resolve("benchmarks-ios-derived").toString(), + "ARCHS=arm64", + "CODE_SIGNING_ALLOWED=NO", + "build" + ), logs.resolve("xcode-build.log") + ) + } + } + + private fun artifact(appBuild: Path, out: Path): Path { + val app = when (target) { + "jvm" -> appBuild.resolve("compose/jars").let { directory -> + if (directory.exists()) directory.listDirectoryEntries("*-release.jar") + .maxByOrNull { it.getLastModifiedTime() } else null + } + + "macos" -> appBuild.resolve("bin/macosArm64/releaseExecutable/benchmarks-app.kexe") + "js", "wasmJs" -> appBuild.resolve("dist/$target/productionExecutable") + else -> out.resolve("benchmarks-ios-derived/Build/Products/Release-iphonesimulator/BenchmarksIosApp.app") + } + check(app != null && app.exists()) { "release artifact not found; run without --skip-build" } + return app + } +} + +private fun absolutePath(value: String): Path = path(value).toAbsolutePath().normalize() + +private fun findRoot(explicit: Path?): Path { + fun valid(candidate: Path) = candidate.resolve("settings.gradle.kts").exists() && + candidate.resolve("gradlew").exists() && candidate.resolve("benchmarks-app").isDirectory() + if (explicit != null) { + require(valid(explicit)) { "not a benchmarks project: $explicit" } + return explicit + } + val cwd = absolutePath(".") + val location = Path.of(Launcher::class.java.protectionDomain.codeSource.location.toURI()) + val candidates = sequenceOf(cwd, cwd.resolve("compose/mpp/benchmarks"), location) + .flatMap { generateSequence(it) { parent -> parent.parent } } + return candidates.firstOrNull { valid(it) } + ?: error("cannot find the benchmarks project; pass --project-dir /path/to/benchmarks") +} + +private fun select(values: List, available: List): List { + val items = values.flatMap { it.split(",") } + if (items == listOf("all")) return available + require(items.all { it in available }) { "unknown values: ${(items - available.toSet()).joinToString()}" } + return items.distinct() +} + +private fun ensureFreePort(port: Int) { + ServerSocket().use { socket -> socket.bind(InetSocketAddress("127.0.0.1", port)) } +} + +private fun waitForCollector(process: Process, port: Int) { + val started = System.nanoTime() + while ((System.nanoTime() - started) / 1e9 < 30) { + check(process.isAlive) { "collector exited; see logs/collector.log" } + try { + Socket().use { it.connect(InetSocketAddress("127.0.0.1", port), 200) } + return + } catch (_: java.io.IOException) { + Thread.sleep(100) + } + } + error("collector did not start within 30 seconds; see logs/collector.log") +} + +private fun waitForReport( + reports: Path, id: String, name: String, mode: String, started: Long, timeout: Double, + application: Process, collector: Process, +): Path { + fun find(): Path? = reports.listDirectoryEntries("$id-*.json").firstOrNull()?.also { + val report = Json.parseToJsonElement(it.readText()).jsonObject + check(report.text("id") == id && report.text("name") == name && report.text("type") == mode) { + "unexpected report contents: $it" + } + } + while (true) { + find()?.let { return it } + check(collector.isAlive) { "collector exited; see logs/collector.log" } + check((System.nanoTime() - started) / 1e9 < timeout) { "no report before test timeout" } + if (!application.isAlive) return find() ?: error("application exited without a report") + Thread.sleep(100) + } +} + +@Serializable +private data class Summary( + val target: String, + val tests: List, + val modes: List, + val repeat: Int, + val timeoutSeconds: Double, + val composeCorePath: String? = null, + val runs: MutableList = mutableListOf(), + var browser: Browser? = null, + var error: String? = null, +) { + fun save(directory: Path) = + directory.resolve("summary.json").writeText(json.encodeToString(this) + "\n") +} + +@Serializable +private data class Browser(val kind: String, val executable: String) + +@Serializable +private data class RunResult( + val id: String, val name: String, val type: String, val iteration: Int, val log: String, + var status: String = "interrupted", var elapsedSeconds: Double = 0.0, + var report: String? = null, var error: String? = null, +) diff --git a/compose/mpp/benchmarks/benchmarks-launcher/src/main/kotlin/org/jetbrains/compose/benchmarks/launcher/Processes.kt b/compose/mpp/benchmarks/benchmarks-launcher/src/main/kotlin/org/jetbrains/compose/benchmarks/launcher/Processes.kt new file mode 100644 index 0000000000000..f252c89755468 --- /dev/null +++ b/compose/mpp/benchmarks/benchmarks-launcher/src/main/kotlin/org/jetbrains/compose/benchmarks/launcher/Processes.kt @@ -0,0 +1,141 @@ +/* + * Copyright 2026 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jetbrains.compose.benchmarks.launcher + +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.StandardOpenOption.APPEND +import java.nio.file.StandardOpenOption.CREATE +import java.util.concurrent.TimeUnit +import java.util.concurrent.TimeoutException +import java.util.concurrent.atomic.AtomicBoolean +import kotlin.io.path.readText + +internal fun appendLog(path: Path, text: String) { + Files.writeString(path, text, CREATE, APPEND) +} + +internal class Processes( + private val root: Path, + private val environment: Map, + private val commandLog: Path, +) : AutoCloseable { + private val active = mutableSetOf() + private var closed = false + + @Synchronized + fun start(command: List, log: Path, output: Path = log): ManagedProcess { + check(!closed) { "launcher is stopping" } + appendLog(log, "\n$ ${command.joinToString(" ")}\n") + val builder = ProcessBuilder(command).directory(root.toFile()) + .redirectOutput(ProcessBuilder.Redirect.appendTo(output.toFile())) + if (output == log) builder.redirectErrorStream(true) + else builder.redirectError(ProcessBuilder.Redirect.appendTo(log.toFile())) + builder.environment().putAll(environment) + return ManagedProcess(builder.start()) { synchronized(this) { active.remove(it) } } + .also { active.add(it) } + } + + fun run( + command: List, + log: Path, + timeoutSeconds: Long? = null, + checkExit: Boolean = true + ) { + println("Running: ${command.joinToString(" ")}") + start(command, log).use { child -> + if (timeoutSeconds == null) child.process.waitFor() + else check( + child.process.waitFor( + timeoutSeconds, + TimeUnit.SECONDS + ) + ) { "command timed out; see $log" } + check(!checkExit || child.process.exitValue() == 0) { "command failed; see $log" } + } + } + + fun capture(command: List, timeoutSeconds: Long = 30): String { + val output = Files.createTempFile("benchmark-command-", ".log") + try { + start(command, commandLog, output).use { child -> + if (!child.process.waitFor(timeoutSeconds, TimeUnit.SECONDS)) { + throw TimeoutException("command timed out: ${command.joinToString(" ")}") + } + val text = output.readText() + appendLog(commandLog, text) + check(child.process.exitValue() == 0) { "command failed; see $commandLog" } + return text + } + } finally { + Files.deleteIfExists(output) + } + } + + override fun close() { + val children = synchronized(this) { + closed = true + active.toList().reversed() + } + children.forEach { it.close() } + } +} + +internal class ManagedProcess( + val process: Process, + private val onClose: (ManagedProcess) -> Unit, +) : AutoCloseable { + private val closed = AtomicBoolean() + + override fun close() { + if (!closed.compareAndSet(false, true)) return + var interrupted = Thread.interrupted() + try { + // Only our descendants are considered. Browser instances use isolated profiles. + // Gradle and Kotlin daemons are intentionally allowed to outlive a build command. + val descendants = process.descendants().use { stream -> stream.toList() }.filterNot { + val command = it.info().commandLine().orElse("") + "GradleDaemon" in command || "KotlinCompileDaemon" in command + } + val handles = descendants.reversed() + process.toHandle() + handles.forEach { runCatching { it.destroy() } } + val deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(3) + while (handles.any { it.isAlive } && System.nanoTime() < deadline) { + try { + Thread.sleep(25) + } catch (_: InterruptedException) { + interrupted = true + } + } + handles.filter { it.isAlive }.forEach { runCatching { it.destroyForcibly() } } + try { + process.waitFor(3, TimeUnit.SECONDS) + } catch (_: InterruptedException) { + interrupted = true + } + } finally { + onClose(this) + if (interrupted) Thread.currentThread().interrupt() + } + } +} + +internal fun deleteTree(directory: Path) { + Files.walk(directory).use { paths -> + paths.sorted(Comparator.reverseOrder()).forEach { Files.deleteIfExists(it) } + } +} diff --git a/compose/mpp/benchmarks/benchmarks-launcher/src/main/kotlin/org/jetbrains/compose/benchmarks/launcher/WebServer.kt b/compose/mpp/benchmarks/benchmarks-launcher/src/main/kotlin/org/jetbrains/compose/benchmarks/launcher/WebServer.kt new file mode 100644 index 0000000000000..4c381c13a3831 --- /dev/null +++ b/compose/mpp/benchmarks/benchmarks-launcher/src/main/kotlin/org/jetbrains/compose/benchmarks/launcher/WebServer.kt @@ -0,0 +1,75 @@ +/* + * Copyright 2026 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jetbrains.compose.benchmarks.launcher + +import com.sun.net.httpserver.HttpServer +import java.net.InetSocketAddress +import java.nio.file.Files +import java.nio.file.Path +import java.util.concurrent.Executors +import kotlin.io.path.extension + +// server to serve static benchmark apps for web targets +internal class WebServer(directory: Path, port: Int, log: Path) : AutoCloseable { + private val root = directory.toRealPath() + private val server = HttpServer.create(InetSocketAddress("127.0.0.1", port), 0) + private val executor = Executors.newCachedThreadPool { task -> + Thread(task, "benchmark-static-server").apply { isDaemon = true } + } + val url: String get() = "http://127.0.0.1:${server.address.port}" + + init { + server.executor = executor + server.createContext("/") { exchange -> + exchange.use { + val method = exchange.requestMethod + val requested = root.resolve(exchange.requestURI.path.removePrefix("/")).normalize() + val file = + if (Files.isDirectory(requested)) requested.resolve("index.html") else requested + val safeFile = runCatching { file.toRealPath() }.getOrNull() + val status = when { + method !in listOf("GET", "HEAD") -> 405 + safeFile == null || !safeFile.startsWith(root) || !Files.isRegularFile(safeFile) -> 404 + else -> 200 + } + appendLog(log, "$method ${exchange.requestURI} $status\n") + if (status != 200) { + exchange.sendResponseHeaders(status, -1) + } else { + val contentType = when (file.extension) { + "wasm" -> "application/wasm" + "js", "mjs" -> "text/javascript" + "html" -> "text/html; charset=utf-8" + "json", "map" -> "application/json" + else -> Files.probeContentType(file) ?: "application/octet-stream" + } + exchange.responseHeaders.set("Content-Type", contentType) + val length = Files.size(file) + exchange.responseHeaders.set("Content-Length", length.toString()) + exchange.sendResponseHeaders(200, if (method == "HEAD") -1 else length) + if (method == "GET") Files.copy(file, exchange.responseBody) + } + } + } + server.start() + } + + override fun close() { + server.stop(0) + executor.shutdownNow() + } +} diff --git a/compose/mpp/benchmarks/gradle/libs.versions.toml b/compose/mpp/benchmarks/gradle/libs.versions.toml index a906d9c58fc6f..252a493a32d5f 100644 --- a/compose/mpp/benchmarks/gradle/libs.versions.toml +++ b/compose/mpp/benchmarks/gradle/libs.versions.toml @@ -4,8 +4,10 @@ compose-multiplatform = "1.13.0-beta02" coroutines = "1.9.0" serialization = "1.8.0" ktor = "3.6.1" +clikt = "5.1.0" [libraries] +clikt = { module = "com.github.ajalt.clikt:clikt", version.ref = "clikt" } compose-runtime = { module = "org.jetbrains.compose.runtime:runtime", version.ref = "compose-multiplatform" } compose-ui = { module = "org.jetbrains.compose.ui:ui", version.ref = "compose-multiplatform" } compose-foundation = { module = "org.jetbrains.compose.foundation:foundation", version.ref = "compose-multiplatform" } diff --git a/compose/mpp/benchmarks/launch-scripts/README.md b/compose/mpp/benchmarks/launch-scripts/README.md deleted file mode 100644 index 227edd28659bc..0000000000000 --- a/compose/mpp/benchmarks/launch-scripts/README.md +++ /dev/null @@ -1,141 +0,0 @@ -# Benchmark launcher - -`run_benchmarks.py` uses Python 3 and its standard library. No Playwright or Python packages are needed. -It runs on macOS and Linux. Apple targets require macOS and Xcode. - -Run these examples from the core repository root. The script also works from another working directory. -It invokes the standalone wrapper in `compose/mpp/benchmarks`, not the core build. -From the standalone project root, use `python3 launch-scripts/run_benchmarks.py` instead. - -## Defaults - -- `--target` is required: `jvm`, `macos`, `js`, `wasmJs`, or `ios` (iOS Simulator). -- All 12 cases and all four modes (`averageFrame`, `fps`, `realFrames`, `startup`). -- One fresh application process and UUID per case/mode/repetition. -- `--repeat 1`, `--timeout 30` seconds per test, collector `--port 8090`. -- Build the collector and selected app once before the series. Apps use release/production artifacts. - Build time is not part of the test timeout. Use `--skip-build` to reuse existing artifacts. -- Compose libraries come from Maven by default. Use `--compose-core-path /path/to/core` for an included - build of core sources. Rebuild when changing versions or switching dependency modes. -- Continue after failed tests and timeouts. Exit with code 1 if any test fails; Ctrl+C exits with code 130. -- Reports go directly into one shared `reports` folder. The script waits for a matching report ID, - name, and type, then closes the test process before starting the next one. -- Stop the collector and web server at the end, including on interruption. Existing browser sessions - are not reused or closed. An iOS Simulator is left booted. - -The collector port must be free: this script starts its own collector, not an existing one. -Do not run two launchers against the same simulator application at the same time. - -## JVM - -Run everything: - -```bash -python3 compose/mpp/benchmarks/launch-scripts/run_benchmarks.py --target jvm -``` - -Select cases and modes, with three repetitions of each combination: - -```bash -python3 compose/mpp/benchmarks/launch-scripts/run_benchmarks.py \ - --target jvm --tests LazyGrid TextLayout --modes averageFrame startup --repeat 3 -``` - -Lists accept spaces or commas. `--tests all` and `--modes all` restore the defaults. -The JVM app is launched directly from its release uber-JAR, not through a Gradle run task. - -## Native macOS (ARM64) - -```bash -python3 compose/mpp/benchmarks/launch-scripts/run_benchmarks.py \ - --target macos --tests LazyGrid --modes averageFrame fps -``` - -## Browser (JS or WasmJS) - -Pass a browser executable, a command on `PATH`, or a macOS `.app` path. -The family is detected from the executable name: Chromium-based browsers, Firefox, or Safari. -There is no `--browser-kind` option. Unknown names are rejected rather than treated as Chromium. -Chromium and Firefox get a temporary profile and a separate instance per test. Each instance is -closed after a report, crash, or timeout. Browser launches are visible by default. - -Chromium: - -```bash -python3 compose/mpp/benchmarks/launch-scripts/run_benchmarks.py \ - --target wasmJs --browser "/Applications/Google Chrome.app" \ - --tests LazyGrid --modes startup averageFrame -``` - -Firefox: - -```bash -python3 compose/mpp/benchmarks/launch-scripts/run_benchmarks.py \ - --target js --browser "/Applications/Firefox.app" \ - --tests LazyGrid --modes averageFrame -``` - -Safari (macOS only): - -```bash -python3 compose/mpp/benchmarks/launch-scripts/run_benchmarks.py \ - --target js --browser "/Applications/Safari.app" \ - --tests LazyGrid --modes startup --repeat 3 -``` - -Close Safari before starting the series. The launcher refuses to use an existing Safari process. -Each test starts Safari with `open -W` and quits it after the report or timeout. Quitting uses AppleScript; -macOS may ask for Automation permission. If quitting fails, the launched Safari process is terminated. -Safari Technology Preview is also recognized by its executable name. -Safari uses its normal profile, not a temporary profile. Its cache and history are not reset between runs. -Finish Safari's first-launch setup before running benchmarks, and do not open Safari manually during a series. - -Use `--browser-arg=--some-flag` to pass extra Chromium/Firefox flags. For Chromium, for example: -`--browser-arg=--js-flags=--expose-gc` enables manual GC. Extra browser flags are not supported for Safari. - -The launcher serves production files on a free loopback port (`--web-port` can override it). -The browser sends reports directly to the collector port. There is no proxy: the collector allows -JSON POST requests from any origin and handles CORS preflight requests. -The browser receives all application parameters in the page URL. - -## iOS Simulator (ARM64) - -Without `--simulator`, exactly one available simulator must already be booted: - -```bash -python3 compose/mpp/benchmarks/launch-scripts/run_benchmarks.py \ - --target ios --tests LazyGrid --modes startup -``` - -Or select a simulator by UUID (recommended) or a unique name. The launcher boots it if necessary: - -```bash -python3 compose/mpp/benchmarks/launch-scripts/run_benchmarks.py \ - --target ios --simulator YOUR_SIMULATOR_UUID --tests LazyGrid --modes startup --repeat 3 -``` - -The launcher builds the Release Xcode app, installs it once, then uses `simctl launch --console` -with the four application arguments. Each run terminates any previous instance of this benchmark -app on the selected simulator. After the report or timeout, it calls `simctl terminate`. -Physical iOS devices are not supported by this launcher. - -## Output and logs - -By default, each series creates a new directory: - -```text -compose/mpp/benchmarks/out/benchmark-reports/--/ - reports/ # All collected benchmark JSON files, prefixed with the run UUID - logs/ # Build logs, collector log, and a separate log for each run UUID - summary.json # Parameters, statuses, timings, report paths, and errors -``` - -Set `--output-dir /path/to/new-directory` to choose the series directory. It must not already exist. -Use `--port 18090` if port 8090 is occupied. `OUT_DIR` selects the default reports and iOS DerivedData -root; its default is the standalone project's `out` directory. Relative `OUT_DIR` values are resolved -from that project. Gradle artifacts use the normal `/build` layout, independent of `OUT_DIR`. -The default iOS DerivedData path is `$OUT_DIR/benchmarks-ios-derived` (or the project's `out/benchmarks-ios-derived`). - -A test timeout limits launch/report waiting; process cleanup may take a few extra seconds. -Reports already collected are kept after failures or interruption. Inspect `summary.json` and the -per-test logs to diagnose failures. For application details and case names, see the [main README](../README.md). diff --git a/compose/mpp/benchmarks/launch-scripts/run_benchmarks.py b/compose/mpp/benchmarks/launch-scripts/run_benchmarks.py deleted file mode 100755 index 0fcf846db25c1..0000000000000 --- a/compose/mpp/benchmarks/launch-scripts/run_benchmarks.py +++ /dev/null @@ -1,492 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2026 The Android Open Source Project -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Build and run local benchmarks, collecting one report per fresh application process.""" - -import argparse -from contextlib import contextmanager, ExitStack -from datetime import datetime, timezone -from functools import partial -from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer -import json -import os -from pathlib import Path -import plistlib -import shutil -import signal -import socket -import subprocess -import sys -import tempfile -import threading -import time -import traceback -from urllib.parse import urlencode -import uuid - - -ROOT = Path(__file__).resolve().parents[1] -APP_TASK = ":benchmarks-app:" -COLLECTOR_TASK = ":benchmarks-collector:" -BUNDLE_ID = "org.jetbrains.compose.benchmarks.app.BenchmarksIosApp" -TESTS = ( - "AnimatedVisibility", "LazyGrid", "LazyGrid-ItemLaunchedEffect", "LazyGrid-SmoothScroll", - "LazyGrid-SmoothScroll-ItemLaunchedEffect", "VisualEffects", "LazyList", "MultipleComponents", - "MultipleComponents-NoVectorGraphics", "TextLayout", "CanvasDrawing", "HeavyShader", -) -MODES = ("averageFrame", "fps", "realFrames", "startup") - - -def parse_args(): - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("--target", required=True, choices=("jvm", "macos", "js", "wasmJs", "ios")) - parser.add_argument("--port", type=int, default=8090, help="collector port (default: 8090)") - parser.add_argument("--tests", nargs="+", default=["all"], help="case names, space/comma separated") - parser.add_argument("--modes", nargs="+", default=["all"], help="modes, space/comma separated") - parser.add_argument("--repeat", type=int, default=1) - parser.add_argument("--timeout", type=float, default=30, help="seconds per test (default: 30)") - parser.add_argument("--output-dir", type=Path, help="new directory for this series") - parser.add_argument("--skip-build", action="store_true", help="reuse existing release artifacts") - parser.add_argument("--compose-core-path", type=Path, help="include a core checkout instead of published Compose libraries") - parser.add_argument("--browser", help="Chromium/Firefox/Safari executable, command name, or macOS .app") - parser.add_argument("--browser-arg", action="append", default=[], help="extra browser flag; use --browser-arg=--flag") - parser.add_argument("--web-port", type=int, default=0, help="static file server port (default: automatic)") - parser.add_argument("--simulator", help="iOS Simulator UUID or unique name (default: one booted device)") - args = parser.parse_args() - if os.name != "posix": - parser.error("this launcher requires macOS or Linux") - if args.target in ("ios", "macos") and sys.platform != "darwin": - parser.error("Apple targets require macOS") - if not 1 <= args.port <= 65535 or not 0 <= args.web_port <= 65535: - parser.error("invalid port") - if args.repeat < 1 or not 0 < args.timeout < float("inf"): - parser.error("repeat and timeout must be positive") - if args.target in ("js", "wasmJs") and not args.browser: - parser.error("web targets require --browser") - for name, available in (("tests", TESTS), ("modes", MODES)): - values = [item for value in getattr(args, name) for item in value.split(",")] - if values == ["all"]: - values = list(available) - unknown = set(values) - set(available) - if unknown: - parser.error(f"unknown {name}: {', '.join(sorted(unknown))}") - setattr(args, name, list(dict.fromkeys(values))) - return args - - -def stop_process(process): - # Each child has its own session. Never signal the user's other browser or a Gradle daemon. - try: - os.killpg(process.pid, signal.SIGTERM) - except ProcessLookupError: - process.wait() - return - except PermissionError: - # macOS may leave sandboxed XPC helpers in the group after the browser exits. - if process.poll() is None: - process.terminate() - try: - process.wait(timeout=3) - except subprocess.TimeoutExpired: - pass - try: - os.killpg(process.pid, signal.SIGKILL) - except ProcessLookupError: - pass - except PermissionError: - if process.poll() is None: - process.kill() - process.wait() - - -@contextmanager -def managed_process(command, log_path): - with log_path.open("ab", buffering=0) as log: - log.write(("$ " + repr([str(arg) for arg in command]) + "\n").encode()) - process = subprocess.Popen( - [str(arg) for arg in command], cwd=ROOT, stdout=log, - stderr=subprocess.STDOUT, start_new_session=True, - ) - try: - yield process - finally: - stop_process(process) - - -def run_command(command, log_path): - print(f"Running: {' '.join(str(arg) for arg in command)}", flush=True) - with managed_process(command, log_path) as process: - if process.wait() != 0: - raise RuntimeError(f"command failed; see {log_path}") - - -def select_simulator(selection): - data = json.loads(subprocess.check_output( - ["xcrun", "simctl", "list", "devices", "available", "--json"], text=True, - )) - devices = [device for group in data["devices"].values() for device in group if device.get("isAvailable")] - matches = [device for device in devices if - (selection in (device["udid"], device["name"]) if selection else device["state"] == "Booted")] - if len(matches) != 1: - raise RuntimeError("specify --simulator UUID, or boot exactly one simulator before running") - return matches[0] - - -def executable(value): - path = Path(value).expanduser() - if path.suffix == ".app" and path.is_dir(): - with (path / "Contents/Info.plist").open("rb") as file: - path = path / "Contents/MacOS" / plistlib.load(file)["CFBundleExecutable"] - found = str(path.resolve()) if path.is_file() else shutil.which(value) - if not found or not os.access(found, os.X_OK): - raise RuntimeError(f"executable not found: {value}") - return found - - -def detect_browser(browser): - name = Path(browser).name.casefold() - if name in ("safari", "safari technology preview"): - return "safari" - if "firefox" in name: - return "firefox" - if any(part in name for part in ("chrome", "chromium", "brave", "edge", "vivaldi")) or name == "arc": - return "chromium" - raise RuntimeError(f"cannot determine browser family from executable name: {browser}") - - -def safari_pids(browser): - processes = subprocess.check_output(["ps", "-ww", "-axo", "pid=,comm="], text=True) - executable_path = Path(browser).resolve() - matches = [] - for line in processes.splitlines(): - fields = line.strip().split(None, 1) - if len(fields) == 2 and Path(fields[1]).resolve() == executable_path: - matches.append(int(fields[0])) - return matches - - -def require_safari_closed(browser): - if safari_pids(browser): - raise RuntimeError("close Safari before starting the series; existing Safari sessions will not be closed") - - -def safari_bundle(browser): - for parent in Path(browser).parents: - if parent.suffix == ".app": - return parent - raise RuntimeError("Safari executable must be inside its .app bundle") - - -def quit_safari(browser, log_path): - pids = safari_pids(browser) - if not pids: - return - script = 'on run argv\n tell application (item 1 of argv) to quit\nend run' - with log_path.open("ab") as log: - try: - subprocess.run(["osascript", "-e", script, str(safari_bundle(browser))], - stdout=log, stderr=log, timeout=5) - except subprocess.TimeoutExpired: - log.write(b"Safari quit timed out; terminating the launched process.\n") - deadline = time.monotonic() + 3 - while time.monotonic() < deadline: - if not set(pids).intersection(safari_pids(browser)): - return - time.sleep(0.1) - # AppleScript may be denied by macOS Automation permissions. Only signal the owned Safari PIDs. - remaining = set(pids).intersection(safari_pids(browser)) - for pid in remaining: - try: - os.kill(pid, signal.SIGKILL) - except ProcessLookupError: - pass - deadline = time.monotonic() + 3 - while time.monotonic() < deadline: - if not remaining.intersection(safari_pids(browser)): - return - time.sleep(0.1) - raise RuntimeError("Safari did not stop; refusing to start the next test") - - -@contextmanager -def launch_safari(browser, url, log_path): - require_safari_closed(browser) - with managed_process(["open", "-W", "-a", safari_bundle(browser), url], log_path) as process: - try: - yield lambda: process.poll() is not None - finally: - quit_safari(browser, log_path) - - -def java_executable(): - home = os.environ.get("JAVA_HOME") - return executable(str(Path(home) / "bin/java") if home else "java") - - -def build(args, out, logs, simulator): - tasks = [COLLECTOR_TASK + "installDist"] - tasks += { - "jvm": [APP_TASK + "packageReleaseUberJarForCurrentOS"], - "macos": [APP_TASK + "linkReleaseExecutableMacosArm64"], - "js": [APP_TASK + "jsBrowserDistribution"], - "wasmJs": [APP_TASK + "wasmJsBrowserDistribution"], - "ios": [], - }[args.target] - run_command([ROOT / "gradlew", *tasks, "--console=plain"], logs / "gradle-build.log") - if args.target == "ios": - run_command([ - "xcodebuild", "-project", ROOT / "benchmarks-ios-app/BenchmarksIosApp.xcodeproj", - "-scheme", "BenchmarksIosApp", "-configuration", "Release", "-sdk", "iphonesimulator", - "-destination", f"id={simulator['udid']}", "-derivedDataPath", out / "benchmarks-ios-derived", - "ARCHS=arm64", "CODE_SIGNING_ALLOWED=NO", "build", - ], logs / "xcode-build.log") - - -def artifact(target, app_build, out): - if target == "jvm": - jars = list((app_build / "compose/jars").glob("*-release.jar")) - if not jars: - raise RuntimeError("release JVM jar not found; run without --skip-build") - return max(jars, key=lambda path: path.stat().st_mtime) - path = { - "macos": app_build / "bin/macosArm64/releaseExecutable/benchmarks-app.kexe", - "js": app_build / "dist/js/productionExecutable", - "wasmJs": app_build / "dist/wasmJs/productionExecutable", - "ios": out / "benchmarks-ios-derived/Build/Products/Release-iphonesimulator/BenchmarksIosApp.app", - }[target] - if not path.exists(): - raise RuntimeError(f"release artifact not found: {path}; run without --skip-build") - return path - - -def ensure_free_port(port): - with socket.socket() as probe: - try: - probe.bind(("127.0.0.1", port)) - except OSError as error: - raise RuntimeError(f"collector port {port} is already in use") from error - - -def wait_for_collector(process, port): - deadline = time.monotonic() + 30 - while time.monotonic() < deadline: - if process.poll() is not None: - raise RuntimeError("collector exited; see logs/collector.log") - try: - with socket.create_connection(("127.0.0.1", port), timeout=0.2): - return - except OSError: - time.sleep(0.1) - raise RuntimeError("collector did not start within 30 seconds; see logs/collector.log") - - -@contextmanager -def web_server(directory, port, log_path): - with log_path.open("a") as log: - class Handler(SimpleHTTPRequestHandler): - extensions_map = {**SimpleHTTPRequestHandler.extensions_map, ".wasm": "application/wasm", ".mjs": "text/javascript"} - - def log_message(self, template, *values): - log.write((template % values) + "\n") - log.flush() - - server = ThreadingHTTPServer(("127.0.0.1", port), partial(Handler, directory=str(directory))) - worker = threading.Thread(target=server.serve_forever, daemon=True) - worker.start() - try: - yield f"http://127.0.0.1:{server.server_port}" - finally: - server.shutdown() - server.server_close() - worker.join() - - -@contextmanager -def launch_test(args, app, browser, simulator, collector_url, web_url, name, mode, run_id, log_path): - parameters = [collector_url, name, mode, run_id] - if args.target == "ios": - try: - command = ["xcrun", "simctl", "launch", "--console", "--terminate-running-process", - simulator["udid"], BUNDLE_ID, *parameters] - with managed_process(command, log_path) as process: - yield lambda: process.poll() is not None - finally: - with log_path.open("ab") as log: - subprocess.run(["xcrun", "simctl", "terminate", simulator["udid"], BUNDLE_ID], - stdout=log, stderr=log, timeout=10) - return - - with ExitStack() as stack: - if args.target in ("js", "wasmJs"): - query = urlencode({"benchmarkCollectorUrl": collector_url, "test": name, "type": mode, "id": run_id}) - url = web_url + "/?" + query - if args.browser_kind == "safari": - with launch_safari(browser, url, log_path) as exited: - yield exited - return - profile = stack.enter_context(tempfile.TemporaryDirectory(prefix="benchmark-browser-")) - if args.browser_kind == "chromium": - command = [browser, f"--user-data-dir={profile}", "--no-first-run", - "--no-default-browser-check", "--new-window", *args.browser_arg, url] - else: - (Path(profile) / "user.js").write_text( - 'user_pref("browser.shell.checkDefaultBrowser", false);\n' - 'user_pref("browser.startup.homepage_override.mstone", "ignore");\n' - ) - command = [browser, "--no-remote", "--profile", profile, "--new-window", *args.browser_arg, url] - elif args.target == "jvm": - command = [java_executable(), "-jar", app, *parameters] - else: - command = [app, *parameters] - process = stack.enter_context(managed_process(command, log_path)) - yield lambda: process.poll() is not None - - -def wait_for_report(reports, run_id, name, mode, deadline, exited, collector): - while True: - for path in reports.glob(f"{run_id}-*.json"): - report = json.loads(path.read_text()) # Collector publishes complete files atomically. - if report.get("id") != run_id or report.get("name") != name or report.get("type") != mode: - raise RuntimeError(f"unexpected report contents: {path}") - return path - if collector.poll() is not None: - raise RuntimeError("collector exited; see logs/collector.log") - if time.monotonic() >= deadline: - raise TimeoutError("no report before test timeout") - if exited(): - # A headless app may exit immediately after POST. Check files before calling this a failure. - for path in reports.glob(f"{run_id}-*.json"): - report = json.loads(path.read_text()) - if report.get("id") == run_id and report.get("name") == name and report.get("type") == mode: - return path - raise RuntimeError("application exited without a report") - time.sleep(0.1) - - -def run_series(args, out, directory, summary): - logs, reports = directory / "logs", directory / "reports" - logs.mkdir() - reports.mkdir() - app_build = ROOT / "benchmarks-app/build" - collector_build = ROOT / "benchmarks-collector/build" - simulator = select_simulator(args.simulator) if args.target == "ios" else None - browser = executable(args.browser) if args.target in ("js", "wasmJs") else None - if browser: - args.browser_kind = detect_browser(browser) - summary["browser"] = {"kind": args.browser_kind, "executable": browser} - print(f"Browser: {args.browser_kind} ({browser})", flush=True) - if args.browser_kind == "safari": - if sys.platform != "darwin": - raise RuntimeError("Safari requires macOS") - if args.browser_arg: - raise RuntimeError("--browser-arg is not supported for Safari") - safari_bundle(browser) - require_safari_closed(browser) - ensure_free_port(args.port) - if not args.skip_build: - build(args, out, logs, simulator) - app = artifact(args.target, app_build, out) - if simulator: - if simulator["state"] != "Booted": - run_command(["xcrun", "simctl", "boot", simulator["udid"]], logs / "simulator.log") - run_command(["xcrun", "simctl", "bootstatus", simulator["udid"], "-b"], logs / "simulator.log") - run_command(["xcrun", "simctl", "install", simulator["udid"], app], logs / "simulator.log") - - collector_url = f"http://127.0.0.1:{args.port}/reports" - with ExitStack() as stack: - ensure_free_port(args.port) - collector = stack.enter_context(managed_process( - [collector_build / "install/benchmarks-collector/bin/benchmarks-collector", str(args.port), reports], - logs / "collector.log", - )) - wait_for_collector(collector, args.port) - web_url = None - if browser: - web_url = stack.enter_context(web_server(app, args.web_port, logs / "web-server.log")) - total = len(args.tests) * len(args.modes) * args.repeat - for name in args.tests: - for mode in args.modes: - for iteration in range(1, args.repeat + 1): - if collector.poll() is not None: - raise RuntimeError("collector exited; see logs/collector.log") - if browser and args.browser_kind == "safari": - require_safari_closed(browser) - run_id = str(uuid.uuid4()) - log_path = logs / f"{run_id}.log" - result = {"id": run_id, "name": name, "type": mode, "iteration": iteration, - "status": "interrupted", "log": str(log_path.relative_to(directory))} - summary["runs"].append(result) - started = time.monotonic() - print(f"[{len(summary['runs'])}/{total}] {name} / {mode} / {run_id}", flush=True) - try: - with launch_test(args, app, browser, simulator, collector_url, web_url, - name, mode, run_id, log_path) as exited: - report = wait_for_report(reports, run_id, name, mode, - started + args.timeout, exited, collector) - result.update(status="passed", report=str(report.relative_to(directory))) - except Exception as error: - result.update(status="failed", error=str(error)) - with log_path.open("a") as log: - log.write(f"\nLauncher: {error}\n") - traceback.print_exc(file=log) - finally: - result["elapsedSeconds"] = round(time.monotonic() - started, 3) - print(f" {result['status']}: {result.get('error', result.get('report', ''))}", flush=True) - (directory / "summary.json").write_text(json.dumps(summary, indent=2) + "\n") - - -def handle_termination(signum, frame): - raise KeyboardInterrupt - - -def main(): - args = parse_args() - signal.signal(signal.SIGTERM, handle_termination) - if args.compose_core_path: - args.compose_core_path = args.compose_core_path.expanduser().resolve() - # Also reaches the Gradle invocation in the Xcode framework build phase. - os.environ["ORG_GRADLE_PROJECT_composeCorePath"] = str(args.compose_core_path) - out = Path(os.environ.get("OUT_DIR", ROOT / "out")).expanduser() - if not out.is_absolute(): - out = ROOT / out - out = out.resolve() - stamp = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ") - directory = (args.output_dir or out / "benchmark-reports" / f"{stamp}-{args.target}-{uuid.uuid4().hex[:8]}").expanduser().resolve() - directory.mkdir(parents=True, exist_ok=False) - summary = {"target": args.target, "tests": args.tests, "modes": args.modes, - "repeat": args.repeat, "timeoutSeconds": args.timeout, "runs": []} - if args.compose_core_path: - summary["composeCorePath"] = str(args.compose_core_path) - print(f"Results: {directory}", flush=True) - exit_code = 0 - try: - run_series(args, out, directory, summary) - exit_code = int(any(run["status"] != "passed" for run in summary["runs"])) - except KeyboardInterrupt: - summary["error"] = "interrupted" - exit_code = 130 - except Exception as error: - summary["error"] = str(error) - print(f"Error: {error}", file=sys.stderr) - exit_code = 1 - finally: - (directory / "summary.json").write_text(json.dumps(summary, indent=2) + "\n") - passed = sum(run["status"] == "passed" for run in summary["runs"]) - print(f"Finished: {passed}/{len(summary['runs'])} passed. Results: {directory}", flush=True) - return exit_code - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/compose/mpp/benchmarks/settings.gradle.kts b/compose/mpp/benchmarks/settings.gradle.kts index bd9aa0a427c6c..f1389f8ca2657 100644 --- a/compose/mpp/benchmarks/settings.gradle.kts +++ b/compose/mpp/benchmarks/settings.gradle.kts @@ -34,6 +34,7 @@ dependencyResolutionManagement { include(":benchmarks-runtime") include(":benchmarks-report") include(":benchmarks-collector") +include(":benchmarks-launcher") include(":benchmarks-app") // Omit this property to benchmark published Compose libraries. From 599b456c1d7c2b3339797090b49788adc5df046e Mon Sep 17 00:00:00 2001 From: Konstantin Date: Wed, 11 Nov 2026 11:52:03 +0100 Subject: [PATCH 23/61] Add readme --- compose/mpp/benchmarks/.gitignore | 2 + compose/mpp/benchmarks/README.md | 125 ++++++++++++++++++++++++++++++ 2 files changed, 127 insertions(+) create mode 100644 compose/mpp/benchmarks/.gitignore create mode 100644 compose/mpp/benchmarks/README.md diff --git a/compose/mpp/benchmarks/.gitignore b/compose/mpp/benchmarks/.gitignore new file mode 100644 index 0000000000000..09cc89828e371 --- /dev/null +++ b/compose/mpp/benchmarks/.gitignore @@ -0,0 +1,2 @@ +/launcher/ +yarn.lock \ No newline at end of file diff --git a/compose/mpp/benchmarks/README.md b/compose/mpp/benchmarks/README.md new file mode 100644 index 0000000000000..166a5d26a1e57 --- /dev/null +++ b/compose/mpp/benchmarks/README.md @@ -0,0 +1,125 @@ +# Compose benchmarks + +A project with Compose Multiplatform benchmarks for measuring application startup and rendering +performance. This is a standalone Gradle build, separate from the core build. + +Run all commands below from `compose/mpp/benchmarks`. The launcher runs on macOS and Linux +and requires JDK 17 or newer. Apple targets require macOS with Xcode. + +## Modules + +- `benchmarks-app` — an application with Compose benchmark cases. +- `benchmarks-runtime` — runs measurements and sends results. +- `benchmarks-report` — shared report models. +- `benchmarks-collector` — receives reports over HTTP and saves them as JSON files. +- `benchmarks-launcher` — a console application that builds and runs benchmarks sequentially. +- `benchmarks-ios-app` — an iOS host application for benchmarks; a separate Xcode project. + +## Compose and Kotlin versions + +Set the versions of published libraries and plugins in the `gradle/libs.versions.toml` version catalog: + +```toml +[versions] +kotlin = "2.4.21" +compose-multiplatform = "1.13.0-beta02" +``` + +`kotlin` sets the version of Kotlin and the matching Compose Compiler plugin. +`compose-multiplatform` sets the version of Compose libraries, resources, and the Compose Multiplatform +Gradle plugin. To compare other versions, change these values and rebuild the project. + +### Using core sources + +You can use a core checkout as an included build instead of published Compose libraries. +Add this property to `gradle.properties`: + +```properties +composeCorePath=../../.. +``` + +Or pass the property on the Gradle command line: + +```bash +./gradlew :benchmarks-launcher:installDist -PcomposeCorePath=/path/to/core --console=plain +``` + +When running an already built launcher, use `--compose-core-path /path/to/core`. +It passes the property to child Gradle builds, including the framework build started by Xcode. +A relative path in the Gradle property is resolved from this project. A relative path in the launcher +argument is resolved from the current working directory. + +The included build replaces Compose library dependencies with core projects. Resources and the +Compose Multiplatform Gradle plugin still use published artifacts with the version from the catalog. + +## Building the launcher + +Build the launcher and copy the distribution: + +```bash +./gradlew :benchmarks-launcher:installDist +cp -R benchmarks-launcher/build/install/benchmarks-launcher ./launcher +``` + +```bash +./launcher/bin/benchmarks-launcher --help +``` + +## Running benchmarks + +The launcher builds the collector and the release application for the selected target once, starts +the collector, and runs tests sequentially. Each run gets a separate process and UUID. +After receiving a report, it closes the application and starts the next run. +The series continues after errors and timeouts. If any test fails, the launcher returns a nonzero +exit code. On completion or interruption, it stops the processes it started and keeps collected results. + +Main options: + +- `--target` — required target: `jvm`, `macos` (ARM64), `js`, `wasmJs`, or `ios` (ARM64 Simulator). +- `--tests` — test names separated by spaces or commas; defaults to all tests. +- `--modes` — measurement modes; defaults to all four: + - `startup` — startup time to the first frames; + - `realFrames` — frame measurements in a real UI; + - `averageFrame` — average offscreen rendering measurements; + - `fps` — per-frame offscreen rendering measurements. +- `--repeat` — repetitions of each test/mode pair; default: `1`. +- `--timeout` — timeout per test in seconds, excluding build time; default: `30`. +- `--port` — a free port for the collector; default: `8090`. +- `--output-dir` — a new directory for the series results; it must not exist already. +- `--skip-build` — use existing release application builds. + +Browser targets require `--browser` with an executable path, a command on `PATH`, or a macOS `.app`. +Chromium, Firefox, and Safari are detected from the executable name. Chromium and Firefox use temporary +profiles. Safari must be closed before the series and uses its normal profile without clearing the cache. +For iOS, select a simulator with `--simulator UUID`. Without this option, exactly one available +simulator must already be booted. + +### Example: JVM + +Run two tests in startup and average rendering modes, with three repetitions: + +```bash +./launcher/bin/benchmarks-launcher --target jvm --tests LazyGrid TextLayout --modes startup averageFrame --repeat 3 +``` + +### Example: Firefox + +Measure offscreen rendering in a WasmJS application: + +```bash +./launcher/bin/benchmarks-launcher --target wasmJs \ + --browser "/Applications/Firefox.app" \ + --tests LazyGrid --modes averageFrame +``` + +By default, each series creates a separate directory under `out/benchmark-reports`: + +```text +--/ + reports/ # JSON reports from all runs + logs/ # Build, collector, and per-test logs + summary.json # Series parameters, statuses, errors, and report paths +``` + +`OUT_DIR` overrides the base directory for results and iOS DerivedData. +A relative `OUT_DIR` is resolved from the project root. Gradle artifacts remain in `/build`. From 625fc50122f17a9cf53bef58915b17108c8696ee Mon Sep 17 00:00:00 2001 From: Konstantin Date: Wed, 11 Nov 2026 13:01:33 +0100 Subject: [PATCH 24/61] Add kotlin/cmp versions params --- compose/mpp/benchmarks/README.md | 98 +++++++--- .../project.pbxproj | 2 +- .../benchmarks/launcher/Applications.kt | 12 +- .../compose/benchmarks/launcher/Main.kt | 179 ++++++++++++------ .../compose/benchmarks/launcher/Versions.kt | 52 +++++ compose/mpp/benchmarks/settings.gradle.kts | 14 +- 6 files changed, 259 insertions(+), 98 deletions(-) create mode 100644 compose/mpp/benchmarks/benchmarks-launcher/src/main/kotlin/org/jetbrains/compose/benchmarks/launcher/Versions.kt diff --git a/compose/mpp/benchmarks/README.md b/compose/mpp/benchmarks/README.md index 166a5d26a1e57..89733ab637503 100644 --- a/compose/mpp/benchmarks/README.md +++ b/compose/mpp/benchmarks/README.md @@ -25,9 +25,15 @@ kotlin = "2.4.21" compose-multiplatform = "1.13.0-beta02" ``` -`kotlin` sets the version of Kotlin and the matching Compose Compiler plugin. -`compose-multiplatform` sets the version of Compose libraries, resources, and the Compose Multiplatform -Gradle plugin. To compare other versions, change these values and rebuild the project. +`kotlin` sets the default version of Kotlin and the matching Compose Compiler plugin. +`compose-multiplatform` sets the default version of Compose libraries, resources, and the Compose +Multiplatform Gradle plugin. Keep a published Compose version in this catalog. + +Use `--kotlin-version` and `--compose-version` to pass lists of versions without editing the catalog +or rebuilding the launcher. Lists accept spaces or commas. The launcher runs every Kotlin × Compose +combination sequentially in one timestamped session. Duplicate entries are ignored. +A published Compose version also selects that version of the resources and Gradle plugin. +Use compatible Kotlin and Compose versions. ### Using core sources @@ -44,13 +50,21 @@ Or pass the property on the Gradle command line: ./gradlew :benchmarks-launcher:installDist -PcomposeCorePath=/path/to/core --console=plain ``` -When running an already built launcher, use `--compose-core-path /path/to/core`. -It passes the property to child Gradle builds, including the framework build started by Xcode. -A relative path in the Gradle property is resolved from this project. A relative path in the launcher -argument is resolved from the current working directory. +In the launcher, use `--compose-version local` to select the current core checkout at `../../..`. +Add `--compose-core-path /path/to/core` to select another checkout or worktree. The existing core path +option or property also selects local Compose when `--compose-version` is omitted. Published entries +in a version list disable substitution even if the property is configured. `--compose-core-path` +requires `local` in the list and applies only to local configurations. + +The launcher passes the resolved version numbers and core path to child Gradle builds, including +Xcode's framework build. Only a non-empty `composeCorePath` enables the included build; the launcher +passes an empty value for published configurations to override any configured path. A relative path in the Gradle property is resolved from this project. A relative path +in the launcher argument is resolved from the current working directory. -The included build replaces Compose library dependencies with core projects. Resources and the -Compose Multiplatform Gradle plugin still use published artifacts with the version from the catalog. +For a `local` entry, the resolved Compose version is taken from the catalog, with core projects +substituted through the included build. Resources and the Compose Multiplatform Gradle plugin also +use that catalog version. `--kotlin-version` changes Kotlin in the benchmark project only; +core uses its own compiler version. ## Building the launcher @@ -58,7 +72,8 @@ Build the launcher and copy the distribution: ```bash ./gradlew :benchmarks-launcher:installDist -cp -R benchmarks-launcher/build/install/benchmarks-launcher ./launcher +mkdir -p launcher +cp -R benchmarks-launcher/build/install/benchmarks-launcher/. ./launcher/ ``` ```bash @@ -67,15 +82,21 @@ cp -R benchmarks-launcher/build/install/benchmarks-launcher ./launcher ## Running benchmarks -The launcher builds the collector and the release application for the selected target once, starts -the collector, and runs tests sequentially. Each run gets a separate process and UUID. -After receiving a report, it closes the application and starts the next run. -The series continues after errors and timeouts. If any test fails, the launcher returns a nonzero -exit code. On completion or interruption, it stops the processes it started and keeps collected results. +For each version pair, the launcher builds the collector and the release application for the selected +target, starts the collector, and runs tests sequentially. Each test run gets a separate process and UUID. +After receiving a report, it closes the application and starts the next test. It then moves to the next +version pair. Builds always run; Gradle can reuse unchanged task outputs. + +The matrix continues after test errors, timeouts, or a failed configuration build. If any configuration +fails, the launcher returns a nonzero exit code. On completion or interruption, it stops the processes +it started and keeps collected results. Main options: - `--target` — required target: `jvm`, `macos` (ARM64), `js`, `wasmJs`, or `ios` (ARM64 Simulator). +- `--compose-version` — a list of published Compose versions and/or `local`; defaults to one catalog + version unless a core checkout is configured. +- `--kotlin-version` — a list of Kotlin and matching Compose Compiler versions; defaults to one catalog version. - `--tests` — test names separated by spaces or commas; defaults to all tests. - `--modes` — measurement modes; defaults to all four: - `startup` — startup time to the first frames; @@ -85,8 +106,8 @@ Main options: - `--repeat` — repetitions of each test/mode pair; default: `1`. - `--timeout` — timeout per test in seconds, excluding build time; default: `30`. - `--port` — a free port for the collector; default: `8090`. -- `--output-dir` — a new directory for the series results; it must not exist already. -- `--skip-build` — use existing release application builds. +- `--output-dir` — the base directory for results; it can already exist. Each invocation creates a new + timestamped subdirectory for the whole version matrix. Browser targets require `--browser` with an executable path, a command on `PATH`, or a macOS `.app`. Chromium, Firefox, and Safari are detected from the executable name. Chromium and Firefox use temporary @@ -99,7 +120,19 @@ simulator must already be booted. Run two tests in startup and average rendering modes, with three repetitions: ```bash -./launcher/bin/benchmarks-launcher --target jvm --tests LazyGrid TextLayout --modes startup averageFrame --repeat 3 +./launcher/bin/benchmarks-launcher --target jvm \ + --compose-version 1.13.0-beta02 --kotlin-version 2.4.21 \ + --tests LazyGrid TextLayout --modes startup averageFrame --repeat 3 +``` + +### Example: version matrix + +Compare two Kotlin versions with published and local Compose (four configurations): + +```bash +./launcher/bin/benchmarks-launcher --target jvm \ + --kotlin-version 2.4.20 2.4.21 --compose-version 1.13.0-beta02,local \ + --tests LazyGrid --modes startup --output-dir ./results ``` ### Example: Firefox @@ -112,14 +145,33 @@ Measure offscreen rendering in a WasmJS application: --tests LazyGrid --modes averageFrame ``` -By default, each series creates a separate directory under `out/benchmark-reports`: +### Example: local Compose + +Build core sources and use Kotlin 2.4.20 for the benchmark project: + +```bash +./launcher/bin/benchmarks-launcher --target jvm \ + --compose-version local --kotlin-version 2.4.20 \ + --tests LazyGrid --modes startup --output-dir ./results +``` + +By default, results go under `out/benchmark-reports`. `--output-dir` replaces this base directory. +Each invocation creates a UTC timestamp directory in `yyyy-MM-dd-HH-mm-ss-SSS` format, then groups +results by versions and target: ```text ---/ - reports/ # JSON reports from all runs - logs/ # Build, collector, and per-test logs - summary.json # Series parameters, statuses, errors, and report paths +-