Skip to content

Commit

Permalink
Allow using Rayon on the main thread
Browse files Browse the repository at this point in the history
Starting with rayon 1.8.1 / rayon-core 1.12.1, Rayon has a web_spin_lock feature powered by wasm-sync that allows blocking on the main thread via spinning - same workaround for forbidden `atomics.wait` as used in e.g. Emscripten. rayon-rs/rayon#1110

We can leverage it and simplify instructions, tests and the demo to avoid an extra worker.
  • Loading branch information
RReverser committed Jan 17, 2024
1 parent 4ee8e54 commit 1f4f16e
Show file tree
Hide file tree
Showing 11 changed files with 85 additions and 125 deletions.
45 changes: 34 additions & 11 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 7 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,14 @@ readme = "README.md"
exclude = [".github"]
repository = "https://github.com/RReverser/wasm-bindgen-rayon"

[dependencies]
[workspace.dependencies]
wasm-bindgen = "0.2.84"
rayon-core = "1.12"
spmc = "0.3.0"
rayon = "1.8.1"

This comment has been minimized.

Copy link
@cuviper

cuviper Jan 17, 2024

Don't you want web_spin_lock here too?
(rayon/web_spin_lock enables rayon-core/web_spin_lock, but not the reverse.)

This comment has been minimized.

Copy link
@RReverser

RReverser Jan 17, 2024

Author Owner

Ah yeah, thanks.

Note that this is just a workspace dependency used by demo & tests - which currently don't use par_bridge, which is the only thing in Rayon itself that is the only thing that cares about web_spin_lock - but it's certainly a good idea to add it for future-proofing.

This comment has been minimized.

Copy link
@RReverser

RReverser Jan 17, 2024

Author Owner

Hmm although that also means I should probably mention it in README, since as of recently I'm only depending on rayon-core...

Maybe I should just revert it and depend on Rayon directly again so that feature is always auto-enabled.

This comment has been minimized.

Copy link
@RReverser

RReverser Jan 17, 2024

Author Owner

I'll think about it a bit more, for now just added a note to README.


[dependencies]
wasm-bindgen = { workspace = true }
rayon-core = { version = "1.12.1", features = ["web_spin_lock"] }
crossbeam-channel = "0.5.9"
js-sys = "0.3.48"

[workspace]
Expand Down
8 changes: 1 addition & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,9 @@ For a quick demo, check out <https://rreverser.com/wasm-bindgen-rayon-demo/>.

