diff --git a/Dart/.gitignore b/Dart/.gitignore new file mode 100644 index 0000000..47b45a2 --- /dev/null +++ b/Dart/.gitignore @@ -0,0 +1 @@ +/wasm_gc_benchmarks/ diff --git a/Dart/README.md b/Dart/README.md new file mode 100644 index 0000000..6febcf0 --- /dev/null +++ b/Dart/README.md @@ -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 +``` diff --git a/Dart/benchmark.js b/Dart/benchmark.js new file mode 100644 index 0000000..f4953cd --- /dev/null +++ b/Dart/benchmark.js @@ -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) { + "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) + }); + } +} diff --git a/Dart/build.log b/Dart/build.log new file mode 100644 index 0000000..cb94299 --- /dev/null +++ b/Dart/build.log @@ -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 diff --git a/Dart/build.sh b/Dart/build.sh new file mode 100755 index 0000000..c41f8ff --- /dev/null +++ b/Dart/build.sh @@ -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` diff --git a/Dart/build/flute.dart2wasm.mjs b/Dart/build/flute.dart2wasm.mjs new file mode 100644 index 0000000..acf4de7 --- /dev/null +++ b/Dart/build/flute.dart2wasm.mjs @@ -0,0 +1,435 @@ + +// Compiles a dart2wasm-generated main module from `source` which can then +// instantiatable via the `instantiate` method. +// +// `source` needs to be a `Response` object (or promise thereof) e.g. created +// via the `fetch()` JS API. +export async function compileStreaming(source) { + const builtins = {builtins: ['js-string']}; + return new CompiledApp( + await WebAssembly.compileStreaming(source, builtins), builtins); +} + +// Compiles a dart2wasm-generated wasm modules from `bytes` which is then +// instantiatable via the `instantiate` method. +export async function compile(bytes) { + const builtins = {builtins: ['js-string']}; + return new CompiledApp(await WebAssembly.compile(bytes, builtins), builtins); +} + +// DEPRECATED: Please use `compile` or `compileStreaming` to get a compiled app, +// use `instantiate` method to get an instantiated app and then call +// `invokeMain` to invoke the main function. +export async function instantiate(modulePromise, importObjectPromise) { + var moduleOrCompiledApp = await modulePromise; + if (!(moduleOrCompiledApp instanceof CompiledApp)) { + moduleOrCompiledApp = new CompiledApp(moduleOrCompiledApp); + } + const instantiatedApp = await moduleOrCompiledApp.instantiate(await importObjectPromise); + return instantiatedApp.instantiatedModule; +} + +// DEPRECATED: Please use `compile` or `compileStreaming` to get a compiled app, +// use `instantiate` method to get an instantiated app and then call +// `invokeMain` to invoke the main function. +export const invoke = (moduleInstance, ...args) => { + moduleInstance.exports.$invokeMain(args); +} + +class CompiledApp { + constructor(module, builtins) { + this.module = module; + this.builtins = builtins; + } + + // The second argument is an options object containing: + // `loadDeferredWasm` is a JS function that takes a module name matching a + // wasm file produced by the dart2wasm compiler and returns the bytes to + // load the module. These bytes can be in either a format supported by + // `WebAssembly.compile` or `WebAssembly.compileStreaming`. + async instantiate(additionalImports, {loadDeferredWasm} = {}) { + let dartInstance; + + // Prints to the console + function printToConsole(value) { + if (typeof dartPrint == "function") { + dartPrint(value); + return; + } + if (typeof console == "object" && typeof console.log != "undefined") { + console.log(value); + return; + } + if (typeof print == "function") { + print(value); + return; + } + + throw "Unable to print message: " + js; + } + + // Converts a Dart List to a JS array. Any Dart objects will be converted, but + // this will be cheap for JSValues. + function arrayFromDartList(constructor, list) { + const exports = dartInstance.exports; + const read = exports.$listRead; + const length = exports.$listLength(list); + const array = new constructor(length); + for (let i = 0; i < length; i++) { + array[i] = read(list, i); + } + return array; + } + + // A special symbol attached to functions that wrap Dart functions. + const jsWrappedDartFunctionSymbol = Symbol("JSWrappedDartFunction"); + + function finalizeWrapper(dartFunction, wrapped) { + wrapped.dartFunction = dartFunction; + wrapped[jsWrappedDartFunctionSymbol] = true; + return wrapped; + } + + // Imports + const dart2wasm = { + + _13: x0 => x0.length, + _15: (x0,x1) => x0[x1], + _19: (x0,x1,x2) => new DataView(x0,x1,x2), + _21: x0 => new Int8Array(x0), + _22: (x0,x1,x2) => new Uint8Array(x0,x1,x2), + _23: x0 => new Uint8Array(x0), + _31: x0 => new Int32Array(x0), + _35: x0 => new Float32Array(x0), + _37: x0 => new Float64Array(x0), + _39: (o, c) => o instanceof c, + _42: (o,s,v) => o[s] = v, + _69: () => Symbol("jsBoxedDartObjectProperty"), + _70: (decoder, codeUnits) => decoder.decode(codeUnits), + _71: () => new TextDecoder("utf-8", {fatal: true}), + _72: () => new TextDecoder("utf-8", {fatal: false}), + _80: Date.now, + _82: s => new Date(s * 1000).getTimezoneOffset() * 60, + _83: s => { + if (!/^\s*[+-]?(?:Infinity|NaN|(?:\.\d+|\d+(?:\.\d*)?)(?:[eE][+-]?\d+)?)\s*$/.test(s)) { + return NaN; + } + return parseFloat(s); + }, + _84: () => { + let stackString = new Error().stack.toString(); + let frames = stackString.split('\n'); + let drop = 2; + if (frames[0] === 'Error') { + drop += 1; + } + return frames.slice(drop).join('\n'); + }, + _85: () => typeof dartUseDateNowForTicks !== "undefined", + _86: () => 1000 * performance.now(), + _87: () => Date.now(), + _90: () => new WeakMap(), + _91: (map, o) => map.get(o), + _92: (map, o, v) => map.set(o, v), + _105: s => JSON.stringify(s), + _106: s => printToConsole(s), + _107: a => a.join(''), + _110: (s, t) => s.split(t), + _111: s => s.toLowerCase(), + _112: s => s.toUpperCase(), + _113: s => s.trim(), + _114: s => s.trimLeft(), + _115: s => s.trimRight(), + _117: (s, p, i) => s.indexOf(p, i), + _118: (s, p, i) => s.lastIndexOf(p, i), + _120: Object.is, + _121: s => s.toUpperCase(), + _122: s => s.toLowerCase(), + _123: (a, i) => a.push(i), + _127: a => a.pop(), + _128: (a, i) => a.splice(i, 1), + _130: (a, s) => a.join(s), + _131: (a, s, e) => a.slice(s, e), + _134: a => a.length, + _136: (a, i) => a[i], + _137: (a, i, v) => a[i] = v, + _139: (o, offsetInBytes, lengthInBytes) => { + var dst = new ArrayBuffer(lengthInBytes); + new Uint8Array(dst).set(new Uint8Array(o, offsetInBytes, lengthInBytes)); + return new DataView(dst); + }, + _140: (o, start, length) => new Uint8Array(o.buffer, o.byteOffset + start, length), + _141: (o, start, length) => new Int8Array(o.buffer, o.byteOffset + start, length), + _142: (o, start, length) => new Uint8ClampedArray(o.buffer, o.byteOffset + start, length), + _143: (o, start, length) => new Uint16Array(o.buffer, o.byteOffset + start, length), + _144: (o, start, length) => new Int16Array(o.buffer, o.byteOffset + start, length), + _145: (o, start, length) => new Uint32Array(o.buffer, o.byteOffset + start, length), + _146: (o, start, length) => new Int32Array(o.buffer, o.byteOffset + start, length), + _148: (o, start, length) => new BigInt64Array(o.buffer, o.byteOffset + start, length), + _149: (o, start, length) => new Float32Array(o.buffer, o.byteOffset + start, length), + _150: (o, start, length) => new Float64Array(o.buffer, o.byteOffset + start, length), + _151: (t, s) => t.set(s), + _153: (o) => new DataView(o.buffer, o.byteOffset, o.byteLength), + _155: o => o.buffer, + _156: o => o.byteOffset, + _157: Function.prototype.call.bind(Object.getOwnPropertyDescriptor(DataView.prototype, 'byteLength').get), + _158: (b, o) => new DataView(b, o), + _159: (b, o, l) => new DataView(b, o, l), + _160: Function.prototype.call.bind(DataView.prototype.getUint8), + _161: Function.prototype.call.bind(DataView.prototype.setUint8), + _162: Function.prototype.call.bind(DataView.prototype.getInt8), + _163: Function.prototype.call.bind(DataView.prototype.setInt8), + _164: Function.prototype.call.bind(DataView.prototype.getUint16), + _165: Function.prototype.call.bind(DataView.prototype.setUint16), + _166: Function.prototype.call.bind(DataView.prototype.getInt16), + _167: Function.prototype.call.bind(DataView.prototype.setInt16), + _168: Function.prototype.call.bind(DataView.prototype.getUint32), + _169: Function.prototype.call.bind(DataView.prototype.setUint32), + _170: Function.prototype.call.bind(DataView.prototype.getInt32), + _171: Function.prototype.call.bind(DataView.prototype.setInt32), + _174: Function.prototype.call.bind(DataView.prototype.getBigInt64), + _175: Function.prototype.call.bind(DataView.prototype.setBigInt64), + _176: Function.prototype.call.bind(DataView.prototype.getFloat32), + _177: Function.prototype.call.bind(DataView.prototype.setFloat32), + _178: Function.prototype.call.bind(DataView.prototype.getFloat64), + _179: Function.prototype.call.bind(DataView.prototype.setFloat64), + _181: () => globalThis.performance, + _182: () => globalThis.JSON, + _183: x0 => x0.measure, + _184: x0 => x0.mark, + _185: x0 => x0.clearMeasures, + _186: x0 => x0.clearMarks, + _187: (x0,x1,x2,x3) => x0.measure(x1,x2,x3), + _188: (x0,x1,x2) => x0.mark(x1,x2), + _189: x0 => x0.clearMeasures(), + _190: x0 => x0.clearMarks(), + _191: (x0,x1) => x0.parse(x1), + _197: (ms, c) => + setTimeout(() => dartInstance.exports.$invokeCallback(c),ms), + _198: (handle) => clearTimeout(handle), + _201: (c) => + queueMicrotask(() => dartInstance.exports.$invokeCallback(c)), + _233: (x0,x1) => x0.matchMedia(x1), + _234: (s, m) => { + try { + return new RegExp(s, m); + } catch (e) { + return String(e); + } + }, + _235: (x0,x1) => x0.exec(x1), + _236: (x0,x1) => x0.test(x1), + _237: (x0,x1) => x0.exec(x1), + _238: (x0,x1) => x0.exec(x1), + _239: x0 => x0.pop(), + _241: o => o === undefined, + _260: o => typeof o === 'function' && o[jsWrappedDartFunctionSymbol] === true, + _263: o => o instanceof RegExp, + _264: (l, r) => l === r, + _265: o => o, + _266: o => o, + _267: o => o, + _268: b => !!b, + _269: o => o.length, + _272: (o, i) => o[i], + _273: f => f.dartFunction, + _274: l => arrayFromDartList(Int8Array, l), + _275: l => arrayFromDartList(Uint8Array, l), + _276: l => arrayFromDartList(Uint8ClampedArray, l), + _277: l => arrayFromDartList(Int16Array, l), + _278: l => arrayFromDartList(Uint16Array, l), + _279: l => arrayFromDartList(Int32Array, l), + _280: l => arrayFromDartList(Uint32Array, l), + _281: l => arrayFromDartList(Float32Array, l), + _282: l => arrayFromDartList(Float64Array, l), + _283: x0 => new ArrayBuffer(x0), + _284: (data, length) => { + const getValue = dartInstance.exports.$byteDataGetUint8; + const view = new DataView(new ArrayBuffer(length)); + for (let i = 0; i < length; i++) { + view.setUint8(i, getValue(data, i)); + } + return view; + }, + _285: l => arrayFromDartList(Array, l), + _286: () => ({}), + _288: l => new Array(l), + _289: () => globalThis, + _292: (o, p) => o[p], + _296: o => String(o), + _298: o => { + if (o === undefined) return 1; + var type = typeof o; + if (type === 'boolean') return 2; + if (type === 'number') return 3; + if (type === 'string') return 4; + if (o instanceof Array) return 5; + if (ArrayBuffer.isView(o)) { + if (o instanceof Int8Array) return 6; + if (o instanceof Uint8Array) return 7; + if (o instanceof Uint8ClampedArray) return 8; + if (o instanceof Int16Array) return 9; + if (o instanceof Uint16Array) return 10; + if (o instanceof Int32Array) return 11; + if (o instanceof Uint32Array) return 12; + if (o instanceof Float32Array) return 13; + if (o instanceof Float64Array) return 14; + if (o instanceof DataView) return 15; + } + if (o instanceof ArrayBuffer) return 16; + return 17; + }, + _299: (jsArray, jsArrayOffset, wasmArray, wasmArrayOffset, length) => { + const getValue = dartInstance.exports.$wasmI8ArrayGet; + for (let i = 0; i < length; i++) { + jsArray[jsArrayOffset + i] = getValue(wasmArray, wasmArrayOffset + i); + } + }, + _300: (jsArray, jsArrayOffset, wasmArray, wasmArrayOffset, length) => { + const setValue = dartInstance.exports.$wasmI8ArraySet; + for (let i = 0; i < length; i++) { + setValue(wasmArray, wasmArrayOffset + i, jsArray[jsArrayOffset + i]); + } + }, + _303: (jsArray, jsArrayOffset, wasmArray, wasmArrayOffset, length) => { + const getValue = dartInstance.exports.$wasmI32ArrayGet; + for (let i = 0; i < length; i++) { + jsArray[jsArrayOffset + i] = getValue(wasmArray, wasmArrayOffset + i); + } + }, + _304: (jsArray, jsArrayOffset, wasmArray, wasmArrayOffset, length) => { + const setValue = dartInstance.exports.$wasmI32ArraySet; + for (let i = 0; i < length; i++) { + setValue(wasmArray, wasmArrayOffset + i, jsArray[jsArrayOffset + i]); + } + }, + _305: (jsArray, jsArrayOffset, wasmArray, wasmArrayOffset, length) => { + const getValue = dartInstance.exports.$wasmF32ArrayGet; + for (let i = 0; i < length; i++) { + jsArray[jsArrayOffset + i] = getValue(wasmArray, wasmArrayOffset + i); + } + }, + _306: (jsArray, jsArrayOffset, wasmArray, wasmArrayOffset, length) => { + const setValue = dartInstance.exports.$wasmF32ArraySet; + for (let i = 0; i < length; i++) { + setValue(wasmArray, wasmArrayOffset + i, jsArray[jsArrayOffset + i]); + } + }, + _307: (jsArray, jsArrayOffset, wasmArray, wasmArrayOffset, length) => { + const getValue = dartInstance.exports.$wasmF64ArrayGet; + for (let i = 0; i < length; i++) { + jsArray[jsArrayOffset + i] = getValue(wasmArray, wasmArrayOffset + i); + } + }, + _308: (jsArray, jsArrayOffset, wasmArray, wasmArrayOffset, length) => { + const setValue = dartInstance.exports.$wasmF64ArraySet; + for (let i = 0; i < length; i++) { + setValue(wasmArray, wasmArrayOffset + i, jsArray[jsArrayOffset + i]); + } + }, + _312: x0 => x0.index, + _315: (x0,x1) => x0.exec(x1), + _317: x0 => x0.flags, + _318: x0 => x0.multiline, + _319: x0 => x0.ignoreCase, + _320: x0 => x0.unicode, + _321: x0 => x0.dotAll, + _322: (x0,x1) => x0.lastIndex = x1, + _324: (o, p) => o[p], + _327: x0 => x0.random(), + _328: x0 => x0.random(), + _332: () => globalThis.Math, + _334: Function.prototype.call.bind(Number.prototype.toString), + _335: (d, digits) => d.toFixed(digits), + _2137: () => globalThis.window, + _8959: x0 => x0.matches, + _12979: x0 => globalThis.window.flutterCanvasKit = x0, + + }; + + const baseImports = { + dart2wasm: dart2wasm, + + + Math: Math, + Date: Date, + Object: Object, + Array: Array, + Reflect: Reflect, + }; + + const jsStringPolyfill = { + "charCodeAt": (s, i) => s.charCodeAt(i), + "compare": (s1, s2) => { + if (s1 < s2) return -1; + if (s1 > s2) return 1; + return 0; + }, + "concat": (s1, s2) => s1 + s2, + "equals": (s1, s2) => s1 === s2, + "fromCharCode": (i) => String.fromCharCode(i), + "length": (s) => s.length, + "substring": (s, a, b) => s.substring(a, b), + "fromCharCodeArray": (a, start, end) => { + if (end <= start) return ''; + + const read = dartInstance.exports.$wasmI16ArrayGet; + let result = ''; + let index = start; + const chunkLength = Math.min(end - index, 500); + let array = new Array(chunkLength); + while (index < end) { + const newChunkLength = Math.min(end - index, 500); + for (let i = 0; i < newChunkLength; i++) { + array[i] = read(a, index++); + } + if (newChunkLength < chunkLength) { + array = array.slice(0, newChunkLength); + } + result += String.fromCharCode(...array); + } + return result; + }, + }; + + const deferredLibraryHelper = { + "loadModule": async (moduleName) => { + if (!loadDeferredWasm) { + throw "No implementation of loadDeferredWasm provided."; + } + const source = await Promise.resolve(loadDeferredWasm(moduleName)); + const module = await ((source instanceof Response) + ? WebAssembly.compileStreaming(source, this.builtins) + : WebAssembly.compile(source, this.builtins)); + return await WebAssembly.instantiate(module, { + ...baseImports, + ...additionalImports, + "wasm:js-string": jsStringPolyfill, + "module0": dartInstance.exports, + }); + }, + }; + + dartInstance = await WebAssembly.instantiate(this.module, { + ...baseImports, + ...additionalImports, + "deferredLibraryHelper": deferredLibraryHelper, + "wasm:js-string": jsStringPolyfill, + }); + + return new InstantiatedApp(this, dartInstance); + } +} + +class InstantiatedApp { + constructor(compiledApp, instantiatedModule) { + this.compiledApp = compiledApp; + this.instantiatedModule = instantiatedModule; + } + + // Call the main function with the given arguments. + invokeMain(...args) { + this.instantiatedModule.exports.$invokeMain(args); + } +} + diff --git a/Dart/build/flute.dart2wasm.wasm b/Dart/build/flute.dart2wasm.wasm new file mode 100644 index 0000000..cb3ce6d Binary files /dev/null and b/Dart/build/flute.dart2wasm.wasm differ diff --git a/Dart/build/run_wasm.js b/Dart/build/run_wasm.js new file mode 100644 index 0000000..8b2eb93 --- /dev/null +++ b/Dart/build/run_wasm.js @@ -0,0 +1,424 @@ +// Copyright (c) 2024, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. +// +// Runner V8/JSShell script for testing dart2wasm, takes ".wasm" files as +// arguments. +// +// Run as follows on D8: +// +// $> d8 run_wasm.js \ +// -- /abs/path/to/.mjs .wasm [.wasm] \ +// [-- Dart commandline arguments...] +// +// Run as follows on JSC: +// +// $> jsc run_wasm.js -- .mjs .wasm [.wasm] \ +// [-- Dart commandline arguments...] +// +// Run as follows on JSShell: +// +// $> js run_wasm.js \ +// /abs/path/to/.mjs .wasm [.wasm] \ +// [-- Dart commandline arguments...] +// +// (Notice the missing -- here!) +// +// Please note we require an absolute path for the JS runtime. This is a +// workaround for a discrepancy in D8. Specifically, `import`(used to load .mjs +// files) searches for imports relative to run_wasm.js, but `readbuffer`(used to +// read in .wasm files) searches relative to CWD. A path relative to +// `run_wasm.js` will also work. +// +// Or with the `run_dart2wasm_d8` helper script: +// +// $> sdk/bin/run_dart2wasm_d8 .wasm [.wasm] +// +// If an FFI module is specified, it will be instantiated first, and its +// exports will be supplied as imports to the Dart module under the 'ffi' +// module name. +const jsRuntimeArg = 0; +const wasmArg = 1; +const ffiArg = 2; + +// This script is intended to be used by D8, JSShell or JSC. We distinguish +// them by the functions they offer to read files: +// +// Engine | Shell | FileRead | Arguments +// -------------------------------------------------------------- +// V8 | D8 | readbuffer | arguments (arg0 arg1) +// JavaScriptCore | JSC | readFile | arguments (arg0 arg1) +// SpiderMonkey | JSShell | readRelativeToScript | scriptArgs (-- arg0 arg1) +// +const isD8 = (typeof readbuffer === "function"); +const isJSC = (typeof readFile === "function"); +const isJSShell = (typeof readRelativeToScript === "function"); + +if (isD8) { + // D8's performance.measure is API incompatible with the browser version. + // + // (see also dart2js's `sdk/**/js_runtime/lib/preambles/d8.js`) + delete performance.measure; +} + +function readFileContentsAsBytes(filename) { + var buffer; + if (isJSC) { + buffer = readFile(filename, "binary"); + } else if (isD8) { + buffer = readbuffer(filename); + } else { + buffer = readRelativeToScript(filename, "binary"); + } + return new Uint8Array(buffer, 0, buffer.byteLength); +} + +var args = (isD8 || isJSC) ? arguments : scriptArgs; +var dartArgs = []; +const argsSplit = args.indexOf("--"); +if (argsSplit != -1) { + dartArgs = args.slice(argsSplit + 1); + args = args.slice(0, argsSplit); +} + +// d8's `setTimeout` doesn't work as expected (it doesn't wait before calling +// the callback), and d8 also doesn't have `setInterval` and `queueMicrotask`. +// So we define our own event loop with these functions. +// +// The code below is copied form dart2js, with some modifications: +// sdk/lib/_internal/js_runtime/lib/preambles/d8.js +(function(self, scriptArguments) { + // Using strict mode to avoid accidentally defining global variables. + "use strict"; // Should be first statement of this function. + + // 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 addInterval(f, ms) { + ms = Math.max(0, ms); + var id = timerIdCounter++; + function repeat() { + // Reactivate with the same id. + repeat.$timerId = id; + timerIds[id] = repeat; + addDelayedTimer(repeat, ms); + f(); + } + repeat.$timerId = id; + timerIds[id] = repeat; + addDelayedTimer(repeat, ms); + return id; + } + + 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) { + // JSC doesn't report/print uncaught async exceptions for some reason. + if (isJSC) { + print('Error: ' + e); + print('Stack: ' + e.stack); + } + if (typeof onerror == "function") { + onerror(e, null, -1); + } else { + throw e; + } + } + action = nextEvent(); + } + } + + // Global properties. "self" refers to the global object, so adding a + // property to "self" defines a global variable. + self.self = self; + self.dartMainRunner = function(main, ignored_args) { + // Initialize. + var action = async function() { await main(scriptArguments, null); } + eventLoop(action); + }; + self.setTimeout = addTimer; + self.clearTimeout = cancelTimer; + self.setInterval = addInterval; + self.clearInterval = cancelTimer; + self.queueMicrotask = addTask; + self.readFileContentsAsBytes = readFileContentsAsBytes; + + self.location = {} + self.location.href = 'file://' + args[wasmArg]; + + // Signals `Stopwatch._initTicker` to use `Date.now` to get ticks instead of + // `performance.now`, as it's not available in d8. + self.dartUseDateNowForTicks = true; +})(this, []); + +// We would like this itself to be a ES module rather than a script, but +// unfortunately d8 does not return a failed error code if an unhandled +// exception occurs asynchronously in an ES module. +const main = async () => { + const dart2wasm = await import(args[jsRuntimeArg]); + + /// Returns whether the `js-string` built-in is supported. + function detectImportedStrings() { + let bytes = [ + 0, 97, 115, 109, 1, 0, 0, 0, 1, 4, 1, 96, 0, + 0, 2, 23, 1, 14, 119, 97, 115, 109, 58, 106, 115, 45, + 115, 116, 114, 105, 110, 103, 4, 99, 97, 115, 116, 0, 0 + ]; + return !WebAssembly.validate( + new Uint8Array(bytes), {builtins: ['js-string']}); + } + + function compile(filename, withJsStringBuiltins) { + // Create a Wasm module from the binary Wasm file. + return WebAssembly.compile( + readFileContentsAsBytes(filename), + withJsStringBuiltins ? {builtins: ['js-string']} : {} + ); + } + + globalThis.window ??= globalThis; + + let importObject = {}; + + // Is an FFI module specified? + if (args.length > 2) { + // Instantiate FFI module. + var ffiInstance = await WebAssembly.instantiate(await compile(args[ffiArg], false), {}); + // Make its exports available as imports under the 'ffi' module name. + importObject.ffi = ffiInstance.exports; + } + + // Instantiate the Dart module, importing from the global scope. + var dartInstance = await dart2wasm.instantiate( + compile(args[wasmArg], detectImportedStrings()), + Promise.resolve(importObject), + ); + + // Call `main`. If tasks are placed into the event loop (by scheduling tasks + // explicitly or awaiting Futures), these will automatically keep the script + // alive even after `main` returns. + await dart2wasm.invoke(dartInstance, ...dartArgs); +}; + +dartMainRunner(main, []); diff --git a/JetStreamDriver.js b/JetStreamDriver.js index e2ca209..33e5ec3 100644 --- a/JetStreamDriver.js +++ b/JetStreamDriver.js @@ -1063,6 +1063,8 @@ class AsyncBenchmark extends DefaultBenchmark { return ` async function doRun() { let __benchmark = new Benchmark(); + if (__benchmark.init) + await __benchmark.init(); let results = []; let benchmarkName = "${this.name}"; @@ -1129,6 +1131,7 @@ class WasmEMCCBenchmark extends AsyncBenchmark { return str; } + // FIXME: Why is this part of the runnerCode and not prerunCode? get runnerCode() { let str = `function loadBlob(key, path, andThen) {`; @@ -2014,6 +2017,18 @@ const BENCHMARKS = [ worstCaseCount: 2, testGroup: WasmGroup }), + new WasmEMCCBenchmark({ + name: "Dart-flute-wasm", + files: [ + "./Dart/benchmark.js", + ], + preload: { + wasmBinary: "./Dart/build/flute.dart2wasm.wasm" + }, + iterations: 15, + worstCaseCount: 2, + testGroup: WasmGroup, + }), new WasmLegacyBenchmark({ name: "tfjs-wasm", files: [ diff --git a/wasm-cli.js b/wasm-cli.js index 207a903..82b6a5e 100644 --- a/wasm-cli.js +++ b/wasm-cli.js @@ -35,6 +35,7 @@ testList = [ "argon2-wasm", "argon2-wasm-simd", "8bitbench-wasm", + "Dart-flute-wasm", "zlib-wasm", ];