Skip to content
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

Add example for wasm_threads usage #841

Merged
merged 7 commits into from
Aug 7, 2024
Merged
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 .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
@@ -175,6 +175,7 @@ jobs:
- webworker
- webworker-gloo
- webworker-module
- wasm_threads
- yew
- yew-tailwindcss
- yew-tls
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -2,6 +2,7 @@ target
dist
site/public/*
.vscode
!examples/wasm_threads/.vscode
.idea/
.DS_Store
/vendor
8 changes: 8 additions & 0 deletions examples/wasm_threads/.cargo/config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Translating complex cargo invocation into this config file, so that trunk will use the same setup
# https://github.com/chemicstry/wasm_thread/blob/main/build_wasm.sh

[target.wasm32-unknown-unknown]
rustflags = ["-C", "target-feature=+atomics,+bulk-memory,+mutable-globals"]

[unstable]
build-std = ["std,panic_abort"]
7 changes: 7 additions & 0 deletions examples/wasm_threads/.vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
// The build-std flag requires an explicit target.
// This breaks the usual rust-analyzer setup.
// But setting the target manually for the workspace fixes that.
// "rust-analyzer.cargo.target": "x86_64-unknown-linux-gnu",
"rust-analyzer.cargo.target": "wasm32-unknown-unknown",
}
300 changes: 300 additions & 0 deletions examples/wasm_threads/Cargo.lock

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

10 changes: 10 additions & 0 deletions examples/wasm_threads/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
[package]
name = "wasm_threads"
version = "0.1.0"
edition = "2021"

[dependencies]
console_error_panic_hook = "0.1.7"
console_log = { version = "1.0.0", features = ["color"] }
log = "0.4.22"
wasm_thread = "0.3.0"
48 changes: 48 additions & 0 deletions examples/wasm_threads/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Support workers with shared memory and one wasm binary

This is a port of [wasm_threads `simple.rs` example](https://github.com/chemicstry/wasm_thread/tree/main?tab=readme-ov-file#simple).

It should also work similarly with `wasm-bindgen-rayon` and other packages that use SharedArrayBuffer.

An explanation of that approach is described [here](https://rustwasm.github.io/wasm-bindgen/examples/raytrace.html)

## Limitations

It has a few considerable advantages over the `webworker*` examples, but also considerable disadvantages.

For starters, this needs Cross Site Isolation (setting 2 headers), which is [required for this approach to workers](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/SharedArrayBuffer#security_requirements).
We've added them in the trunk config.

These same headers are also required during deployment. Github pages does not allow setting headers, and alternatives such as using `<meta>` did not work in my testing, so these sites can't be deployed like that. Cloudflare Pages is a free alternative that allows setting headers that worked for me.

Then it also requires nightly Rust, because [the standard library needs to be rebuild](https://github.com/RReverser/wasm-bindgen-rayon?tab=readme-ov-file#building-rust-code).

Some libraries might not work correctly like that, since they are written under the assumption of the historially single threaded wasm32 runtimes. `wgpu` has the `fragile-send-sync-non-atomic-wasm` flag, which if set will not work with this. Eg. `egui` sets this flag currently, though it can be removed with a manual, not well tested [patch](https://github.com/9SMTM6/egui/commit/11b00084e34c8b0ff40bac82274291dff64c26db).

Additional limitations are listed [here](https://rustwasm.github.io/wasm-bindgen/examples/raytrace.html#caveats) (some of them might be solved or worked around in libraries). Specifically for `wasm_thread` limitations are explained in the comments in the source code.

## Advantages

* code sharing
* improves dev experience
* also means that the WASM binary will be shared, which can in the extreme case half the size of the website.
* shared memory between threads
* can be a huge performance win

## Notes on applying this

Note that this requires the [toolchain file](./rust-toolchain.toml) and the [cargo config](.cargo/config.toml).

The `_headers` file and its copy in `index.html` is simply an example of how to set the headers using Cloudflare Pages.

If you get errors such as

> [Firefox] The WebAssembly.Memory object cannot be serialized. The Cross-Origin-Opener-Policy and Cross-Origin-Embedder-Policy HTTP headers can be used to enable this.
> [Chrome] SharedArrayBuffer transfer requires self.crossOriginIsolated.
Then the headers did not set correctly. You can check the response headers on the `/` file in the network tab of the browser developer tools.

## Using rust-analyzer

Since we use the build-std flag in the toolchain file, and that requires an explicit target to be set for compilation etc., this will break rust-analyzer in many setups. This can be solved by specifying an explicit target for the workspace, such as with the provided [config file for vscode](./.vscode/settings.json).
9 changes: 9 additions & 0 deletions examples/wasm_threads/Trunk.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
[build]
target = "index.html"
dist = "dist"

[serve.headers]
# see ./assets/_headers for more documentation
"cross-origin-embedder-policy"= "require-corp"
"cross-origin-opener-policy"= "same-origin"
"cross-origin-resource-policy"= "same-site"
10 changes: 10 additions & 0 deletions examples/wasm_threads/assets/_headers
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Sets headers on cloudflare pages as required for wasm_threads, because they rely on SharedArrayBuffer.
# https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/SharedArrayBuffer#security_requirements
# https://developers.cloudflare.com/pages/configuration/headers/
/*
# Alternatively `credentialless` also works
# MDN: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cross-Origin-Embedder-Policy
cross-origin-embedder-policy: require-corp
cross-origin-opener-policy: same-origin
# not strictly required, just allows you to load assets from the same... subdomain IIRC.
cross-origin-resource-policy: same-site
22 changes: 22 additions & 0 deletions examples/wasm_threads/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="data:," />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Trunk | wasm_threads</title>

<base data-trunk-public-url />
</head>
<body>
<link
data-trunk
rel="rust"
href="Cargo.toml"
data-wasm-opt="z"
data-bindgen-target="web"
/>
<link data-trunk rel="copy-file" href="assets/_headers" />
<p>See the console for the thread output</p>
</body>
</html>
6 changes: 6 additions & 0 deletions examples/wasm_threads/rust-toolchain.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Before upgrading check that everything is available on all tier1 targets here:
# https://rust-lang.github.io/rustup-components-history
[toolchain]
channel = "nightly-2024-08-02"
targets = ["wasm32-unknown-unknown"]
components = ["rust-src", "rustfmt", "clippy"]
77 changes: 77 additions & 0 deletions examples/wasm_threads/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
use std::time::Duration;

use wasm_thread as thread;

fn main() {
#[cfg(target_arch = "wasm32")]
{
console_log::init().unwrap();
console_error_panic_hook::set_once();
}

#[cfg(not(target_arch = "wasm32"))]
env_logger::init_from_env(env_logger::Env::default().filter_or(env_logger::DEFAULT_FILTER_ENV, "info"));

log::info!("Available parallelism: {:?}", thread::available_parallelism());

let mut threads = vec![];

for _ in 0..2 {
threads.push(thread::spawn(|| {
for i in 1..3 {
log::info!("hi number {} from the spawned thread {:?}!", i, thread::current().id());
thread::sleep(Duration::from_millis(1));
}
}));
}

for i in 1..3 {
log::info!("hi number {} from the main thread {:?}!", i, thread::current().id());
}

// It's not possible to do a scope on the main thread, because blocking waits are not supported, but we can use
// scope inside web workers.
threads.push(thread::spawn(|| {
log::info!("Start scope test on thread {:?}", thread::current().id());

let mut a = vec![1, 2, 3];
let mut x = 0;

thread::scope(|s| {
let handle = s.spawn(|| {
log::info!("hello from the first scoped thread {:?}", thread::current().id());
// We can borrow `a` here.
log::info!("a = {:?}", &a);
// Return a subslice of borrowed `a`
&a[0..2]
});

// Wait for the returned value from first thread
log::info!("a[0..2] = {:?}", handle.join().unwrap());

s.spawn(|| {
log::info!("hello from the second scoped thread {:?}", thread::current().id());
// We can even mutably borrow `x` here,
// because no other threads are using it.
x += a[0] + a[2];
});

log::info!(
"Hello from scope \"main\" thread {:?} inside scope.",
thread::current().id()
);
});

// After the scope, we can modify and access our variables again:
a.push(4);
assert_eq!(x, a.len());
log::info!("Scope done x = {}, a.len() = {}", x, a.len());
}));

// Wait for all threads, otherwise program exits before threads finish execution.
// We can't do blocking join on wasm main thread though, but the browser window will continue running.
#[cfg(not(target_arch = "wasm32"))]
for handle in threads {
handle.join().unwrap();
}
}