Skip to content

Commit 84ee5aa

Browse files
committed
probe: wasm32 byte-reproducibility across machines (#218)
1 parent 34e314b commit 84ee5aa

9 files changed

Lines changed: 167 additions & 0 deletions

File tree

.github/workflows/wasm-repro.yml

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# THROWAWAY. Issue #218 only. This file must never reach `main`.
2+
#
3+
# The question: does the pinned toolchain produce a byte-identical
4+
# wasm32-unknown-unknown module on an ubuntu runner and on a macOS dev machine?
5+
# The answer decides whether the `rust` job can assert byte identity for the
6+
# committed engine.wasm, or has to fall back to rebuild-and-run-the-tests.
7+
#
8+
# `on: push` restricted to this branch, NOT `workflow_dispatch`: GitHub only
9+
# makes a workflow_dispatch workflow triggerable once the file exists on the
10+
# DEFAULT branch, so dispatching one that lives only here fails with "workflow
11+
# does not have 'workflow_dispatch' trigger". Pushing the branch has no such
12+
# requirement.
13+
name: wasm-repro
14+
15+
on:
16+
push:
17+
branches: [probe/wasm-reproducibility]
18+
19+
permissions:
20+
contents: read
21+
22+
jobs:
23+
hash:
24+
runs-on: ubuntu-latest
25+
timeout-minutes: 10
26+
steps:
27+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
28+
- name: Toolchain
29+
run: |
30+
rustup toolchain install 1.97.1 --profile minimal
31+
rustup target add wasm32-unknown-unknown --toolchain 1.97.1
32+
rustup run 1.97.1 rustc --version --verbose
33+
- name: Build and hash
34+
working-directory: crates-probe
35+
run: |
36+
rustup run 1.97.1 cargo build --locked --release \
37+
--target wasm32-unknown-unknown -p fmw-wasm
38+
F=target/wasm32-unknown-unknown/release/fmw_wasm.wasm
39+
echo "PROBE_BYTES=$(wc -c < "$F")"
40+
echo "PROBE_SHA=$(sha256sum "$F" | cut -d' ' -f1)"

crates-probe/Cargo.lock

Lines changed: 14 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates-probe/Cargo.toml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
[workspace]
2+
resolver = "2"
3+
members = ["crates/fmw-noise", "crates/fmw-wasm"]
4+
[profile.release]
5+
opt-level = 3
6+
lto = true
7+
codegen-units = 1
8+
panic = "abort"
9+
strip = true
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
[package]
2+
name = "fmw-noise"
3+
version = "0.0.0"
4+
edition = "2021"
5+
publish = false
6+
[dependencies]
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
const FNV_OFFSET_BASIS: u64 = 0xcbf2_9ce4_8422_2325;
2+
const FNV_PRIME: u64 = 0x0000_0100_0000_01b3;
3+
4+
#[must_use]
5+
pub fn fnv1a64(bytes: &[u8]) -> u64 {
6+
let mut hash = FNV_OFFSET_BASIS;
7+
for &byte in bytes {
8+
hash ^= u64::from(byte);
9+
hash = hash.wrapping_mul(FNV_PRIME);
10+
}
11+
hash
12+
}
13+
14+
#[must_use]
15+
pub fn fold_f64(acc: u64, value: f64) -> u64 {
16+
let mut hash = if acc == 0 { FNV_OFFSET_BASIS } else { acc };
17+
for &byte in &value.to_bits().to_le_bytes() {
18+
hash ^= u64::from(byte);
19+
hash = hash.wrapping_mul(FNV_PRIME);
20+
}
21+
hash
22+
}
23+
24+
#[cfg(test)]
25+
mod tests {
26+
use super::*;
27+
#[test]
28+
fn matches_the_published_fnv1a64_vectors() {
29+
assert_eq!(fnv1a64(b""), 0xcbf2_9ce4_8422_2325);
30+
assert_eq!(fnv1a64(b"a"), 0xaf63_dc4c_8601_ec8c);
31+
assert_eq!(fnv1a64(b"foobar"), 0x8594_4171_f739_67e8);
32+
}
33+
#[test]
34+
fn the_fold_is_order_sensitive() {
35+
let a = fold_f64(fold_f64(0, 1.5), 2.5);
36+
let b = fold_f64(fold_f64(0, 2.5), 1.5);
37+
assert_ne!(a, b, "fold must depend on order; an XOR fold would not");
38+
}
39+
#[test]
40+
fn the_fold_depends_on_the_value() {
41+
assert_ne!(fold_f64(0, 1.5), fold_f64(0, 1.5000000000000002));
42+
}
43+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
#![allow(clippy::suboptimal_flops)]
2+
pub mod checksum;
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
[package]
2+
name = "fmw-wasm"
3+
version = "0.0.0"
4+
edition = "2021"
5+
publish = false
6+
[lib]
7+
crate-type = ["cdylib"]
8+
[dependencies]
9+
fmw-noise = { path = "../fmw-noise" }
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
//! The WASM boundary, and nothing else. No logic lives here.
2+
3+
use fmw_noise::checksum;
4+
5+
/// A fixed scratch region the caller writes into.
6+
///
7+
/// No allocator, and that is deliberate rather than minimal: the module's
8+
/// lifecycle is "instantiate once per worker, fill a buffer per call", so an
9+
/// allocator would exist only to hand back memory nothing ever frees. Phase 3
10+
/// replaces this with an explicitly reused render buffer of the same shape.
11+
const SCRATCH_BYTES: usize = 1 << 16;
12+
static mut SCRATCH: [u8; SCRATCH_BYTES] = [0; SCRATCH_BYTES];
13+
14+
#[unsafe(no_mangle)]
15+
pub extern "C" fn scratch_ptr() -> u32 {
16+
core::ptr::addr_of!(SCRATCH) as u32
17+
}
18+
19+
#[unsafe(no_mangle)]
20+
pub extern "C" fn scratch_len() -> u32 {
21+
SCRATCH_BYTES as u32
22+
}
23+
24+
/// FNV-1a 64 over `len` bytes at the start of the scratch region.
25+
///
26+
/// # Safety
27+
/// The caller must have written `len <= scratch_len()` bytes there.
28+
#[unsafe(no_mangle)]
29+
pub extern "C" fn fnv1a64(len: u32) -> u64 {
30+
let bytes = unsafe {
31+
core::slice::from_raw_parts(core::ptr::addr_of!(SCRATCH).cast::<u8>(), len as usize)
32+
};
33+
checksum::fnv1a64(bytes)
34+
}
35+
36+
#[unsafe(no_mangle)]
37+
pub extern "C" fn fold_f64(acc: u64, value: f64) -> u64 {
38+
checksum::fold_f64(acc, value)
39+
}

rust-toolchain.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
[toolchain]
2+
channel = "1.97.1"
3+
components = ["rustfmt", "clippy"]
4+
targets = ["wasm32-unknown-unknown"]
5+
profile = "minimal"

0 commit comments

Comments
 (0)