Skip to content

Add Dart flute WasmGC workload #33

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 4 commits into from
Feb 26, 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
1 change: 1 addition & 0 deletions Dart/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/wasm_gc_benchmarks/
31 changes: 31 additions & 0 deletions Dart/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Dart flute benchmark application

This is a WasmGC build of the `flute` benchmark, a Dart application using the
`Flutter` UI framework.
It performs the Wasm/compute-only part of rendering a certain number of frames.
Since JetStream runs in JS engine shells, the actual rendering to a Canvas is
stubbed out.
The upstream repository containing pre-built WebAssembly binaries (which we
use here) is at https://github.com/mkustermann/wasm_gc_benchmarks.
The Dart source code of the flute application is at https://github.com/dart-lang/flute.

## Build Instructions

See `build.sh` or just run it.
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.
In short, the main runner is `build/run_wasm.js`, which takes as arguments a
application-specific generated JS and Wasm file, and the arguments passed to
the Dart main method.
Since different engines / shells resolve JS modules and parse command-line
arguments differently, the invocations are something like (from this directory):

```
path/to/d8 build/run_wasm.js -- flute.dart2wasm.mjs build/flute.dart2wasm.wasm -- $(date +%s.%N) 1000
path/to/spidermonkey/js build/run_wasm.js build/flute.dart2wasm.mjs flute.dart2wasm.wasm -- $(date +%s.%N) 1000
path/to/jsc build/run_wasm.js -- ./flute.dart2wasm.mjs build/flute.dart2wasm.wasm -- $(date +%s.%N) 1000
```
310 changes: 310 additions & 0 deletions Dart/benchmark.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,310 @@
// Copyright 2024 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 `build/run_wasm.js` to add own task queue implementation, since
// `setTimeout` and `queueMicrotask` are not always available in shells.
function addTaskQueue(self) {
Comment on lines +5 to +7
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wouldn't be opposed to adding a queueMicrotask to JSC shell. jsc/v8 already have setTimout (although the delay time is ignored for jsc not sure about v8). AFAIK, all the shell runners have a microtask queue anyway. I assume those queues are mostly equivalent to the browser one too. Is this something V8 would be willing to do? @eqrion for thoughts on adding these to sm?

All that said, I'm also happy to leave this as no browser seems to show any samples in this code. If we want to change this let's do this as a follow up.

"use strict";

// Task queue as cyclic list queue.
var taskQueue = new Array(8); // Length is power of 2.
var head = 0;
var tail = 0;
var mask = taskQueue.length - 1;

function addTask(elem) {
taskQueue[head] = elem;
head = (head + 1) & mask;
if (head == tail) _growTaskQueue();
}

function removeTask() {
if (head == tail) return;
var result = taskQueue[tail];
taskQueue[tail] = undefined;
tail = (tail + 1) & mask;
return result;
}

function _growTaskQueue() {
// head == tail.
var length = taskQueue.length;
var split = head;
taskQueue.length = length * 2;
if (split * 2 < length) { // split < length / 2
for (var i = 0; i < split; i++) {
taskQueue[length + i] = taskQueue[i];
taskQueue[i] = undefined;
}
head += length;
} else {
for (var i = split; i < length; i++) {
taskQueue[length + i] = taskQueue[i];
taskQueue[i] = undefined;
}
tail += length;
}
mask = taskQueue.length - 1;
}

// Mapping from timer id to timer function.
// The timer id is written on the function as .$timerId.
// That field is cleared when the timer is cancelled, but it is not returned
// from the queue until its time comes.
var timerIds = {};
var timerIdCounter = 1; // Counter used to assign ids.

// Zero-timer queue as simple array queue using push/shift.
var zeroTimerQueue = [];

function addTimer(f, ms) {
ms = Math.max(0, ms);
var id = timerIdCounter++;
// A callback can be scheduled at most once.
// (console.assert is only available on D8)
// if (isD8) console.assert(f.$timerId === undefined);
f.$timerId = id;
timerIds[id] = f;
if (ms == 0 && !isNextTimerDue()) {
zeroTimerQueue.push(f);
} else {
addDelayedTimer(f, ms);
}
return id;
}

function nextZeroTimer() {
while (zeroTimerQueue.length > 0) {
var action = zeroTimerQueue.shift();
if (action.$timerId !== undefined) return action;
}
}

function nextEvent() {
var action = removeTask();
if (action) {
return action;
}
do {
action = nextZeroTimer();
if (action) break;
var nextList = nextDelayedTimerQueue();
if (!nextList) {
return;
}
var newTime = nextList.shift();
advanceTimeTo(newTime);
zeroTimerQueue = nextList;
} while (true)
var id = action.$timerId;
clearTimerId(action, id);
return action;
}

// Mocking time.
var timeOffset = 0;
var now = function() {
// Install the mock Date object only once.
// Following calls to "now" will just use the new (mocked) Date.now
// method directly.
installMockDate();
now = Date.now;
return Date.now();
};
var originalDate = Date;
var originalNow = originalDate.now;

function advanceTimeTo(time) {
var now = originalNow();
if (timeOffset < time - now) {
timeOffset = time - now;
}
}

function installMockDate() {
var NewDate = function Date(Y, M, D, h, m, s, ms) {
if (this instanceof Date) {
// Assume a construct call.
switch (arguments.length) {
case 0: return new originalDate(originalNow() + timeOffset);
case 1: return new originalDate(Y);
case 2: return new originalDate(Y, M);
case 3: return new originalDate(Y, M, D);
case 4: return new originalDate(Y, M, D, h);
case 5: return new originalDate(Y, M, D, h, m);
case 6: return new originalDate(Y, M, D, h, m, s);
default: return new originalDate(Y, M, D, h, m, s, ms);
}
}
return new originalDate(originalNow() + timeOffset).toString();
};
NewDate.UTC = originalDate.UTC;
NewDate.parse = originalDate.parse;
NewDate.now = function now() { return originalNow() + timeOffset; };
NewDate.prototype = originalDate.prototype;
originalDate.prototype.constructor = NewDate;
Date = NewDate;
}

// Heap priority queue with key index.
// Each entry is list of [timeout, callback1 ... callbackn].
var timerHeap = [];
var timerIndex = {};

function addDelayedTimer(f, ms) {
var timeout = now() + ms;
var timerList = timerIndex[timeout];
if (timerList == null) {
timerList = [timeout, f];
timerIndex[timeout] = timerList;
var index = timerHeap.length;
timerHeap.length += 1;
bubbleUp(index, timeout, timerList);
} else {
timerList.push(f);
}
}

function isNextTimerDue() {
if (timerHeap.length == 0) return false;
var head = timerHeap[0];
return head[0] < originalNow() + timeOffset;
}

function nextDelayedTimerQueue() {
if (timerHeap.length == 0) return null;
var result = timerHeap[0];
var last = timerHeap.pop();
if (timerHeap.length > 0) {
bubbleDown(0, last[0], last);
}
return result;
}

function bubbleUp(index, key, value) {
while (index != 0) {
var parentIndex = (index - 1) >> 1;
var parent = timerHeap[parentIndex];
var parentKey = parent[0];
if (key > parentKey) break;
timerHeap[index] = parent;
index = parentIndex;
}
timerHeap[index] = value;
}

function bubbleDown(index, key, value) {
while (true) {
var leftChildIndex = index * 2 + 1;
if (leftChildIndex >= timerHeap.length) break;
var minChildIndex = leftChildIndex;
var minChild = timerHeap[leftChildIndex];
var minChildKey = minChild[0];
var rightChildIndex = leftChildIndex + 1;
if (rightChildIndex < timerHeap.length) {
var rightChild = timerHeap[rightChildIndex];
var rightKey = rightChild[0];
if (rightKey < minChildKey) {
minChildIndex = rightChildIndex;
minChild = rightChild;
minChildKey = rightKey;
}
}
if (minChildKey > key) break;
timerHeap[index] = minChild;
index = minChildIndex;
}
timerHeap[index] = value;
}

function cancelTimer(id) {
var f = timerIds[id];
if (f == null) return;
clearTimerId(f, id);
}

function clearTimerId(f, id) {
f.$timerId = undefined;
delete timerIds[id];
}

async function eventLoop(action) {
while (action) {
try {
await action();
} catch (e) {
if (typeof onerror == "function") {
onerror(e, null, -1);
} else {
throw e;
}
}
action = nextEvent();
}
}

self.setTimeout = addTimer;
self.clearTimeout = cancelTimer;
self.queueMicrotask = addTask;
self.eventLoop = eventLoop;
}