Before we get started, check out caveats listed in the [wasm-bindgen threading docs](https://rustwasm.github.io/wasm-bindgen/examples/raytrace.html). While this library specifically targets Rayon and automatically provides the necessary shims for you, some of the caveats still apply.

Most notably, even when you're using multithreading, the main thread still **can't be blocked** while waiting for the Rayon pool to get ready or for all the background operations to finish.

You must instantiate the main JS+Wasm in a dedicated `Worker` to avoid blocking the main thread - that is, don't mix UI and Rayon code together. Instead, use a library like [Comlink](https://github.com/GoogleChromeLabs/comlink) or a custom glue code to expose required wasm-bindgen methods to the main thread, and do the UI work from there.

## Setting up

First of all, in order to use `SharedArrayBuffer` on the Web, you need to enable [cross-origin isolation policies](https://web.dev/coop-coep/). Check out the linked article for details.
In order to use `SharedArrayBuffer` on the Web, you need to enable [cross-origin isolation policies](https://web.dev/coop-coep/). Check out the linked article for details.

Then, add this crate as a dependency to your `Cargo.toml` (in addition to `wasm-bindgen` and `rayon` themselves):

Expand Down Expand Up @@ -194,8 +190,6 @@ If you want to build this library for usage without bundlers, enable the `no-bun
wasm-bindgen-rayon = { version = "1.0", features = ["no-bundler"] }
```

Note that, in addition to the earlier mentioned restrictions, this will work out of the box only in browsers with [support for Module Workers](https://caniuse.com/mdn-api_worker_worker_ecmascript_modules). To ensure that it works in all browsers, either bundle your code for production, or include [module-workers-polyfill](https://unpkg.com/module-workers-polyfill) on the same page.

# License

This crate is licensed under the Apache-2.0 license.
4 changes: 2 additions & 2 deletions demo/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ crate-type = ["cdylib"]

[dependencies]
wasm-bindgen-rayon = { path = "..", optional = true }
wasm-bindgen = "0.2.74"
rayon = { version = "1.8", optional = true }
wasm-bindgen = { workspace = true }
rayon = { workspace = true, optional = true }
num-complex = "0.4.0"
once_cell = "1.7.2"
getrandom = { version = "0.2.2", features = ["js"] }
Expand Down
49 changes: 25 additions & 24 deletions demo/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,35 +11,27 @@
* limitations under the License.
*/

import * as Comlink from 'comlink';
import { threads } from 'wasm-feature-detect';

const maxIterations = 1000;

const canvas = document.getElementById('canvas');
const canvas = /** @type {HTMLCanvasElement} */ (
document.getElementById('canvas')
);
const { width, height } = canvas;
const ctx = canvas.getContext('2d');
const timeOutput = document.getElementById('time');
const timeOutput = /** @type {HTMLOutputElement} */ (
document.getElementById('time')
);

// Create a separate thread from wasm-worker.js and get a proxy to its handlers.
let handlers = await Comlink.wrap(
new Worker(new URL('./wasm-worker.js', import.meta.url), {
type: 'module'
})
).handlers;

function setupBtn(id) {
// Handlers are named in the same way as buttons.
let handler = handlers[id];
// If handler doesn't exist, it's not supported.
if (!handler) return;
function setupBtn(id, { generate }) {
// Assign onclick handler + enable the button.
Object.assign(document.getElementById(id), {
async onclick() {
let { rawImageData, time } = await handler({
width,
height,
maxIterations
});
const start = performance.now();
const rawImageData = generate(width, height, maxIterations);
const time = performance.now() - start;

timeOutput.value = `${time.toFixed(2)} ms`;
const imgData = new ImageData(rawImageData, width, height);
ctx.putImageData(imgData, 0, 0);
Expand All @@ -48,7 +40,16 @@ function setupBtn(id) {
});
}

setupBtn('singleThread');
if (await handlers.supportsThreads) {
setupBtn('multiThread');
}
(async function initSingleThread() {
const singleThread = await import('./pkg/wasm_bindgen_rayon_demo.js');
await singleThread.default();
setupBtn('singleThread', singleThread);
})();

(async function initMultiThread() {
if (!(await threads())) return;
const multiThread = await import('./pkg-parallel/wasm_bindgen_rayon_demo.js');
await multiThread.default();
await multiThread.initThreadPool(navigator.hardwareConcurrency);
setupBtn('multiThread', multiThread);
})();
46 changes: 0 additions & 46 deletions demo/wasm-worker.js

This file was deleted.

4 changes: 2 additions & 2 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@
#[cfg(all(not(doc), not(target_feature = "atomics")))]
compile_error!("Did you forget to enable `atomics` and `bulk-memory` features as outlined in wasm-bindgen-rayon README?");

use crossbeam_channel::{bounded, Receiver, Sender};
use js_sys::Promise;
use spmc::{channel, Receiver, Sender};
use wasm_bindgen::prelude::*;
use wasm_bindgen::JsValue;

Expand Down Expand Up @@ -60,7 +60,7 @@ fn _ensure_worker_emitted() {
#[wasm_bindgen]
impl wbg_rayon_PoolBuilder {
fn new(num_threads: usize) -> Self {
let (sender, receiver) = channel();
let (sender, receiver) = bounded(num_threads);
Self {
num_threads,
sender,
Expand Down
4 changes: 2 additions & 2 deletions test/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,5 @@ crate-type = ["cdylib"]

[dependencies]
wasm-bindgen-rayon = { path = ".." }
wasm-bindgen = "0.2.74"
rayon = "1.8"
wasm-bindgen = { workspace = true }
rayon = { workspace = true }
14 changes: 11 additions & 3 deletions test/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,18 @@
* limitations under the License.
*/

import init, { initThreadPool, sum } from './pkg/test.js';

await init();
await initThreadPool(navigator.hardwareConcurrency);
// 1...10
let arr = Int32Array.from({ length: 10 }, (_, i) => i + 1);
if (sum(arr) !== 55) {
throw new Error('Wrong result.');
}

// Note: this will be overridden by the Playwright test runner.
// The default implementation is provided only for manual testing.
globalThis.onDone ??= () => console.log('OK');

new Worker(new URL('./index.worker.js', import.meta.url), {
type: 'module'
}).addEventListener('message', globalThis.onDone);
onDone();
25 changes: 0 additions & 25 deletions test/index.worker.js

This file was deleted.

1 change: 1 addition & 0 deletions test/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 1f4f16e

Please sign in to comment.