From 6ddd5cbde69fb912a8e165ff729ab6a95058b3cd Mon Sep 17 00:00:00 2001 From: Jeff Hemphill Date: Wed, 13 Sep 2023 18:34:24 -0700 Subject: [PATCH] Minimal repro --- Cargo.toml | 1 + examples/thread-object-sharing/Cargo.toml | 12 ++++ examples/thread-object-sharing/README.md | 9 +++ examples/thread-object-sharing/build.py | 58 +++++++++++++++++ examples/thread-object-sharing/index.html | 20 ++++++ examples/thread-object-sharing/index.js | 78 +++++++++++++++++++++++ examples/thread-object-sharing/run.py | 9 +++ examples/thread-object-sharing/server.py | 16 +++++ examples/thread-object-sharing/src/lib.rs | 28 ++++++++ examples/thread-object-sharing/worker.js | 50 +++++++++++++++ 10 files changed, 281 insertions(+) create mode 100644 examples/thread-object-sharing/Cargo.toml create mode 100644 examples/thread-object-sharing/README.md create mode 100644 examples/thread-object-sharing/build.py create mode 100644 examples/thread-object-sharing/index.html create mode 100644 examples/thread-object-sharing/index.js create mode 100644 examples/thread-object-sharing/run.py create mode 100644 examples/thread-object-sharing/server.py create mode 100644 examples/thread-object-sharing/src/lib.rs create mode 100644 examples/thread-object-sharing/worker.js diff --git a/Cargo.toml b/Cargo.toml index aa4fe5e6e05c..d11345f5fc94 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -84,6 +84,7 @@ members = [ "examples/performance", "examples/raytrace-parallel", "examples/request-animation-frame", + "examples/thread-object-sharing", "examples/todomvc", "examples/wasm-audio-worklet", "examples/wasm-in-wasm", diff --git a/examples/thread-object-sharing/Cargo.toml b/examples/thread-object-sharing/Cargo.toml new file mode 100644 index 000000000000..11ff02d645c6 --- /dev/null +++ b/examples/thread-object-sharing/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "thread-object-sharing" +version = "0.1.0" +authors = ["The wasm-bindgen Developers"] +edition = "2021" +rust-version = "1.74" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +wasm-bindgen = "0.2.87" diff --git a/examples/thread-object-sharing/README.md b/examples/thread-object-sharing/README.md new file mode 100644 index 000000000000..21d1c6c35c8d --- /dev/null +++ b/examples/thread-object-sharing/README.md @@ -0,0 +1,9 @@ +# Object sharing across threads + +You can build the example locally with: + +``` +$ python3 ./run.py +``` + +and then visiting http://localhost:8080 in a browser should run the example! diff --git a/examples/thread-object-sharing/build.py b/examples/thread-object-sharing/build.py new file mode 100644 index 000000000000..0d32376c2898 --- /dev/null +++ b/examples/thread-object-sharing/build.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python3 +import os +import subprocess + +root_dir = os.path.dirname(__file__) + +# A couple of steps are necessary to get this build working which makes it slightly +# nonstandard compared to most other builds. +# +# * First, the Rust standard library needs to be recompiled with atomics +# enabled. to do that we use Cargo's unstable `-Zbuild-std` feature. +# +# * Next we need to compile everything with the `atomics` and `bulk-memory` +# features enabled, ensuring that LLVM will generate atomic instructions, +# shared memory, passive segments, etc. + +os.environ.update( + {"RUSTFLAGS": "-C target-feature=+atomics,+bulk-memory,+mutable-globals"} +) + +subprocess.run( + [ + "cargo", + "build", + "--target", + "wasm32-unknown-unknown", + "--release", + "-Zbuild-std=std,panic_abort", + ], + cwd=root_dir, +).check_returncode() + +# Note the usage of `--target no-modules` here which is required for passing +# the memory import to each wasm module. +subprocess.run( + [ + "cargo", + "run", + "-p", + "wasm-bindgen-cli", + "--", + os.path.join( + root_dir, + "..", + "..", + "target", + "wasm32-unknown-unknown", + "release", + "thread_object_sharing.wasm", + ), + "--out-dir", + os.path.join(root_dir, "pkg"), + "--target", + "web", + # "--split-linked-modules", + ], + cwd=root_dir, +).check_returncode() diff --git a/examples/thread-object-sharing/index.html b/examples/thread-object-sharing/index.html new file mode 100644 index 000000000000..a7ac34edebf9 --- /dev/null +++ b/examples/thread-object-sharing/index.html @@ -0,0 +1,20 @@ + + + + + Sharing a wasm object across workers + + + + + + +
+ Some atomic value + 0 +
+ + + + + \ No newline at end of file diff --git a/examples/thread-object-sharing/index.js b/examples/thread-object-sharing/index.js new file mode 100644 index 000000000000..6a5f4d4766e5 --- /dev/null +++ b/examples/thread-object-sharing/index.js @@ -0,0 +1,78 @@ +/** @type {import('./pkg/thread_object_sharing')} */ +import initWasm, { OpaqueObject } from "./pkg/thread_object_sharing.js"; + +// Create a few webworkers, leaving some breathing room so our computer doesn't die +const numWorkers = navigator.hardwareConcurrency / 2; +const workers = []; +for (let i = 0; i < numWorkers; ++i) { + workers.push(new Worker( + new URL('./worker.js', import.meta.url), + { name: 'Call OpaqueObject.get() a bunch of times', type: 'module' }, + )); +} + +// Load the webassembly and send our memory to the workers +const wasmInternals = await initWasm(); +for (const worker of workers) { + worker.postMessage({ type: 'load', memory: wasmInternals.memory }); +} + +const opaqueObject = new OpaqueObject(47); + +// True iff webworkers are running +let running = false; + +// Visual representation of opaqueObject.get() +const valueMeter = window.document.getElementById('value-meter'); +const valueDisplayText = window.document.getElementById('value-display-text'); + +// Change the rate at which the webworkers increment opaqueObject.get()'s value +const incrementInput = window.document.getElementById('increment-input'); +incrementInput.onchange = (event) => { + const incrementVal = event.target.value; + for (const worker of workers) { + worker.postMessage({ type: 'increment', incrementVal }); + } +} + +/** + * Start the webworkers + */ +function start() { + for (const worker of workers) { + worker.postMessage({ type: 'start', opaqueObjectPtr: opaqueObject.__wbg_ptr }); + } + running = true; +} + +/** + * Stop the webworkers + */ +function stop() { + for (const worker of workers) { + worker.postMessage({ type: 'stop' }); + } + running = false; +} + +// Clickable button to start and stop the webworkers +const toggleButton = window.document.getElementById('toggle-button'); +toggleButton.onclick = () => { + if (running) { + stop(); + } else { + start(); + } + toggleButton.innerText = running ? 'stop' : 'start'; +}; +toggleButton.removeAttribute('disabled'); +toggleButton.innerText = running ? 'stop' : 'start'; + +function render() { + const val = opaqueObject.get(); + valueMeter.setAttribute('value', val); + valueDisplayText.innerText = val.toString(); + + window.requestAnimationFrame(render); +} +window.requestAnimationFrame(render); diff --git a/examples/thread-object-sharing/run.py b/examples/thread-object-sharing/run.py new file mode 100644 index 000000000000..411fb94d0e3b --- /dev/null +++ b/examples/thread-object-sharing/run.py @@ -0,0 +1,9 @@ +#!/usr/bin/env python3 +import os +import subprocess + +root_dir = os.path.dirname(__file__) + +subprocess.run(["python3", "build.py"], cwd=root_dir).check_returncode() + +subprocess.run(["python3", "server.py"], cwd=root_dir).check_returncode() diff --git a/examples/thread-object-sharing/server.py b/examples/thread-object-sharing/server.py new file mode 100644 index 000000000000..161c3e6088d2 --- /dev/null +++ b/examples/thread-object-sharing/server.py @@ -0,0 +1,16 @@ +#!/usr/bin/env python3 +from http.server import HTTPServer, SimpleHTTPRequestHandler, test +import sys + + +class RequestHandler(SimpleHTTPRequestHandler): + def end_headers(self): + self.send_header("Cross-Origin-Opener-Policy", "same-origin") + self.send_header("Cross-Origin-Embedder-Policy", "require-corp") + SimpleHTTPRequestHandler.end_headers(self) + + +if __name__ == "__main__": + test( + RequestHandler, HTTPServer, port=int(sys.argv[1]) if len(sys.argv) > 1 else 8080 + ) diff --git a/examples/thread-object-sharing/src/lib.rs b/examples/thread-object-sharing/src/lib.rs new file mode 100644 index 000000000000..f9cce3062235 --- /dev/null +++ b/examples/thread-object-sharing/src/lib.rs @@ -0,0 +1,28 @@ +use std::sync::atomic::{AtomicI32, Ordering}; + +use wasm_bindgen::prelude::*; + +#[wasm_bindgen] +pub struct OpaqueObject { + val: AtomicI32, +} + +#[wasm_bindgen] +impl OpaqueObject { + #[wasm_bindgen(constructor)] + pub fn new(val: i32) -> Self { + Self { + val: AtomicI32::new(val), + } + } + + #[wasm_bindgen] + pub fn get(&self) -> i32 { + self.val.load(Ordering::SeqCst) + } + + #[wasm_bindgen] + pub fn add(&self, num: i32) -> i32 { + self.val.fetch_add(num, Ordering::SeqCst) + } +} diff --git a/examples/thread-object-sharing/worker.js b/examples/thread-object-sharing/worker.js new file mode 100644 index 000000000000..c6b47339d572 --- /dev/null +++ b/examples/thread-object-sharing/worker.js @@ -0,0 +1,50 @@ +/** @type {import('./pkg/thread_object_sharing')} */ +import init, { OpaqueObject } from "./pkg/thread_object_sharing.js"; + +/** + * @typedef {{type: 'load', memory: WebAssembly.Memory}} WorkerRequestLoad + * @typedef {{type: 'start', opaqueObjectPtr: number}} WorkerRequestStart + * @typedef {{type: 'stop'}} WorkerRequestStop + * @typedef {{type: 'increment', 'incrementVal': number}} WorkerRequestIncrement + * @typedef {WorkerRequestLoad | WorkerRequestStart | WorkerRequestStop | WorkerRequestIncrement} WorkerRequest + */ + +/** @type {number | undefined} */ +let intervalTimer = undefined; +let incrementVal = 10; + +/** @type {OpaqueObject} */ +const opaqueObject = Object.create(OpaqueObject.prototype); + +function tick() { + opaqueObject.add(incrementVal); +} + +self.onmessage = async (event) => { + /** + * @type {WorkerRequest} + */ + const request = event.data; + + switch (request.type) { + case 'load': + await init(undefined, request.memory); + break; + case 'start': + opaqueObject.__wbg_ptr = request.opaqueObjectPtr; + if (intervalTimer === undefined) { + intervalTimer = self.setInterval(tick, 15); + console.log('starting intervalTimer'); + } + break; + case 'stop': + console.log('stopping intervalTimer'); + self.clearInterval(intervalTimer); + intervalTimer = undefined; + break; + case 'increment': + incrementVal = request.incrementVal; + console.log(`New increment value: ${incrementVal}`); + break; + } +};