function dartPrint(...args) { print(args); }
addTaskQueue(globalThis);
globalThis.window ??= globalThis;

class Benchmark {
dart2wasmJsModule;
compiledApp;

async init() {
// The generated JavaScript code from dart2wasm is an ES module, which we
// can only load with a dynamic import (since this file is not a module.)
// TODO: Support ES6 modules in the driver instead of this one-off solution.
// This probably requires a new `Benchmark` field called `modules` that
// is a map from module variable name (which will hold the resulting module
// namespace object) to relative module URL, which is resolved in the
// `preRunnerCode`, similar to this code here.
if (isInBrowser) {
// In browsers, relative imports don't work since we are not in a module.
// (`import.meta.url` is not defined.)
this.dart2wasmJsModule = await import(location.origin + "/Dart/build/flute.dart2wasm.mjs");
} else {
// In shells, relative imports require different paths, so try with and
// without the "./" prefix (e.g., JSC requires it).
try {
this.dart2wasmJsModule = await import("Dart/build/flute.dart2wasm.mjs");
} catch {
this.dart2wasmJsModule = await import("./Dart/build/flute.dart2wasm.mjs");
}
}
}

async runIteration() {
// Compile once in the first iteration.
if (!this.compiledApp) {
this.compiledApp = await this.dart2wasmJsModule.compile(Module.wasmBinary);
}

// Instantiate each iteration, since we can only `invokeMain()` with a
// freshly instantiated module.
const additionalImports = {};
const instantiatedApp = await this.compiledApp.instantiate(additionalImports);

const startTimeSinceEpochSeconds = new Date().getTime() / 1000;
// Reduce workload size for a single iteration.
// The default is 1000 frames, but that takes too long (>2s per iteration).
const framesToDraw = 100;
const initialFramesToSkip = 0;
const dartArgs = [
startTimeSinceEpochSeconds,
framesToDraw.toString(),
initialFramesToSkip.toString()
];

await eventLoop(async () => {
await instantiatedApp.invokeMain(...dartArgs)
});
}
}
5 changes: 5 additions & 0 deletions Dart/build.log
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Built on 2025-01-28 15:34:10+01:00
Cloning into 'wasm_gc_benchmarks'...
cf32ca4 Recompile all benchmarks
Copying files from wasm_gc_benchmarks/ into build/
Build success
27 changes: 27 additions & 0 deletions Dart/build.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
#!/bin/bash

set -eo pipefail

# Cleanup old files.
rm -rf wasm_gc_benchmarks/
rm -rf build/

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

git clone https://github.com/mkustermann/wasm_gc_benchmarks |& tee -a "$BUILD_LOG"
pushd wasm_gc_benchmarks/
git log -1 --oneline | tee -a "$BUILD_LOG"
popd

echo "Copying files from wasm_gc_benchmarks/ into build/" | tee -a "$BUILD_LOG"
mkdir -p build/ | tee -a "$BUILD_LOG"
# Generic Dart2wasm runner.
cp wasm_gc_benchmarks/tools/run_wasm.js build/ | tee -a "$BUILD_LOG"
# "Flute Complex" benchmark application.
cp wasm_gc_benchmarks/benchmarks-out/flute.dart2wasm.{mjs,wasm} build/ | tee -a "$BUILD_LOG"

echo "Build success" | tee -a "$BUILD_LOG"

# TODO: We could actually build the application/benchmark from Dart sources with
# the dart2wasm compiler / Dart SDK. See `wasm_gc_benchmarks/compile.sh`
Loading