Skip to content

Commit e803e0b

Browse files
authored
Merge pull request #84 from danleh/kotlin
Kotlin Compose Multiplatform WasmGC workload
2 parents e39273b + 9ba5e52 commit e803e0b

20 files changed

+10073
-0
lines changed

JetStreamDriver.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2069,6 +2069,26 @@ let BENCHMARKS = [
20692069
worstCaseCount: 2,
20702070
tags: ["Default", "Wasm"],
20712071
}),
2072+
new WasmEMCCBenchmark({
2073+
name: "Kotlin-compose-wasm",
2074+
files: [
2075+
"./Kotlin-compose/benchmark.js",
2076+
],
2077+
preload: {
2078+
skikoJsModule: "./Kotlin-compose/build/skiko.mjs",
2079+
skikoWasmBinary: "./Kotlin-compose/build/skiko.wasm",
2080+
composeJsModule: "./Kotlin-compose/build/compose-benchmarks-benchmarks.uninstantiated.mjs",
2081+
composeWasmBinary: "./Kotlin-compose/build/compose-benchmarks-benchmarks.wasm",
2082+
inputImageCompose: "./Kotlin-compose/build/compose-multiplatform.png",
2083+
inputImageCat: "./Kotlin-compose/build/example1_cat.jpg",
2084+
inputImageComposeCommunity: "./Kotlin-compose/build/example1_compose-community-primary.png",
2085+
inputFontItalic: "./Kotlin-compose/build/jetbrainsmono_italic.ttf",
2086+
inputFontRegular: "./Kotlin-compose/build/jetbrainsmono_regular.ttf"
2087+
},
2088+
iterations: 15,
2089+
worstCaseCount: 2,
2090+
tags: ["Default", "Wasm"],
2091+
}),
20722092
new WasmLegacyBenchmark({
20732093
name: "tfjs-wasm",
20742094
files: [

Kotlin-compose/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/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 or run `build.sh`.
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, specifically https://github.com/JetBrains/compose-multiplatform/blob/master/benchmarks/multiplatform/README.md

Kotlin-compose/benchmark.js

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
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 = { /* Initialized in init() below due to async. */ };
42+
const originalFetch = globalThis.fetch ?? function(url) {
43+
throw new Error("no fetch available");
44+
}
45+
globalThis.fetch = async function(url) {
46+
// DEBUG
47+
// console.log('fetch', url);
48+
49+
// Redirect some paths to cached/preloaded resources.
50+
if (preload[url]) {
51+
return {
52+
ok: true,
53+
status: 200,
54+
arrayBuffer() { return preload[url]; },
55+
async blob() {
56+
return {
57+
size: preload[url].byteLength,
58+
async arrayBuffer() { return preload[url]; }
59+
}
60+
},
61+
};
62+
}
63+
64+
// This should only be called in the browser, where fetch() is available.
65+
return originalFetch(url);
66+
};
67+
globalThis.WebAssembly.instantiateStreaming = async function(m,i) {
68+
// DEBUG
69+
// console.log('instantiateStreaming',m,i);
70+
return WebAssembly.instantiate((await m).arrayBuffer(),i);
71+
};
72+
73+
// Provide `setTimeout` for Kotlin coroutines.
74+
const originalSetTimeout = setTimeout;
75+
globalThis.setTimeout = function(f, delayMs) {
76+
// DEBUG
77+
// console.log('setTimeout', f, t);
78+
79+
// Deep in the Compose UI framework, one task is scheduled every 16ms, see
80+
// https://github.com/JetBrains/compose-multiplatform-core/blob/a52f2981b9bc7cdba1d1fbe71654c4be448ebea7/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/spatial/RectManager.kt#L138
81+
// and
82+
// https://github.com/JetBrains/compose-multiplatform-core/blob/a52f2981b9bc7cdba1d1fbe71654c4be448ebea7/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/OnLayoutRectChangedModifier.kt#L56
83+
// We don't want to delay work in the Wall-time based measurement in JetStream,
84+
// but executing this immediately (without delay) produces redundant work that
85+
// is not realistic for a full-browser Kotlin/multiplatform application either,
86+
// according to Kotlin/JetBrains folks.
87+
// Hence the early return for 16ms delays.
88+
if (delayMs === 16) return;
89+
if (delayMs !== 0) {
90+
throw new Error('Unexpected delay for setTimeout polyfill: ' + delayMs);
91+
}
92+
originalSetTimeout(f);
93+
}
94+
95+
// Don't automatically run the main function on instantiation.
96+
globalThis.skipFunMain = true;
97+
98+
// Prevent this from being detected as a shell environment, so that we use the
99+
// same code paths as in the browser.
100+
// See `compose-benchmarks-benchmarks.uninstantiated.mjs`.
101+
delete globalThis.d8;
102+
delete globalThis.inIon;
103+
delete globalThis.jscOptions;
104+
105+
class Benchmark {
106+
skikoInstantiate;
107+
mainInstantiate;
108+
wasmInstanceExports;
109+
110+
async init() {
111+
// DEBUG
112+
// console.log("init");
113+
114+
preload = {
115+
'skiko.wasm': await getBinary(skikoWasmBinary),
116+
'./compose-benchmarks-benchmarks.wasm': await getBinary(composeWasmBinary),
117+
'./composeResources/compose_benchmarks.benchmarks.generated.resources/drawable/compose-multiplatform.png': await getBinary(inputImageCompose),
118+
'./composeResources/compose_benchmarks.benchmarks.generated.resources/drawable/example1_cat.jpg': await getBinary(inputImageCat),
119+
'./composeResources/compose_benchmarks.benchmarks.generated.resources/files/example1_compose-community-primary.png': await getBinary(inputImageComposeCommunity),
120+
'./composeResources/compose_benchmarks.benchmarks.generated.resources/font/jetbrainsmono_italic.ttf': await getBinary(inputFontItalic),
121+
'./composeResources/compose_benchmarks.benchmarks.generated.resources/font/jetbrainsmono_regular.ttf': await getBinary(inputFontRegular),
122+
};
123+
124+
// We patched `skiko.mjs` to not immediately instantiate the `skiko.wasm`
125+
// module, so that we can move the dynamic JS import here but measure
126+
// WebAssembly compilation and instantiation as part of the first iteration.
127+
this.skikoInstantiate = (await dynamicImport(skikoJsModule)).default;
128+
this.mainInstantiate = (await dynamicImport(composeJsModule)).instantiate;
129+
}
130+
131+
async runIteration() {
132+
// DEBUG
133+
// console.log("runIteration");
134+
135+
// Compile once in the first iteration.
136+
if (!this.wasmInstanceExports) {
137+
const skikoExports = (await this.skikoInstantiate()).wasmExports;
138+
this.wasmInstanceExports = (await this.mainInstantiate({ './skiko.mjs': skikoExports })).exports;
139+
}
140+
141+
// We render/animate/process fewer frames than in the upstream benchmark,
142+
// since we run multiple iterations in JetStream (to measure first, worst,
143+
// and average runtime) and don't want the overall workload to take too long.
144+
const frameCountFactor = 5;
145+
146+
// The factors for the subitems are chosen to make them take the same order
147+
// of magnitude in terms of Wall time.
148+
await this.wasmInstanceExports.customLaunch("AnimatedVisibility", 100 * frameCountFactor);
149+
await this.wasmInstanceExports.customLaunch("LazyGrid", 1 * frameCountFactor);
150+
await this.wasmInstanceExports.customLaunch("LazyGrid-ItemLaunchedEffect", 1 * frameCountFactor);
151+
// The `SmoothScroll` variants of the LazyGrid workload are much faster.
152+
await this.wasmInstanceExports.customLaunch("LazyGrid-SmoothScroll", 5 * frameCountFactor);
153+
await this.wasmInstanceExports.customLaunch("LazyGrid-SmoothScroll-ItemLaunchedEffect", 5 * frameCountFactor);
154+
// This is quite GC-heavy, is this realistic for Kotlin/compose applications?
155+
await this.wasmInstanceExports.customLaunch("VisualEffects", 1 * frameCountFactor);
156+
await this.wasmInstanceExports.customLaunch("MultipleComponents-NoVectorGraphics", 10 * frameCountFactor);
157+
}
158+
}

Kotlin-compose/build.log

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Built on 2025-08-08 16:11:09+02:00
2+
Cloning into 'compose-multiplatform'...
3+
84dad4d3f6 Use custom skiko (0.9.4.3) to fix the FinalizationRegistry API usage for web targets
4+
Copying generated files into build/
5+
Build success

Kotlin-compose/build.sh

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
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+
# FIXME: Use main branch and remove hotfix patch below, once
13+
# https://youtrack.jetbrains.com/issue/SKIKO-1040 is resolved upstream.
14+
# See https://github.com/WebKit/JetStream/pull/84#discussion_r2252418900.
15+
git clone -b ok/jetstream3_hotfix https://github.com/JetBrains/compose-multiplatform.git |& tee -a "$BUILD_LOG"
16+
pushd compose-multiplatform/
17+
git log -1 --oneline | tee -a "$BUILD_LOG"
18+
# FIXME: Use stable 2.3 Kotlin/Wasm toolchain, once available around Sep 2025.
19+
git apply ../use-beta-toolchain.patch | tee -a "$BUILD_LOG"
20+
pushd benchmarks/multiplatform
21+
./gradlew :benchmarks:wasmJsProductionExecutableCompileSync
22+
# For building polyfills and JavaScript launcher to run in d8 (which inspires the benchmark.js launcher here):
23+
# ./gradlew :benchmarks:buildD8Distribution
24+
BUILD_SRC_DIR="compose-multiplatform/benchmarks/multiplatform/build/wasm/packages/compose-benchmarks-benchmarks/kotlin"
25+
popd
26+
popd
27+
28+
echo "Copying generated files into build/" | tee -a "$BUILD_LOG"
29+
mkdir -p build/ | tee -a "$BUILD_LOG"
30+
cp $BUILD_SRC_DIR/compose-benchmarks-benchmarks.{wasm,uninstantiated.mjs} build/ | tee -a "$BUILD_LOG"
31+
git apply hook-print.patch | tee -a "$BUILD_LOG"
32+
# FIXME: Remove once the JSC fixes around JSTag have landed in Safari, see
33+
# https://github.com/WebKit/JetStream/pull/84#issuecomment-3164672425.
34+
git apply jstag-workaround.patch | tee -a "$BUILD_LOG"
35+
cp $BUILD_SRC_DIR/skiko.{wasm,mjs} build/ | tee -a "$BUILD_LOG"
36+
git apply skiko-disable-instantiate.patch | tee -a "$BUILD_LOG"
37+
cp $BUILD_SRC_DIR/composeResources/compose_benchmarks.benchmarks.generated.resources/drawable/example1_cat.jpg build/ | tee -a "$BUILD_LOG"
38+
cp $BUILD_SRC_DIR/composeResources/compose_benchmarks.benchmarks.generated.resources/drawable/compose-multiplatform.png build/ | tee -a "$BUILD_LOG"
39+
cp $BUILD_SRC_DIR/composeResources/compose_benchmarks.benchmarks.generated.resources/files/example1_compose-community-primary.png build/ | tee -a "$BUILD_LOG"
40+
cp $BUILD_SRC_DIR/composeResources/compose_benchmarks.benchmarks.generated.resources/font/jetbrainsmono_*.ttf build/ | tee -a "$BUILD_LOG"
41+
echo "Build success" | tee -a "$BUILD_LOG"

0 commit comments

Comments
 (0)