Skip to content

Commit 8f56a4e

Browse files
authored
Merge pull request #33 from danleh/dart-flute-wasm-squash
Add Dart flute WasmGC workload
2 parents 3d36c5b + 42a9adc commit 8f56a4e

10 files changed

+1249
-0
lines changed

Dart/.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/wasm_gc_benchmarks/

Dart/README.md

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# Dart flute benchmark application
2+
3+
This is a WasmGC build of the `flute` benchmark, a Dart application using the
4+
`Flutter` UI framework.
5+
It performs the Wasm/compute-only part of rendering a certain number of frames.
6+
Since JetStream runs in JS engine shells, the actual rendering to a Canvas is
7+
stubbed out.
8+
The upstream repository containing pre-built WebAssembly binaries (which we
9+
use here) is at https://github.com/mkustermann/wasm_gc_benchmarks.
10+
The Dart source code of the flute application is at https://github.com/dart-lang/flute.
11+
12+
## Build Instructions
13+
14+
See `build.sh` or just run it.
15+
See `build.log` for the last build time, used sources, and toolchain versions.
16+
17+
## Running in JS shells
18+
19+
To run the unmodified upstream benchmark, without the JetStream driver, see the
20+
upstream repo.
21+
In short, the main runner is `build/run_wasm.js`, which takes as arguments a
22+
application-specific generated JS and Wasm file, and the arguments passed to
23+
the Dart main method.
24+
Since different engines / shells resolve JS modules and parse command-line
25+
arguments differently, the invocations are something like (from this directory):
26+
27+
```
28+
path/to/d8 build/run_wasm.js -- flute.dart2wasm.mjs build/flute.dart2wasm.wasm -- $(date +%s.%N) 1000
29+
path/to/spidermonkey/js build/run_wasm.js build/flute.dart2wasm.mjs flute.dart2wasm.wasm -- $(date +%s.%N) 1000
30+
path/to/jsc build/run_wasm.js -- ./flute.dart2wasm.mjs build/flute.dart2wasm.wasm -- $(date +%s.%N) 1000
31+
```

Dart/benchmark.js

