|
| 1 | +/** |
| 2 | + * This script is served by `harness.mjs` and runs in the browser. |
| 3 | + */ |
| 4 | +import { WASI, File, OpenFile, ConsoleStdout, PreopenDirectory } from 'https://cdn.jsdelivr.net/npm/@bjorn3/browser_wasi_shim@0.3.0/+esm' |
| 5 | +import { polyfill } from 'https://cdn.jsdelivr.net/npm/wasm-imports-parser@1.0.4/polyfill.js/+esm'; |
| 6 | + |
| 7 | +/** |
| 8 | + * @param {{ |
| 9 | + * module: WebAssembly.Module, |
| 10 | + * addToImports: (importObject: WebAssembly.Imports) => Promise<void> |
| 11 | + * }} |
| 12 | + */ |
| 13 | +export async function instantiate({ module, addToImports }) { |
| 14 | + const args = ["target.wasm"] |
| 15 | + const env = [] |
| 16 | + const fds = [ |
| 17 | + new OpenFile(new File([])), // stdin |
| 18 | + ConsoleStdout.lineBuffered((stdout) => { |
| 19 | + console.log(stdout); |
| 20 | + }), |
| 21 | + ConsoleStdout.lineBuffered((stderr) => { |
| 22 | + console.error(stderr); |
| 23 | + }), |
| 24 | + new PreopenDirectory("/", new Map()), |
| 25 | + ]; |
| 26 | + const wasi = new WASI(args, env, fds); |
| 27 | + |
| 28 | + const importObject = { |
| 29 | + wasi_snapshot_preview1: wasi.wasiImport, |
| 30 | + }; |
| 31 | + await addToImports(importObject); |
| 32 | + const instance = await WebAssembly.instantiate(module, importObject); |
| 33 | + return { wasi, instance }; |
| 34 | +} |
| 35 | + |
| 36 | +class WorkerInfoView { |
| 37 | + /** |
| 38 | + * The memory layout of a worker info placed in a shared array buffer. |
| 39 | + * All offsets are represented in Int32Array indices. |
| 40 | + */ |
| 41 | + static Layout = { |
| 42 | + STATE: 0, |
| 43 | + TID: 1, |
| 44 | + START_ARG: 2, |
| 45 | + SIZE: 3, |
| 46 | + }; |
| 47 | + |
| 48 | + /** @param {Int32Array} view - The memory view of the worker info */ |
| 49 | + constructor(view) { |
| 50 | + this.view = view; |
| 51 | + } |
| 52 | + |
| 53 | + get state() { |
| 54 | + return this.view[WorkerInfoView.Layout.STATE]; |
| 55 | + } |
| 56 | + |
| 57 | + setStateAndNotify(state) { |
| 58 | + this.view[WorkerInfoView.Layout.STATE] = state; |
| 59 | + Atomics.notify(this.view, WorkerInfoView.Layout.STATE); |
| 60 | + } |
| 61 | + |
| 62 | + async waitWhile(state) { |
| 63 | + return await Atomics.waitAsync(this.view, WorkerInfoView.Layout.STATE, state); |
| 64 | + } |
| 65 | + |
| 66 | + get tid() { return this.view[WorkerInfoView.Layout.TID]; } |
| 67 | + set tid(value) { this.view[WorkerInfoView.Layout.TID] = value; } |
| 68 | + |
| 69 | + get startArg() { return this.view[WorkerInfoView.Layout.START_ARG]; } |
| 70 | + set startArg(value) { this.view[WorkerInfoView.Layout.START_ARG] = value; } |
| 71 | +} |
| 72 | + |
| 73 | +const WorkerState = { |
| 74 | + NOT_STARTED: 0, |
| 75 | + READY: 1, |
| 76 | + STARTED: 2, |
| 77 | + FINISHED: 3, |
| 78 | + ERROR: 4, |
| 79 | +}; |
| 80 | + |
| 81 | +class Threads { |
| 82 | + /** |
| 83 | + * @param {number} poolSize - The number of threads to pool |
| 84 | + * @param {WebAssembly.Module} module - The WebAssembly module to use |
| 85 | + * @param {WebAssembly.Memory} memory - The memory to use |
| 86 | + */ |
| 87 | + static async create(poolSize, module, memory) { |
| 88 | + const workerScript = new Blob([` |
| 89 | + self.onmessage = async (event) => { |
| 90 | + const { selfFilePath } = event.data; |
| 91 | + const { startWorker } = await import(selfFilePath); |
| 92 | + await startWorker(event.data); |
| 93 | + } |
| 94 | + `], { type: 'text/javascript' }); |
| 95 | + const workerScriptURL = URL.createObjectURL(workerScript); |
| 96 | + // Create a new SAB to communicate with the workers |
| 97 | + // Rationale: Some of the tests busy-wait on the main thread, and it |
| 98 | + // makes it impossible to use `postMessage` to communicate with workers |
| 99 | + // during the busy-wait as the event loop is blocked. Instead, we use a |
| 100 | + // shared array buffer and send notifications by `Atomics.notify`. |
| 101 | + const channel = new SharedArrayBuffer(poolSize * WorkerInfoView.Layout.SIZE * Int32Array.BYTES_PER_ELEMENT); |
| 102 | + |
| 103 | + const workers = []; |
| 104 | + for (let workerIndex = 0; workerIndex < poolSize; workerIndex++) { |
| 105 | + const worker = new Worker(workerScriptURL); |
| 106 | + const selfFilePath = import.meta.url; |
| 107 | + worker.postMessage({ selfFilePath, channel, workerIndex, module, memory }); |
| 108 | + workers.push(worker); |
| 109 | + } |
| 110 | + |
| 111 | + // Wait until all workers are ready |
| 112 | + for (let workerIndex = 0; workerIndex < poolSize; workerIndex++) { |
| 113 | + const view = new Int32Array(channel, workerIndex * WorkerInfoView.Layout.SIZE * Int32Array.BYTES_PER_ELEMENT); |
| 114 | + const infoView = new WorkerInfoView(view); |
| 115 | + await (await infoView.waitWhile(WorkerState.NOT_STARTED)).value; |
| 116 | + const state = infoView.state; |
| 117 | + if (state !== WorkerState.READY) { |
| 118 | + throw new Error(`Worker ${workerIndex} is not ready: ${state}`); |
| 119 | + } |
| 120 | + } |
| 121 | + |
| 122 | + return new Threads(poolSize, workers, channel); |
| 123 | + } |
| 124 | + |
| 125 | + constructor(poolSize, workers, channel) { |
| 126 | + this.poolSize = poolSize; |
| 127 | + this.workers = workers; |
| 128 | + this.nextTid = 1; |
| 129 | + this.channel = channel; |
| 130 | + } |
| 131 | + |
| 132 | + findAvailableWorker() { |
| 133 | + for (let i = 0; i < this.workers.length; i++) { |
| 134 | + const view = new Int32Array(this.channel, i * WorkerInfoView.Layout.SIZE * Int32Array.BYTES_PER_ELEMENT); |
| 135 | + const infoView = new WorkerInfoView(view); |
| 136 | + const state = infoView.state; |
| 137 | + if (state === WorkerState.READY) { |
| 138 | + return i; |
| 139 | + } |
| 140 | + } |
| 141 | + throw new Error("No available worker"); |
| 142 | + } |
| 143 | + |
| 144 | + spawnThread(startArg) { |
| 145 | + const tid = this.nextTid++; |
| 146 | + const index = this.findAvailableWorker(); |
| 147 | + const view = new Int32Array(this.channel, index * WorkerInfoView.Layout.SIZE * Int32Array.BYTES_PER_ELEMENT); |
| 148 | + const infoView = new WorkerInfoView(view); |
| 149 | + infoView.tid = tid; |
| 150 | + infoView.startArg = startArg; |
| 151 | + infoView.setStateAndNotify(WorkerState.STARTED); |
| 152 | + return tid; |
| 153 | + } |
| 154 | +} |
| 155 | + |
| 156 | +export async function runWasmTest(wasmPath) { |
| 157 | + const response = await fetch(wasmPath); |
| 158 | + const wasmBytes = await response.arrayBuffer(); |
| 159 | + |
| 160 | + // Polyfill WebAssembly if "Type Reflection JS API" is unavailable. |
| 161 | + // The feature is required to know the imported memory type. |
| 162 | + const WebAssembly = polyfill(globalThis.WebAssembly); |
| 163 | + |
| 164 | + const module = await WebAssembly.compile(wasmBytes); |
| 165 | + const imports = WebAssembly.Module.imports(module); |
| 166 | + |
| 167 | + const { wasi, instance } = await instantiate({ |
| 168 | + module, |
| 169 | + addToImports: async (importObject) => { |
| 170 | + const memoryImport = imports.find(i => i.module === 'env' && i.name === 'memory'); |
| 171 | + if (!memoryImport) { |
| 172 | + return; |
| 173 | + } |
| 174 | + |
| 175 | + // Add wasi-threads support if memory is imported |
| 176 | + const memoryType = memoryImport.type; |
| 177 | + const memory = new WebAssembly.Memory({ |
| 178 | + initial: memoryType.minimum, |
| 179 | + maximum: memoryType.maximum, |
| 180 | + shared: memoryType.shared, |
| 181 | + }); |
| 182 | + const threads = await Threads.create(8, module, memory); |
| 183 | + importObject.env = { memory }; |
| 184 | + importObject.wasi = { |
| 185 | + "thread-spawn": (startArg) => { |
| 186 | + return threads.spawnThread(startArg); |
| 187 | + } |
| 188 | + }; |
| 189 | + }, |
| 190 | + }); |
| 191 | + |
| 192 | + const exitCode = wasi.start(instance); |
| 193 | + return exitCode === 0; |
| 194 | +} |
| 195 | + |
| 196 | +/** |
| 197 | + * @param {{ |
| 198 | + * channel: SharedArrayBuffer, |
| 199 | + * workerIndex: number, |
| 200 | + * module: WebAssembly.Module, |
| 201 | + * memory: WebAssembly.Memory |
| 202 | + * }} |
| 203 | + */ |
| 204 | +export async function startWorker({ channel, workerIndex, module, memory }) { |
| 205 | + const view = new Int32Array(channel, workerIndex * WorkerInfoView.Layout.SIZE * Int32Array.BYTES_PER_ELEMENT); |
| 206 | + const infoView = new WorkerInfoView(view); |
| 207 | + // Mark the worker as ready |
| 208 | + infoView.setStateAndNotify(WorkerState.READY); |
| 209 | + // Wait until the main thread marks the worker as started |
| 210 | + await (await infoView.waitWhile(WorkerState.READY)).value; |
| 211 | + const tid = infoView.tid; |
| 212 | + const startArg = infoView.startArg; |
| 213 | + await startThread({ module, memory, tid, startArg }); |
| 214 | + // Mark the worker as finished |
| 215 | + infoView.setStateAndNotify(WorkerState.FINISHED); |
| 216 | +} |
| 217 | + |
| 218 | +async function startThread({ module, memory, tid, startArg }) { |
| 219 | + const { instance, wasi } = await instantiate({ |
| 220 | + module, |
| 221 | + addToImports(importObject) { |
| 222 | + importObject["env"] = { memory } |
| 223 | + importObject["wasi"] = { |
| 224 | + "thread-spawn": () => { throw new Error("Cannot spawn a new thread from a worker thread"); } |
| 225 | + }; |
| 226 | + }, |
| 227 | + }); |
| 228 | + |
| 229 | + wasi.inst = instance; |
| 230 | + instance.exports.wasi_thread_start(tid, startArg); |
| 231 | +} |
0 commit comments