Skip to content

Commit 30a7f1f

Browse files
authored
Web optimizations (#559)
As discussed in #541. This PR adds the following improvements: - Caching of the global `Crypto` object. - Detecting if our Wasm memory is based on a `SharedArrayBuffer`. If not, we can copy bytes directly into our memory instead of having to go through JS. This saves allocating the buffer in JS and copying the bytes into Wasm memory. This is also the most common path. `SharedArrayBuffer` requires `target_feature = "atomics"`, which is unstable and requires Rust nightly. See #559 (comment) for full context. - The atomic path only creates a sub-array when necessary, potentially saving another FFI call. - The atomic path will now allocate an `Uint8Array` with the minimum amount of bytes necessary instead of a fixed size. - The maximum chunk size for the non-atomic path and the maximum `Uint8Array` size for the atomic paths have been increased to 65536 bytes: the maximum allowed buffer size for `Crypto.getRandomValues()`. All in all this should give a performance improvement of ~5% to ~500% depending on the amount of requested bytes and which path is taken. See #559 (comment) for some benchmark results. This spawned a bunch of improvements and fixes in `wasm-bindgen` that are being used here: - rustwasm/wasm-bindgen#4315 - rustwasm/wasm-bindgen#4316 - rustwasm/wasm-bindgen#4318 - rustwasm/wasm-bindgen#4319 - rustwasm/wasm-bindgen#4340
1 parent ae0c807 commit 30a7f1f

File tree

5 files changed

+75
-45
lines changed

5 files changed

+75
-45
lines changed

.github/workflows/tests.yml

+34-14
Original file line numberDiff line numberDiff line change
@@ -229,11 +229,31 @@ jobs:
229229
# run: cargo test
230230

231231
web:
232-
name: Web
232+
name: ${{ matrix.rust.description }}
233233
runs-on: ubuntu-24.04
234+
strategy:
235+
fail-fast: false
236+
matrix:
237+
rust:
238+
- {
239+
description: Web,
240+
version: stable,
241+
flags: -Dwarnings --cfg getrandom_backend="wasm_js",
242+
args: --features=std,
243+
}
244+
- {
245+
description: Web with Atomics,
246+
version: nightly,
247+
components: rust-src,
248+
flags: '-Dwarnings --cfg getrandom_backend="wasm_js" -Ctarget-feature=+atomics,+bulk-memory',
249+
args: '--features=std -Zbuild-std=panic_abort,std',
250+
}
234251
steps:
235252
- uses: actions/checkout@v4
236-
- uses: dtolnay/rust-toolchain@stable
253+
- uses: dtolnay/rust-toolchain@master
254+
with:
255+
toolchain: ${{ matrix.rust.version }}
256+
components: ${{ matrix.rust.components }}
237257
- name: Install precompiled wasm-pack
238258
shell: bash
239259
run: |
@@ -244,34 +264,34 @@ jobs:
244264
- uses: Swatinem/rust-cache@v2
245265
- name: Test (Node)
246266
env:
247-
RUSTFLAGS: -Dwarnings --cfg getrandom_backend="wasm_js"
248-
run: wasm-pack test --node -- --features std
267+
RUSTFLAGS: ${{ matrix.rust.flags }}
268+
run: wasm-pack test --node -- ${{ matrix.rust.args }}
249269
- name: Test (Firefox)
250270
env:
251271
WASM_BINDGEN_USE_BROWSER: 1
252-
RUSTFLAGS: -Dwarnings --cfg getrandom_backend="wasm_js"
253-
run: wasm-pack test --headless --firefox -- --features std
272+
RUSTFLAGS: ${{ matrix.rust.flags }}
273+
run: wasm-pack test --headless --firefox -- ${{ matrix.rust.args }}
254274
- name: Test (Chrome)
255275
env:
256276
WASM_BINDGEN_USE_BROWSER: 1
257-
RUSTFLAGS: -Dwarnings --cfg getrandom_backend="wasm_js"
258-
run: wasm-pack test --headless --chrome -- --features std
277+
RUSTFLAGS: ${{ matrix.rust.flags }}
278+
run: wasm-pack test --headless --chrome -- ${{ matrix.rust.args }}
259279
- name: Test (dedicated worker)
260280
env:
261281
WASM_BINDGEN_USE_DEDICATED_WORKER: 1
262-
RUSTFLAGS: -Dwarnings --cfg getrandom_backend="wasm_js"
263-
run: wasm-pack test --headless --firefox -- --features std
282+
RUSTFLAGS: ${{ matrix.rust.flags }}
283+
run: wasm-pack test --headless --firefox -- ${{ matrix.rust.args }}
264284
- name: Test (shared worker)
265285
env:
266286
WASM_BINDGEN_USE_SHARED_WORKER: 1
267-
RUSTFLAGS: -Dwarnings --cfg getrandom_backend="wasm_js"
268-
run: wasm-pack test --headless --firefox -- --features std
287+
RUSTFLAGS: ${{ matrix.rust.flags }}
288+
run: wasm-pack test --headless --firefox -- ${{ matrix.rust.args }}
269289
- name: Test (service worker)
270290
env:
271291
WASM_BINDGEN_USE_SERVICE_WORKER: 1
272-
RUSTFLAGS: -Dwarnings --cfg getrandom_backend="wasm_js"
292+
RUSTFLAGS: ${{ matrix.rust.flags }}
273293
# Firefox doesn't support module service workers and therefor can't import scripts
274-
run: wasm-pack test --headless --chrome -- --features std
294+
run: wasm-pack test --headless --chrome -- ${{ matrix.rust.args }}
275295

276296
wasi:
277297
name: WASI

.github/workflows/workspace.yml

+4
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,10 @@ jobs:
4949
env:
5050
RUSTFLAGS: -Dwarnings --cfg getrandom_backend="wasm_js"
5151
run: cargo clippy -Zbuild-std --target wasm32-unknown-unknown
52+
- name: Web WASM with atomics (wasm_js.rs)
53+
env:
54+
RUSTFLAGS: -Dwarnings --cfg getrandom_backend="wasm_js" -Ctarget-feature=+atomics,+bulk-memory
55+
run: cargo clippy -Zbuild-std --target wasm32-unknown-unknown
5256
- name: Linux (linux_android.rs)
5357
env:
5458
RUSTFLAGS: -Dwarnings --cfg getrandom_backend="linux_getrandom"

Cargo.toml

+3-2
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,9 @@ windows-targets = "0.52"
6464

6565
# wasm_js
6666
[target.'cfg(all(getrandom_backend = "wasm_js", target_arch = "wasm32", any(target_os = "unknown", target_os = "none")))'.dependencies]
67-
wasm-bindgen = { version = "0.2.96", default-features = false }
68-
js-sys = { version = "0.3.73", default-features = false }
67+
wasm-bindgen = { version = "0.2.98", default-features = false }
68+
[target.'cfg(all(getrandom_backend = "wasm_js", target_arch = "wasm32", any(target_os = "unknown", target_os = "none"), target_feature = "atomics"))'.dependencies]
69+
js-sys = { version = "0.3.75", default-features = false }
6970
[target.'cfg(all(getrandom_backend = "wasm_js", target_arch = "wasm32", any(target_os = "unknown", target_os = "none")))'.dev-dependencies]
7071
wasm-bindgen-test = "0.3"
7172

src/backends/wasm_js.rs

+34-26
Original file line numberDiff line numberDiff line change
@@ -7,35 +7,46 @@ pub use crate::util::{inner_u32, inner_u64};
77
#[cfg(not(all(target_arch = "wasm32", any(target_os = "unknown", target_os = "none"))))]
88
compile_error!("`wasm_js` backend can be enabled only for OS-less WASM targets!");
99

10-
use js_sys::{global, Uint8Array};
11-
use wasm_bindgen::{prelude::wasm_bindgen, JsCast, JsValue};
10+
use wasm_bindgen::{prelude::wasm_bindgen, JsValue};
1211

13-
// Size of our temporary Uint8Array buffer used with WebCrypto methods
14-
// Maximum is 65536 bytes see https://developer.mozilla.org/en-US/docs/Web/API/Crypto/getRandomValues
15-
const CRYPTO_BUFFER_SIZE: u16 = 256;
12+
// Maximum buffer size allowed in `Crypto.getRandomValuesSize` is 65536 bytes.
13+
// See https://developer.mozilla.org/en-US/docs/Web/API/Crypto/getRandomValues
14+
const MAX_BUFFER_SIZE: usize = 65536;
1615

16+
#[cfg(not(target_feature = "atomics"))]
1717
pub fn fill_inner(dest: &mut [MaybeUninit<u8>]) -> Result<(), Error> {
18-
let global: Global = global().unchecked_into();
19-
let crypto = global.crypto();
20-
21-
if !crypto.is_object() {
22-
return Err(Error::WEB_CRYPTO);
18+
for chunk in dest.chunks_mut(MAX_BUFFER_SIZE) {
19+
if get_random_values(chunk).is_err() {
20+
return Err(Error::WEB_CRYPTO);
21+
}
2322
}
23+
Ok(())
24+
}
2425

26+
#[cfg(target_feature = "atomics")]
27+
pub fn fill_inner(dest: &mut [MaybeUninit<u8>]) -> Result<(), Error> {
2528
// getRandomValues does not work with all types of WASM memory,
2629
// so we initially write to browser memory to avoid exceptions.
27-
let buf = Uint8Array::new_with_length(CRYPTO_BUFFER_SIZE.into());
28-
for chunk in dest.chunks_mut(CRYPTO_BUFFER_SIZE.into()) {
29-
let chunk_len: u32 = chunk
30+
let buf_len = usize::min(dest.len(), MAX_BUFFER_SIZE);
31+
let buf_len_u32 = buf_len
32+
.try_into()
33+
.expect("buffer length is bounded by MAX_BUFFER_SIZE");
34+
let buf = js_sys::Uint8Array::new_with_length(buf_len_u32);
35+
for chunk in dest.chunks_mut(buf_len) {
36+
let chunk_len = chunk
3037
.len()
3138
.try_into()
32-
.expect("chunk length is bounded by CRYPTO_BUFFER_SIZE");
39+
.expect("chunk length is bounded by MAX_BUFFER_SIZE");
3340
// The chunk can be smaller than buf's length, so we call to
3441
// JS to create a smaller view of buf without allocation.
35-
let sub_buf = buf.subarray(0, chunk_len);
42+
let sub_buf = if chunk_len == buf_len_u32 {
43+
&buf
44+
} else {
45+
&buf.subarray(0, chunk_len)
46+
};
3647

37-
if crypto.get_random_values(&sub_buf).is_err() {
38-
return Err(Error::WEB_GET_RANDOM_VALUES);
48+
if get_random_values(sub_buf).is_err() {
49+
return Err(Error::WEB_CRYPTO);
3950
}
4051

4152
// SAFETY: `sub_buf`'s length is the same length as `chunk`
@@ -46,14 +57,11 @@ pub fn fill_inner(dest: &mut [MaybeUninit<u8>]) -> Result<(), Error> {
4657

4758
#[wasm_bindgen]
4859
extern "C" {
49-
// Return type of js_sys::global()
50-
type Global;
51-
// Web Crypto API: Crypto interface (https://www.w3.org/TR/WebCryptoAPI/)
52-
type Crypto;
53-
// Getters for the Crypto API
54-
#[wasm_bindgen(method, getter)]
55-
fn crypto(this: &Global) -> Crypto;
5660
// Crypto.getRandomValues()
57-
#[wasm_bindgen(method, js_name = getRandomValues, catch)]
58-
fn get_random_values(this: &Crypto, buf: &Uint8Array) -> Result<(), JsValue>;
61+
#[cfg(not(target_feature = "atomics"))]
62+
#[wasm_bindgen(js_namespace = ["globalThis", "crypto"], js_name = getRandomValues, catch)]
63+
fn get_random_values(buf: &mut [MaybeUninit<u8>]) -> Result<(), JsValue>;
64+
#[cfg(target_feature = "atomics")]
65+
#[wasm_bindgen(js_namespace = ["globalThis", "crypto"], js_name = getRandomValues, catch)]
66+
fn get_random_values(buf: &js_sys::Uint8Array) -> Result<(), JsValue>;
5967
}

src/error.rs

-3
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,6 @@ impl Error {
3939
pub const NO_RDRAND: Error = Self::new_internal(6);
4040
/// The environment does not support the Web Crypto API.
4141
pub const WEB_CRYPTO: Error = Self::new_internal(7);
42-
/// Calling Web Crypto API `crypto.getRandomValues` failed.
43-
pub const WEB_GET_RANDOM_VALUES: Error = Self::new_internal(8);
4442
/// On VxWorks, call to `randSecure` failed (random number generator is not yet initialized).
4543
pub const VXWORKS_RAND_SECURE: Error = Self::new_internal(11);
4644
/// Calling Windows ProcessPrng failed.
@@ -155,7 +153,6 @@ fn internal_desc(error: Error) -> Option<&'static str> {
155153
Error::FAILED_RDRAND => "RDRAND: failed multiple times: CPU issue likely",
156154
Error::NO_RDRAND => "RDRAND: instruction not supported",
157155
Error::WEB_CRYPTO => "Web Crypto API is unavailable",
158-
Error::WEB_GET_RANDOM_VALUES => "Calling Web API crypto.getRandomValues failed",
159156
Error::VXWORKS_RAND_SECURE => "randSecure: VxWorks RNG module is not initialized",
160157
Error::WINDOWS_PROCESS_PRNG => "ProcessPrng: Windows system function failure",
161158
Error::RNDR_FAILURE => "RNDR: Could not generate a random number",

0 commit comments

Comments
 (0)