+310
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,310 @@
1+
// Copyright 2024 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 `build/run_wasm.js` to add own task queue implementation, since
6+
// `setTimeout` and `queueMicrotask` are not always available in shells.
7+
function addTaskQueue(self) {
8+
"use strict";
9+
10+
// Task queue as cyclic list queue.
11+
var taskQueue = new Array(8); // Length is power of 2.
12+
var head = 0;
13+
var tail = 0;
14+
var mask = taskQueue.length - 1;
15+
16+
function addTask(elem) {
17+
taskQueue[head] = elem;
18+
head = (head + 1) & mask;
19+
if (head == tail) _growTaskQueue();
20+
}
21+
22+
function removeTask() {
23+
if (head == tail) return;
24+
var result = taskQueue[tail];
25+
taskQueue[tail] = undefined;
26+
tail = (tail + 1) & mask;
27+
return result;
28+
}
29+
30+
function _growTaskQueue() {
31+
// head == tail.
32+
var length = taskQueue.length;
33+
var split = head;
34+
taskQueue.length = length * 2;
35+
if (split * 2 < length) { // split < length / 2
36+
for (var i = 0; i < split; i++) {
37+
taskQueue[length + i] = taskQueue[i];
38+
taskQueue[i] = undefined;
39+
}
40+
head += length;
41+
} else {
42+
for (var i = split; i < length; i++) {
43+
taskQueue[length + i] = taskQueue[i];
44+
taskQueue[i] = undefined;
45+
}
46+
tail += length;
47+
}
48+
mask = taskQueue.length - 1;
49+
}
50+
51+
// Mapping from timer id to timer function.
52+
// The timer id is written on the function as .$timerId.
53+
// That field is cleared when the timer is cancelled, but it is not returned
54+
// from the queue until its time comes.
55+
var timerIds = {};
56+
var timerIdCounter = 1; // Counter used to assign ids.
57+
58+
// Zero-timer queue as simple array queue using push/shift.
59+
var zeroTimerQueue = [];
60+
61+
function addTimer(f, ms) {
62+
ms = Math.max(0, ms);
63+
var id = timerIdCounter++;
64+
// A callback can be scheduled at most once.
65+
// (console.assert is only available on D8)
66+
// if (isD8) console.assert(f.$timerId === undefined);
67+
f.$timerId = id;
68+
timerIds[id] = f;
69+
if (ms == 0 && !isNextTimerDue()) {
70+
zeroTimerQueue.push(f);
71+
} else {
72+
addDelayedTimer(f, ms);
73+
}
74+
return id;
75+
}
76+
77+
function nextZeroTimer() {
78+
while (zeroTimerQueue.length > 0) {
79+
var action = zeroTimerQueue.shift();
80+
if (action.$timerId !== undefined) return action;
81+
}
82+
}
83+
84+
function nextEvent() {
85+
var action = removeTask();
86+
if (action) {
87+
return action;
88+
}
89+
do {
90+
action = nextZeroTimer();
91+
if (action) break;
92+
var nextList = nextDelayedTimerQueue();
93+
if (!nextList) {
94+
return;
95+
}
96+
var newTime = nextList.shift();
97+
advanceTimeTo(newTime);
98+
zeroTimerQueue = nextList;
99+
} while (true)
100+
var id = action.$timerId;
101+
clearTimerId(action, id);
102+
return action;
103+
}
104+
105+
// Mocking time.
106+
var timeOffset = 0;
107+
var now = function() {
108+
// Install the mock Date object only once.
109+
// Following calls to "now" will just use the new (mocked) Date.now
110+
// method directly.
111+
installMockDate();
112+
now = Date.now;
113+
return Date.now();
114+
};
115+
var originalDate = Date;
116+
var originalNow = originalDate.now;
117+
118+
function advanceTimeTo(time) {
119+
var now = originalNow();
120+
if (timeOffset < time - now) {
121+
timeOffset = time - now;
122+
}
123+
}
124+
125+
function installMockDate() {
126+
var NewDate = function Date(Y, M, D, h, m, s, ms) {
127+
if (this instanceof Date) {
128+
// Assume a construct call.
129+
switch (arguments.length) {
130+
case 0: return new originalDate(originalNow() + timeOffset);
131+
case 1: return new originalDate(Y);
132+
case 2: return new originalDate(Y, M);
133+
case 3: return new originalDate(Y, M, D);
134+
case 4: return new originalDate(Y, M, D, h);
135+
case 5: return new originalDate(Y, M, D, h, m);
136+
case 6: return new originalDate(Y, M, D, h, m, s);
137+
default: return new originalDate(Y, M, D, h, m, s, ms);
138+
}
139+
}
140+
return new originalDate(originalNow() + timeOffset).toString();
141+
};
142+
NewDate.UTC = originalDate.UTC;
143+
NewDate.parse = originalDate.parse;
144+
NewDate.now = function now() { return originalNow() + timeOffset; };
145+
NewDate.prototype = originalDate.prototype;
146+
originalDate.prototype.constructor = NewDate;
147+
Date = NewDate;
148+
}
149+
150+
// Heap priority queue with key index.
151+
// Each entry is list of [timeout, callback1 ... callbackn].
152+
var timerHeap = [];
153+
var timerIndex = {};
154+
155+
function addDelayedTimer(f, ms) {
156+
var timeout = now() + ms;
157+
var timerList = timerIndex[timeout];
158+
if (timerList == null) {
159+
timerList = [timeout, f];
160+
timerIndex[timeout] = timerList;
161+
var index = timerHeap.length;
162+
timerHeap.length += 1;
163+
bubbleUp(index, timeout, timerList);
164+
} else {
165+
timerList.push(f);
166+
}
167+
}
168+
169+
function isNextTimerDue() {
170+
if (timerHeap.length == 0) return false;
171+
var head = timerHeap[0];
172+
return head[0] < originalNow() + timeOffset;
173+
}
174+
175+
function nextDelayedTimerQueue() {
176+
if (timerHeap.length == 0) return null;
177+
var result = timerHeap[0];
178+
var last = timerHeap.pop();
179+
if (timerHeap.length > 0) {
180+
bubbleDown(0, last[0], last);
181+
}
182+
return result;
183+
}
184+
185+
function bubbleUp(index, key, value) {
186+
while (index != 0) {
187+
var parentIndex = (index - 1) >> 1;
188+
var parent = timerHeap[parentIndex];
189+
var parentKey = parent[0];
190+
if (key > parentKey) break;
191+
timerHeap[index] = parent;
192+
index = parentIndex;
193+
}
194+
timerHeap[index] = value;
195+
}
196+
197+
function bubbleDown(index, key, value) {
198+
while (true) {
199+
var leftChildIndex = index * 2 + 1;
200+
if (leftChildIndex >= timerHeap.length) break;
201+
var minChildIndex = leftChildIndex;
202+
var minChild = timerHeap[leftChildIndex];
203+
var minChildKey = minChild[0];
204+
var rightChildIndex = leftChildIndex + 1;
205+
if (rightChildIndex < timerHeap.length) {
206+
var rightChild = timerHeap[rightChildIndex];
207+
var rightKey = rightChild[0];
208+
if (rightKey < minChildKey) {
209+
minChildIndex = rightChildIndex;
210+
minChild = rightChild;
211+
minChildKey = rightKey;
212+
}
213+
}
214+
if (minChildKey > key) break;
215+
timerHeap[index] = minChild;
216+
index = minChildIndex;
217+
}
218+
timerHeap[index] = value;
219+
}
220+
221+
function cancelTimer(id) {
222+
var f = timerIds[id];
223+
if (f == null) return;
224+
clearTimerId(f, id);
225+
}
226+
227+
function clearTimerId(f, id) {
228+
f.$timerId = undefined;
229+
delete timerIds[id];
230+
}
231+
232+
async function eventLoop(action) {
233+
while (action) {
234+
try {
235+
await action();
236+
} catch (e) {
237+
if (typeof onerror == "function") {
238+
onerror(e, null, -1);
239+
} else {
240+
throw e;
241+
}
242+
}
243+
action = nextEvent();
244+
}
245+
}
246+
247+
self.setTimeout = addTimer;
248+
self.clearTimeout = cancelTimer;
249+
self.queueMicrotask = addTask;
250+
self.eventLoop = eventLoop;
251+
}
252+
253+
function dartPrint(...args) { print(args); }
254+
addTaskQueue(globalThis);
255+
globalThis.window ??= globalThis;
256+
257+
class Benchmark {
258+
dart2wasmJsModule;
259+
compiledApp;
260+
261+
async init() {
262+
// The generated JavaScript code from dart2wasm is an ES module, which we
263+
// can only load with a dynamic import (since this file is not a module.)
264+
// TODO: Support ES6 modules in the driver instead of this one-off solution.
265+
// This probably requires a new `Benchmark` field called `modules` that
266+
// is a map from module variable name (which will hold the resulting module
267+
// namespace object) to relative module URL, which is resolved in the
268+
// `preRunnerCode`, similar to this code here.
269+
if (isInBrowser) {
270+
// In browsers, relative imports don't work since we are not in a module.
271+
// (`import.meta.url` is not defined.)
272+
this.dart2wasmJsModule = await import(location.origin + "/Dart/build/flute.dart2wasm.mjs");
273+
} else {
274+
// In shells, relative imports require different paths, so try with and
275+
// without the "./" prefix (e.g., JSC requires it).
276+
try {
277+
this.dart2wasmJsModule = await import("Dart/build/flute.dart2wasm.mjs");
278+
} catch {
279+
this.dart2wasmJsModule = await import("./Dart/build/flute.dart2wasm.mjs");
280+
}
281+
}
282+
}
283+
284+
async runIteration() {
285+
// Compile once in the first iteration.
286+
if (!this.compiledApp) {
287+
this.compiledApp = await this.dart2wasmJsModule.compile(Module.wasmBinary);
288+
}
289+
290+
// Instantiate each iteration, since we can only `invokeMain()` with a
291+
// freshly instantiated module.
292+
const additionalImports = {};
293+
const instantiatedApp = await this.compiledApp.instantiate(additionalImports);
294+
295+
const startTimeSinceEpochSeconds = new Date().getTime() / 1000;
296+
// Reduce workload size for a single iteration.
297+
// The default is 1000 frames, but that takes too long (>2s per iteration).
298+
const framesToDraw = 100;
299+
const initialFramesToSkip = 0;
300+
const dartArgs = [
301+
startTimeSinceEpochSeconds,
302+
framesToDraw.toString(),
303+
initialFramesToSkip.toString()
304+
];
305+
306+
await eventLoop(async () => {
307+
await instantiatedApp.invokeMain(...dartArgs)
308+
});
309+
}
310+
}

