diff --git a/quest/m0/checks/README.md b/quest/m0/checks/README.md index 21bce22ff..b42eb97cc 100644 --- a/quest/m0/checks/README.md +++ b/quest/m0/checks/README.md @@ -18,7 +18,6 @@ Each child reports its CPU saved, cold and warm, against `origin/main`. ## Required -- [A cheaper differential](/quest/m0/checks/differential.md) - fixtures' wasm builds at opt-level 1 under single-threaded Node, and `stable_names` reuses `lang`'s translation - [Checks by the diff](/quest/m0/checks/dispatch.md) - `just ci` runs only the projects a change touches, as moq's `sh/dispatch.sh` does, with a nightly `just gate` - [A warm gate](/quest/m0/checks/cache.md) - the land gate builds through mbx, as moq's does - [Stable names on a small fixture](/quest/m0/checks/stable-names.md) - `stable_names` runs on a small fixture with generic instances and trait impls diff --git a/quest/m0/checks/differential.md b/quest/m0/checks/differential.md deleted file mode 100644 index fd88f5db2..000000000 --- a/quest/m0/checks/differential.md +++ /dev/null @@ -1,18 +0,0 @@ -# [S] A cheaper differential - -## Goal - -The ts differential fixtures run their native wasm32 side built at opt-level 1, under -`node --single-threaded`, and `stable_names` reuses the `lang` test's translation as its -base, with every case still compared. - -## Plan - -Measured by the 2026-11-01 checks audit on nixos2: `lang`'s differential spends about 590 of -its 872 CPU-seconds running a 212 MB debug wasm in V8. At opt-level 1 it takes 164, with 0 -differences across 709k cases. Overflow checks and debug assertions stay on, so panics and -overflow still match. Apply it to every differential fixture. `stable_names`'s base config -equals `lang`'s roots, so its first translation repeats the `lang` test's (about 150 -CPU-seconds). - -Public API: none. diff --git a/quest/m0/checks/stable-names.md b/quest/m0/checks/stable-names.md index 35feda6c7..3eb63a0da 100644 --- a/quest/m0/checks/stable-names.md +++ b/quest/m0/checks/stable-names.md @@ -9,7 +9,7 @@ has generic instances and trait impls, instead of translating all of `lang`. Decided with the maintainer on 2026-11-01: it caught 3 failures nothing else did, so it stays in the gate, but on a fixture that costs a fraction of `lang`'s (about 300 -CPU-seconds saved after [the reuse](/quest/m0/checks/differential.md)). Cover each naming +CPU-seconds saved now that it reuses `lang`'s translation). Cover each naming rule it relies on in `lang` today. Public API: none. diff --git a/ts/Cargo.toml b/ts/Cargo.toml index 063f12f3c..3909e04b8 100644 --- a/ts/Cargo.toml +++ b/ts/Cargo.toml @@ -40,6 +40,13 @@ toml = "1.1.8" [lints] workspace = true +# The differential tests' native side: optimized, since V8 runs a debug build's wasm several +# times slower, with dev's overflow checks and debug assertions, so its panics match the +# translation's. +[profile.differential] +inherits = "dev" +opt-level = 1 + # The size-optimized profile moq's WASM shipped with: the benchmark's WASM route. [profile.wasm-release] inherits = "release" diff --git a/ts/tests/fixtures.rs b/ts/tests/fixtures.rs index 2f0b2d825..b3ed2a2eb 100644 --- a/ts/tests/fixtures.rs +++ b/ts/tests/fixtures.rs @@ -5,9 +5,13 @@ use std::path::{Path, PathBuf}; use std::process::{Command, Output}; +use std::sync::OnceLock; const TARGET: &str = "wasm32-unknown-unknown"; +/// The native side's profile, from `Cargo.toml`. +const PROFILE: &str = "differential"; + fn manifest_dir() -> PathBuf { PathBuf::from(env!("CARGO_MANIFEST_DIR")) } @@ -161,6 +165,8 @@ fn differential(package: &str, build: &[&str], out: &Path, entry: &str, cases: u package, "--target", TARGET, + "--profile", + PROFILE, "--target-dir", ]) .arg(&target_dir) @@ -173,10 +179,13 @@ fn differential(package: &str, build: &[&str], out: &Path, entry: &str, cases: u assert!(status.success(), "building {package} for {TARGET} failed"); let wasm = target_dir .join(TARGET) - .join("debug") + .join(PROFILE) .join(format!("{}.wasm", package.replace('-', "_"))); std::fs::write(out.join("package.json"), "{ \"type\": \"module\" }\n").unwrap(); + // One thread: the cases run one at a time, and V8's background compilers and + // collectors would spend more CPU than they save. let output = Command::new("node") + .arg("--single-threaded") .arg(manifest_dir().join("tests").join("differential.ts")) .arg(&wasm) .arg(out.join(entry)) @@ -284,50 +293,44 @@ fn shake(out: &Path) { /// text and order, though translation reaches it in another order. #[test] fn source_order() { - added_root( - "codec", - r#"roots = ["harness::run", "bench::run"]"#, - r#"roots = ["harness::run"]"#, - // First, so that translation reaches what both share in another order. - r#"roots = ["bench::run", "harness::run"]"#, - ); + let roots = r#"roots = ["harness::run", "bench::run"]"#; + let base = rooted("codec", roots, "base", r#"roots = ["harness::run"]"#); + // First, so that translation reaches what both share in another order. + let added = r#"roots = ["bench::run", "harness::run"]"#; + kept(&base, &rooted("codec", roots, "added", added), added); } /// Adding a root that reaches another instance of a generic function, or another trait's /// impl of a method, renames nothing the old roots reach. #[test] fn stable_names() { + // `lang`'s own roots, so its translation is the base. let roots = r#"roots = ["harness::run", "harness::op", "fmt::Fault"]"#; - added_root( - "lang", - roots, - roots, - r#"roots = ["fmt::added", "harness::run", "harness::op", "fmt::Fault"]"#, - ); + let added = r#"roots = ["fmt::added", "harness::run", "harness::op", "fmt::Fault"]"#; + kept(&sources(lang_out()), &rooted("lang", roots, "added", added), added); } -/// Translate fixture `name` with its config's `roots` line replaced by `base`, then by -/// `added`, and check that every line of the first output stays, in order, in the second. -fn added_root(name: &str, roots: &str, base: &str, added: &str) { +/// Translate fixture `name` into `file` with its config's `roots` line replaced by `with`. +fn rooted(name: &str, roots: &str, file: &str, with: &str) -> Vec<(PathBuf, String)> { let config = std::fs::read_to_string(fixture(name).join("ts.toml")).unwrap(); assert!(config.contains(roots), "{name}'s roots changed: update this test"); let dir = scratch().join(name).join("order"); std::fs::create_dir_all(&dir).unwrap(); - let run = |file: &str, with: &str| { - let path = dir.join(format!("{file}.toml")); - std::fs::write(&path, config.replace(roots, with)).unwrap(); - let out = dir.join(file); - let output = driver(name, &path, &["--out".as_ref(), out.as_ref()], "order"); - assert!( - output.status.success(), - "translating {name} with {with} failed:\n{}", - String::from_utf8_lossy(&output.stderr) - ); - sources(&out) - }; - let base = run("base", base); - let more = run("added", added); - for (rel, text) in &base { + let path = dir.join(format!("{file}.toml")); + std::fs::write(&path, config.replace(roots, with)).unwrap(); + let out = dir.join(file); + let output = driver(name, &path, &["--out".as_ref(), out.as_ref()], "order"); + assert!( + output.status.success(), + "translating {name} with {with} failed:\n{}", + String::from_utf8_lossy(&output.stderr) + ); + sources(&out) +} + +/// Check that every line of `base` stays, in order, in `more`, its translation once `added`. +fn kept(base: &[(PathBuf, String)], more: &[(PathBuf, String)], added: &str) { + for (rel, text) in base { let more = more.iter().find(|(r, _)| r == rel).map(|(_, text)| text); let more = more.unwrap_or_else(|| panic!("{} is gone once {added}", rel.display())); let mut rest = lines(more).into_iter(); @@ -358,9 +361,15 @@ fn lines(text: &str) -> Vec { #[test] fn lang() { - let out = translated("lang"); - snapshot("lang", &out); - lang_differential(&out); + let out = lang_out(); + snapshot("lang", out); + lang_differential(out); +} + +/// `lang`'s translation, which `stable_names` shares rather than translating it again. +fn lang_out() -> &'static Path { + static OUT: OnceLock = OnceLock::new(); + OUT.get_or_init(|| translated("lang")) } fn lang_differential(out: &Path) { @@ -630,7 +639,7 @@ fn sysroot() { /// Selects the fixture's own getrandom backend, which `rand::rng()` seeds from natively: /// wasm32 has no OS to read. Only getrandom's flags change, so nothing else rebuilds. -const GETRANDOM: &str = r#"profile.dev.package.getrandom.rustflags=["--cfg", 'getrandom_backend="custom"']"#; +const GETRANDOM: &str = r#"profile.differential.package.getrandom.rustflags=["--cfg", 'getrandom_backend="custom"']"#; /// `bytes`, `rand`, and `tracing`, which translation maps without a binding or config: /// it picks getrandom's backend itself, as a browser's binary would.