Skip to content

Kotlin Compose Multiplatform WasmGC workload #84

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 15 commits into from
Aug 13, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions JetStreamDriver.js
Original file line number Diff line number Diff line change
Expand Up @@ -2069,6 +2069,26 @@ let BENCHMARKS = [
worstCaseCount: 2,
tags: ["Default", "Wasm"],
}),
new WasmEMCCBenchmark({
name: "Kotlin-compose-wasm",
files: [
"./Kotlin-compose/benchmark.js",
],
preload: {
skikoJsModule: "./Kotlin-compose/build/skiko.mjs",
skikoWasmBinary: "./Kotlin-compose/build/skiko.wasm",
composeJsModule: "./Kotlin-compose/build/compose-benchmarks-benchmarks.uninstantiated.mjs",
composeWasmBinary: "./Kotlin-compose/build/compose-benchmarks-benchmarks.wasm",
inputImageCompose: "./Kotlin-compose/build/compose-multiplatform.png",
inputImageCat: "./Kotlin-compose/build/example1_cat.jpg",
inputImageComposeCommunity: "./Kotlin-compose/build/example1_compose-community-primary.png",
inputFontItalic: "./Kotlin-compose/build/jetbrainsmono_italic.ttf",
inputFontRegular: "./Kotlin-compose/build/jetbrainsmono_regular.ttf"
},
iterations: 15,
worstCaseCount: 2,
tags: ["Default", "Wasm"],
}),
new WasmLegacyBenchmark({
name: "tfjs-wasm",
files: [
Expand Down
1 change: 1 addition & 0 deletions Kotlin-compose/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/compose-multiplatform/
18 changes: 18 additions & 0 deletions Kotlin-compose/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Kotlin/Wasm Compose Multiplatform Benchmark

Citing https://github.com/JetBrains/compose-multiplatform:

> [Compose Multiplatform](https://jb.gg/cmp) is a declarative framework for sharing UIs across multiple platforms with Kotlin.
[...]
Compose for Web is based on [Kotlin/Wasm](https://kotl.in/wasm), the newest target for Kotlin Multiplatform projects.
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.

## Build Instructions

See or run `build.sh`.
See `build.log` for the last build time, used sources, and toolchain versions.

## Running in JS shells

To run the unmodified upstream benchmark, without the JetStream driver, see the
upstream repo, specifically https://github.com/JetBrains/compose-multiplatform/blob/master/benchmarks/multiplatform/README.md
158 changes: 158 additions & 0 deletions Kotlin-compose/benchmark.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
// Copyright 2025 the V8 project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

// Excerpt from `polyfills.mjs` from the upstream Kotlin compose-multiplatform
// benchmark directory, with minor changes for JetStream.

globalThis.window ??= globalThis;

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;

// Disable explicit GC (it wouldn't work in browsers anyway).
globalThis.gc = () => {
// DEBUG
// console.log("gc()");
}

class URL {
href;
constructor(url, base) {
// DEBUG
// console.log('URL', url, base);
this.href = url;
}
}
globalThis.URL = URL;

// We always polyfill `fetch` and `instantiateStreaming` for consistency between
// engine shells and browsers and to avoid introducing network latency into the
// first iteration / instantiation measurement.
// The downside is that this doesn't test streaming Wasm instantiation, which we
// are willing to accept.
let preload = { /* Initialized in init() below due to async. */ };
const originalFetch = globalThis.fetch ?? function(url) {
throw new Error("no fetch available");
}
globalThis.fetch = async function(url) {
// DEBUG
// console.log('fetch', url);

// Redirect some paths to cached/preloaded resources.
if (preload[url]) {
return {
ok: true,
status: 200,
arrayBuffer() { return preload[url]; },
async blob() {
return {
size: preload[url].byteLength,
async arrayBuffer() { return preload[url]; }
}
},
};
}

// This should only be called in the browser, where fetch() is available.
return originalFetch(url);
};
globalThis.WebAssembly.instantiateStreaming = async function(m,i) {
// DEBUG
// console.log('instantiateStreaming',m,i);
return WebAssembly.instantiate((await m).arrayBuffer(),i);
};

// Provide `setTimeout` for Kotlin coroutines.
const originalSetTimeout = setTimeout;
globalThis.setTimeout = function(f, delayMs) {
// DEBUG
// console.log('setTimeout', f, t);

// Deep in the Compose UI framework, one task is scheduled every 16ms, see
// https://github.com/JetBrains/compose-multiplatform-core/blob/a52f2981b9bc7cdba1d1fbe71654c4be448ebea7/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/spatial/RectManager.kt#L138
// and
// https://github.com/JetBrains/compose-multiplatform-core/blob/a52f2981b9bc7cdba1d1fbe71654c4be448ebea7/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/OnLayoutRectChangedModifier.kt#L56
// We don't want to delay work in the Wall-time based measurement in JetStream,
// but executing this immediately (without delay) produces redundant work that
// is not realistic for a full-browser Kotlin/multiplatform application either,
// according to Kotlin/JetBrains folks.
// Hence the early return for 16ms delays.
if (delayMs === 16) return;
if (delayMs !== 0) {
throw new Error('Unexpected delay for setTimeout polyfill: ' + delayMs);
}
originalSetTimeout(f);
}

// Don't automatically run the main function on instantiation.
globalThis.skipFunMain = true;

// Prevent this from being detected as a shell environment, so that we use the
// same code paths as in the browser.
// See `compose-benchmarks-benchmarks.uninstantiated.mjs`.
delete globalThis.d8;
delete globalThis.inIon;
delete globalThis.jscOptions;

class Benchmark {
skikoInstantiate;
mainInstantiate;
wasmInstanceExports;

async init() {
// DEBUG
// console.log("init");

preload = {
'skiko.wasm': await getBinary(skikoWasmBinary),
'./compose-benchmarks-benchmarks.wasm': await getBinary(composeWasmBinary),
'./composeResources/compose_benchmarks.benchmarks.generated.resources/drawable/compose-multiplatform.png': await getBinary(inputImageCompose),
'./composeResources/compose_benchmarks.benchmarks.generated.resources/drawable/example1_cat.jpg': await getBinary(inputImageCat),
'./composeResources/compose_benchmarks.benchmarks.generated.resources/files/example1_compose-community-primary.png': await getBinary(inputImageComposeCommunity),
'./composeResources/compose_benchmarks.benchmarks.generated.resources/font/jetbrainsmono_italic.ttf': await getBinary(inputFontItalic),
'./composeResources/compose_benchmarks.benchmarks.generated.resources/font/jetbrainsmono_regular.ttf': await getBinary(inputFontRegular),
};

// We patched `skiko.mjs` to not immediately instantiate the `skiko.wasm`
// module, so that we can move the dynamic JS import here but measure
// WebAssembly compilation and instantiation as part of the first iteration.
this.skikoInstantiate = (await dynamicImport(skikoJsModule)).default;
this.mainInstantiate = (await dynamicImport(composeJsModule)).instantiate;
}

async runIteration() {
// DEBUG
// console.log("runIteration");

// Compile once in the first iteration.
if (!this.wasmInstanceExports) {
const skikoExports = (await this.skikoInstantiate()).wasmExports;
this.wasmInstanceExports = (await this.mainInstantiate({ './skiko.mjs': skikoExports })).exports;
}

// We render/animate/process fewer frames than in the upstream benchmark,
// since we run multiple iterations in JetStream (to measure first, worst,
// and average runtime) and don't want the overall workload to take too long.
const frameCountFactor = 5;

// The factors for the subitems are chosen to make them take the same order
// of magnitude in terms of Wall time.
await this.wasmInstanceExports.customLaunch("AnimatedVisibility", 100 * frameCountFactor);
await this.wasmInstanceExports.customLaunch("LazyGrid", 1 * frameCountFactor);
await this.wasmInstanceExports.customLaunch("LazyGrid-ItemLaunchedEffect", 1 * frameCountFactor);
// The `SmoothScroll` variants of the LazyGrid workload are much faster.
await this.wasmInstanceExports.customLaunch("LazyGrid-SmoothScroll", 5 * frameCountFactor);
await this.wasmInstanceExports.customLaunch("LazyGrid-SmoothScroll-ItemLaunchedEffect", 5 * frameCountFactor);
// This is quite GC-heavy, is this realistic for Kotlin/compose applications?
await this.wasmInstanceExports.customLaunch("VisualEffects", 1 * frameCountFactor);
await this.wasmInstanceExports.customLaunch("MultipleComponents-NoVectorGraphics", 10 * frameCountFactor);
}
}
5 changes: 5 additions & 0 deletions Kotlin-compose/build.log
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Built on 2025-08-08 16:11:09+02:00
Cloning into 'compose-multiplatform'...
84dad4d3f6 Use custom skiko (0.9.4.3) to fix the FinalizationRegistry API usage for web targets
Copying generated files into build/
Build success
41 changes: 41 additions & 0 deletions Kotlin-compose/build.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
#!/bin/bash

set -eo pipefail

# Cleanup old files.
rm -rf build/

BUILD_LOG="$(realpath build.log)"
echo -e "Built on $(date --rfc-3339=seconds)" | tee "$BUILD_LOG"

# Build the benchmark from source.
# FIXME: Use main branch and remove hotfix patch below, once
# https://youtrack.jetbrains.com/issue/SKIKO-1040 is resolved upstream.
# See https://github.com/WebKit/JetStream/pull/84#discussion_r2252418900.
git clone -b ok/jetstream3_hotfix https://github.com/JetBrains/compose-multiplatform.git |& tee -a "$BUILD_LOG"
pushd compose-multiplatform/
git log -1 --oneline | tee -a "$BUILD_LOG"
# FIXME: Use stable 2.3 Kotlin/Wasm toolchain, once available around Sep 2025.
git apply ../use-beta-toolchain.patch | tee -a "$BUILD_LOG"
pushd benchmarks/multiplatform
./gradlew :benchmarks:wasmJsProductionExecutableCompileSync
# For building polyfills and JavaScript launcher to run in d8 (which inspires the benchmark.js launcher here):
# ./gradlew :benchmarks:buildD8Distribution
BUILD_SRC_DIR="compose-multiplatform/benchmarks/multiplatform/build/wasm/packages/compose-benchmarks-benchmarks/kotlin"
popd
popd

echo "Copying generated files into build/" | tee -a "$BUILD_LOG"
mkdir -p build/ | tee -a "$BUILD_LOG"
cp $BUILD_SRC_DIR/compose-benchmarks-benchmarks.{wasm,uninstantiated.mjs} build/ | tee -a "$BUILD_LOG"
git apply hook-print.patch | tee -a "$BUILD_LOG"
# FIXME: Remove once the JSC fixes around JSTag have landed in Safari, see
# https://github.com/WebKit/JetStream/pull/84#issuecomment-3164672425.
git apply jstag-workaround.patch | tee -a "$BUILD_LOG"
cp $BUILD_SRC_DIR/skiko.{wasm,mjs} build/ | tee -a "$BUILD_LOG"
git apply skiko-disable-instantiate.patch | tee -a "$BUILD_LOG"
cp $BUILD_SRC_DIR/composeResources/compose_benchmarks.benchmarks.generated.resources/drawable/example1_cat.jpg build/ | tee -a "$BUILD_LOG"
cp $BUILD_SRC_DIR/composeResources/compose_benchmarks.benchmarks.generated.resources/drawable/compose-multiplatform.png build/ | tee -a "$BUILD_LOG"
cp $BUILD_SRC_DIR/composeResources/compose_benchmarks.benchmarks.generated.resources/files/example1_compose-community-primary.png build/ | tee -a "$BUILD_LOG"
cp $BUILD_SRC_DIR/composeResources/compose_benchmarks.benchmarks.generated.resources/font/jetbrainsmono_*.ttf build/ | tee -a "$BUILD_LOG"
echo "Build success" | tee -a "$BUILD_LOG"
Loading
Loading