Dart/build.log

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Built on 2025-01-28 15:34:10+01:00
2+
Cloning into 'wasm_gc_benchmarks'...
3+
cf32ca4 Recompile all benchmarks
4+
Copying files from wasm_gc_benchmarks/ into build/
5+
Build success

Dart/build.sh

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
#!/bin/bash
2+
3+
set -eo pipefail
4+
5+
# Cleanup old files.
6+
rm -rf wasm_gc_benchmarks/
7+
rm -rf build/
8+
9+
BUILD_LOG="$(realpath build.log)"
10+
echo -e "Built on $(date --rfc-3339=seconds)" | tee "$BUILD_LOG"
11+
12+
git clone https://github.com/mkustermann/wasm_gc_benchmarks |& tee -a "$BUILD_LOG"
13+
pushd wasm_gc_benchmarks/
14+
git log -1 --oneline | tee -a "$BUILD_LOG"
15+
popd
16+
17+
echo "Copying files from wasm_gc_benchmarks/ into build/" | tee -a "$BUILD_LOG"
18+
mkdir -p build/ | tee -a "$BUILD_LOG"
19+
# Generic Dart2wasm runner.
20+
cp wasm_gc_benchmarks/tools/run_wasm.js build/ | tee -a "$BUILD_LOG"
21+
# "Flute Complex" benchmark application.
22+
cp wasm_gc_benchmarks/benchmarks-out/flute.dart2wasm.{mjs,wasm} build/ | tee -a "$BUILD_LOG"
23+
24+
echo "Build success" | tee -a "$BUILD_LOG"
25+
26+
# TODO: We could actually build the application/benchmark from Dart sources with
27+
# the dart2wasm compiler / Dart SDK. See `wasm_gc_benchmarks/compile.sh`

0 commit comments

Comments
 (0)