Skip to content

Commit 4137356

Browse files
committed
Kotlin compose multiplatform workload
...composed of multiple subitems, for more details see https://github.com/JetBrains/compose-multiplatform/blob/master/benchmarks/multiplatform/README.md
1 parent 6947a46 commit 4137356

18 files changed

+10040
-0
lines changed

JetStreamDriver.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2097,6 +2097,24 @@ let BENCHMARKS = [
20972097
worstCaseCount: 2,
20982098
tags: ["Wasm"],
20992099
}),
2100+
new WasmEMCCBenchmark({
2101+
name: "Kotlin-compose-wasm",
2102+
files: [
2103+
"./Kotlin-compose/benchmark.js",
2104+
],
2105+
preload: {
2106+
wasmBinary: "./Kotlin-compose/build/compose-benchmarks-benchmarks-wasm-js.wasm",
2107+
wasmSkikoBinary: "./Kotlin-compose/build/skiko.wasm",
2108+
inputImageCompose: "./Kotlin-compose/build/compose-multiplatform.png",
2109+
inputImageCat: "./Kotlin-compose/build/example1_cat.jpg",
2110+
inputImageComposeCommunity: "./Kotlin-compose/build/example1_compose-community-primary.png",
2111+
inputFontItalic: "./Kotlin-compose/build/jetbrainsmono_italic.ttf",
2112+
inputFontRegular: "./Kotlin-compose/build/jetbrainsmono_regular.ttf"
2113+
},
2114+
iterations: 10,
2115+
worstCaseCount: 2,
2116+
testGroup: WasmGroup,
2117+
}),
21002118
new WasmLegacyBenchmark({
21012119
name: "tfjs-wasm",
21022120
files: [

Kotlin-compose/.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
/cfw_d8_bench_bin/
2+
/compose-multiplatform/

Kotlin-compose/README.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Kotlin/Wasm Compose Multiplatform Benchmark
2+
3+
Citing https://github.com/JetBrains/compose-multiplatform:
4+
5+
[Compose Multiplatform](https://jb.gg/cmp) is a declarative framework for sharing UIs across multiple platforms with Kotlin.
6+
[...]
7+
Compose for Web is based on [Kotlin/Wasm](https://kotl.in/wasm), the newest target for Kotlin Multiplatform projects.
8+
It allows Kotlin developers to run their code in the browser with all the benefits that WebAssembly has to offer, such as good and predictable performance for your applications.
9+
10+
## Build Instructions
11+
12+
See `build.sh` or just run it.
13+
See `build.log` for the last build time, used sources, and toolchain versions.
14+
15+
## Running in JS shells
16+
17+
To run the unmodified upstream benchmark, without the JetStream driver, see the
18+
upstream repo.

Kotlin-compose/benchmark.js

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
// Copyright 2025 the V8 project authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
// Excerpt from `polyfills.mjs` from the upstream Kotlin compose-multiplatform
6+
// benchmark directory, with minor changes for JetStream.
7+
8+
globalThis.window ??= globalThis;
9+
10+
globalThis.navigator ??= {};
11+
if (!globalThis.navigator.languages) {
12+
globalThis.navigator.languages = ['en-US', 'en'];
13+
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';
14+
globalThis.navigator.platform = "MacIntel";
15+
}
16+
17+
// Compose reads `window.isSecureContext` in its Clipboard feature:
18+
globalThis.isSecureContext = false;
19+
20+
// Disable explicit GC (it wouldn't work in browsers anyway).
21+
globalThis.gc = () => {
22+
// DEBUG
23+
// console.log("gc()");
24+
}
25+
26+
class URL {
27+
href;
28+
constructor(url, base) {
29+
// DEBUG
30+
// console.log('URL', url, base);
31+
this.href = url;
32+
}
33+
}
34+
globalThis.URL = URL;
35+
36+
// We always polyfill `fetch` and `instantiateStreaming` for consistency between
37+
// engine shells and browsers and to avoid introducing network latency into the
38+
// first iteration / instantiation measurement.
39+
// The downside is that this doesn't test streaming Wasm instantiation, which we
40+
// are willing to accept.
41+
let preload = {};
42+
globalThis.fetch = async function(url) {
43+
// DEBUG
44+
// console.log('fetch', url);
45+
if (!preload[url]) {
46+
throw new Error('Unexpected fetch: ' + url);
47+
}
48+
return {
49+
ok: true,
50+
status: 200,
51+
arrayBuffer() { return preload[url]; },
52+
async blob() {
53+
return {
54+
size: preload[url].byteLength,
55+
async arrayBuffer() { return preload[url]; }
56+
}
57+
},
58+
};
59+
};
60+
globalThis.WebAssembly.instantiateStreaming = async function(m,i) {
61+
// DEBUG
62+
// console.log('instantiateStreaming',m,i);
63+
return WebAssembly.instantiate((await m).arrayBuffer(),i);
64+
};
65+
66+
// Provide `setTimeout` for Kotlin coroutines.
67+
// Deep in the Compose UI framework, one task is scheduled every 16ms, see
68+
// https://github.com/JetBrains/compose-multiplatform-core/blob/a52f2981b9bc7cdba1d1fbe71654c4be448ebea7/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/spatial/RectManager.kt#L138
69+
// and
70+
// https://github.com/JetBrains/compose-multiplatform-core/blob/a52f2981b9bc7cdba1d1fbe71654c4be448ebea7/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/OnLayoutRectChangedModifier.kt#L56
71+
// We don't want to delay work in the Wall-time based measurement in JetStream,
72+
// but executing this immediately (without delay) produces redundant work that
73+
// is not realistic for a full-browser Kotlin/multiplatform application either,
74+
// according to Kotlin/JetBrains folks.
75+
// Hence the early return for 16ms delays below.
76+
// FIXME: The SpiderMonkey shell doesn't have `setTimeout` (yet). We could also
77+
// polyfill this with `Promise.resolve().then(f)`, but that changes the CPU
78+
// profile slightly on other engines, so it's probably best to just add support.
79+
const originalSetTimeout = setTimeout;
80+
globalThis.setTimeout = function(f, delayMs) {
81+
// DEBUG
82+
// console.log('setTimeout', f, t);
83+
84+
if (delayMs === 16) return;
85+
if (delayMs !== 0) {
86+
throw new Error('Unexpected delay for setTimeout polyfill: ' + delayMs);
87+
}
88+
originalSetTimeout(f);
89+
90+
// Alternative, if setTimeout is not available in a shell (but that changes
91+
// the performance profile a little bit, so I'd rather not do that):
92+
// Promise.resolve().then(f);
93+
94+
// Yet another alternative is to run the task synchronously, but that obviously
95+
// overflows the stack at some point if the callback itself spawns more work:
96+
// f();
97+
}
98+
99+
// Don't automatically run the main function on instantiation.
100+
globalThis.skipFunMain = true;
101+
102+
// Prevent this from being detected as a shell environment, so that we use the
103+
// same code paths as in the browser.
104+
// See `compose-benchmarks-benchmarks-wasm-js.uninstantiated.mjs`.
105+
delete globalThis.d8;
106+
delete globalThis.inIon;
107+
delete globalThis.jscOptions;
108+
109+
// The JetStream driver doesn't have support for ES6 modules yet.
110+
// Since this file is not an ES module, we have to use a dynamic import.
111+
// However, browsers and different shalls have different requirements on whether
112+
// the path can or may be relative, so try all possible combinations.
113+
// TODO: Support ES6 modules in the driver instead of this one-off solution.
114+
// This probably requires a new `Benchmark` field called `modules` that
115+
// is a map from module variable name (which will hold the resulting module
116+
// namespace object) to relative module URL, which is resolved in the
117+
// `preRunnerCode`, similar to this code here.
118+
async function dynamicJSImport(path) {
119+
let result;
120+
if (isInBrowser) {
121+
// In browsers, relative imports don't work since we are not in a module.
122+
// (`import.meta.url` is not defined.)
123+
const pathname = location.pathname.match(/^(.*\/)(?:[^.]+(?:\.(?:[^\/]+))+)?$/)[1];
124+
result = await import(location.origin + pathname + './' + path);
125+
} else {
126+
// In shells, relative imports require different paths, so try with and
127+
// without the "./" prefix (e.g., JSC requires it).
128+
try {
129+
result = await import(path);
130+
} catch {
131+
result = await import('./' + path);
132+
}
133+
}
134+
return result;
135+
}
136+
137+
class Benchmark {
138+
skikoInstantiate;
139+
mainInstantiate;
140+
wasmInstanceExports;
141+
142+
async init() {
143+
// DEBUG
144+
// console.log("init");
145+
146+
preload = {
147+
'skiko.wasm': Module.wasmSkikoBinary,
148+
'./compose-benchmarks-benchmarks-wasm-js.wasm': Module.wasmBinary,
149+
'./composeResources/compose_benchmarks.benchmarks.generated.resources/drawable/compose-multiplatform.png': Module.inputImageCompose,
150+
'./composeResources/compose_benchmarks.benchmarks.generated.resources/drawable/example1_cat.jpg': Module.inputImageCat,
151+
'./composeResources/compose_benchmarks.benchmarks.generated.resources/files/example1_compose-community-primary.png': Module.inputImageComposeCommunity,
152+
'./composeResources/compose_benchmarks.benchmarks.generated.resources/font/jetbrainsmono_italic.ttf': Module.inputFontItalic,
153+
'./composeResources/compose_benchmarks.benchmarks.generated.resources/font/jetbrainsmono_regular.ttf': Module.inputFontRegular,
154+
};
155+
156+
// We patched `skiko.mjs` to not immediately instantiate the `skiko.wasm`
157+
// module, so that we can move the dynamic JS import here and measure
158+
// WebAssembly compilation and instantiation as part of the first iteration.
159+
this.skikoInstantiate = (await dynamicJSImport('Kotlin-compose/build/skiko.mjs')).default;
160+
this.mainInstantiate = (await dynamicJSImport('Kotlin-compose/build/compose-benchmarks-benchmarks-wasm-js.uninstantiated.mjs')).instantiate;
161+
}
162+
163+
async runIteration() {
164+
// DEBUG
165+
// console.log("runIteration");
166+
167+
// Compile once in the first iteration.
168+
if (!this.wasmInstanceExports) {
169+
const skikoExports = (await this.skikoInstantiate()).wasmExports;
170+
this.wasmInstanceExports = (await this.mainInstantiate({ './skiko.mjs': skikoExports })).exports;
171+
}
172+
173+
// We render/animate/process fewer frames than in the upstream benchmark,
174+
// since we run multiple iterations in JetStream (to measure first, worst,
175+
// and average runtime) and don't want the overall workload to take too long.
176+
const frameCountFactor = 5;
177+
178+
// The factors for the subitems are chosen to make them take the same order
179+
// of magnitude in terms of Wall time.
180+
await this.wasmInstanceExports.customLaunch("AnimatedVisibility", 100 * frameCountFactor);
181+
await this.wasmInstanceExports.customLaunch("LazyGrid", 1 * frameCountFactor);
182+
await this.wasmInstanceExports.customLaunch("LazyGrid-ItemLaunchedEffect", 1 * frameCountFactor);
183+
// The `SmoothScroll` variants of the LazyGrid workload are much faster.
184+
await this.wasmInstanceExports.customLaunch("LazyGrid-SmoothScroll", 5 * frameCountFactor);
185+
await this.wasmInstanceExports.customLaunch("LazyGrid-SmoothScroll-ItemLaunchedEffect", 5 * frameCountFactor);
186+
// This is quite GC-heavy, is this realistic for Kotlin/compose applications?
187+
await this.wasmInstanceExports.customLaunch("VisualEffects", 1 * frameCountFactor);
188+
await this.wasmInstanceExports.customLaunch("MultipleComponents-NoVectorGraphics", 10 * frameCountFactor);
189+
}
190+
}

Kotlin-compose/build.log

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Built on 2025-07-08 12:20:38+02:00
2+
754dc25396 Update CHANGELOG for 1.9.0-alpha03 release (#5349)
3+
Copying generated files into build/
4+
Build success

Kotlin-compose/build.sh

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
#!/bin/bash
2+
3+
set -eo pipefail
4+
5+
# Cleanup old files.
6+
rm -rf build/
7+
8+
BUILD_LOG="$(realpath build.log)"
9+
echo -e "Built on $(date --rfc-3339=seconds)" | tee "$BUILD_LOG"
10+
11+
# Build the benchmark from source.
12+
git clone https://github.com/JetBrains/compose-multiplatform.git |& tee -a "$BUILD_LOG"
13+
pushd compose-multiplatform/
14+
git log -1 --oneline | tee -a "$BUILD_LOG"
15+
pushd benchmarks/multiplatform
16+
./gradlew :benchmarks:wasmJsProductionExecutableCompileSync
17+
# For building polyfills and JavaScript launcher to run in d8 (which inspires the benchmark.js launcher here):
18+
# ./gradlew :benchmarks:buildD8Distribution
19+
BUILD_SRC_DIR="compose-multiplatform/benchmarks/multiplatform/build/js/packages/compose-benchmarks-benchmarks-wasm-js/kotlin"
20+
popd
21+
popd
22+
23+
echo "Copying generated files into build/" | tee -a "$BUILD_LOG"
24+
mkdir -p build/drawable/ | tee -a "$BUILD_LOG"
25+
cp $BUILD_SRC_DIR/compose-benchmarks-benchmarks-wasm-js.{wasm,uninstantiated.mjs} build/ | tee -a "$BUILD_LOG"
26+
git apply hook-print.patch | tee -a "$BUILD_LOG"
27+
cp $BUILD_SRC_DIR/skiko.{wasm,mjs} build/ | tee -a "$BUILD_LOG"
28+
git apply skiko-disable-instantiate.patch | tee -a "$BUILD_LOG"
29+
cp $BUILD_SRC_DIR/composeResources/compose_benchmarks.benchmarks.generated.resources/drawable/example1_cat.jpg build/ | tee -a "$BUILD_LOG"
30+
cp $BUILD_SRC_DIR/composeResources/compose_benchmarks.benchmarks.generated.resources/drawable/compose-multiplatform.png build/ | tee -a "$BUILD_LOG"
31+
cp $BUILD_SRC_DIR/composeResources/compose_benchmarks.benchmarks.generated.resources/files/example1_compose-community-primary.png build/ | tee -a "$BUILD_LOG"
32+
cp $BUILD_SRC_DIR/composeResources/compose_benchmarks.benchmarks.generated.resources/font/jetbrainsmono_*.ttf build/ | tee -a "$BUILD_LOG"
33+
echo "Build success" | tee -a "$BUILD_LOG"

0 commit comments

Comments
 (0)