From dae21406d192e1dfe3eae549460cdabf67793298 Mon Sep 17 00:00:00 2001 From: Till Schneidereit Date: Tue, 12 May 2026 18:37:00 +0200 Subject: [PATCH 1/4] [ci] Optimizations to reduce critical-path length This commit introduces three different optimizations: 1. only run `cargo check [..]` with MSRV instead of the whole test suite, based on the assumption that all possible failures would be identified statically 2. Reduce work done for MPK builds 3. shard the `wasmtime-cli` job into three buckets to run in parallel, at the cost of rebuilding the binaries multiple times We'll see how kind the real world is to the theoretical analysis here. --- .github/workflows/main.yml | 43 ++++++++++++- ci/build-test-matrix.js | 121 ++++++++++++++++++++++++++++--------- 2 files changed, 134 insertions(+), 30 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 510965459e95..fc36c502d8eb 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -493,6 +493,36 @@ jobs: - run: rustup component add clippy - run: cargo clippy --workspace --all-targets --features p3,component-model-async + # Verify that the workspace compiles cleanly under the MSRV toolchain. This + # job replaces a previous `Test MSRV` matrix entry which ran the full test + # suite under MSRV; running tests adds no coverage beyond running them on + # current stable on the same target, while `cargo check` catches the + # behavior we actually care about: "did we use a Rust feature too new for + # our MSRV?". + # + # The excludes mirror `ci/run-tests.py` so that crates which require + # nightly toolchains or external dependencies (OCaml, SMT solvers) are + # skipped. + msrv_check: + name: MSRV check + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + with: + submodules: true + - uses: ./.github/actions/install-rust + with: + toolchain: msrv + - run: | + cargo check --workspace --all-targets --all-features --locked \ + --exclude test-programs \ + --exclude wasmtime-wasi-nn \ + --exclude wasmtime-wasi-tls \ + --exclude wasmtime-fuzzing \ + --exclude wasm-spec-interpreter \ + --exclude veri_engine \ + --exclude calculator + # Similar to `micro_checks` but where we need to install some more state # (e.g. Android NDK) and we haven't factored support for those things out into # a parallel jobs yet. @@ -930,14 +960,22 @@ jobs: # Since MPK (PKU) is not present on some GitHub runners, we check if it is # available before force-enabling it. This occasional testing is better than # none at all; ideally we would test in a system-mode QEMU VM. + # + # We probe via /proc/cpuinfo rather than `cargo run --example + # mpk-available` because the latter previously cost ~3.5 minutes by + # building wasmtime with a feature set that did not match the subsequent + # `cargo test --all-features` step, forcing a rebuild. The conditions + # checked here mirror what `wasmtime::vm::mpk::is_supported` does on + # Linux x86_64: Intel CPU with the PKU CPUID bit set. - name: Force-run with MPK enabled, if available if: ${{ contains(matrix.name, 'MPK') }} run: | - if cargo run --example mpk-available; then + if grep -q '^vendor_id.*GenuineIntel' /proc/cpuinfo \ + && grep -qE '^flags.*\bpku\b' /proc/cpuinfo; then echo "::notice::This CI run will force-enable MPK; this ensures tests conditioned with the \`WASMTIME_TEST_FORCE_MPK\` environment variable will run with MPK-protected memory pool stripes." echo WASMTIME_TEST_FORCE_MPK=1 >> $GITHUB_ENV else - echo "::warning::This CI run will not test MPK; it has been detected as not available on this machine (\`cargo run --example mpk-available\`)." + echo "::warning::This CI run will not test MPK; it has been detected as not available on this machine (no Intel CPU with PKU flag in /proc/cpuinfo)." fi # Install VTune, see `cli_tests::profile_with_vtune`. @@ -1387,6 +1425,7 @@ jobs: - special_tests - test_wasi_tls - clippy + - msrv_check - monolith_checks - platform_checks - bench diff --git a/ci/build-test-matrix.js b/ci/build-test-matrix.js index 4869be377dcc..bd37e596b625 100644 --- a/ci/build-test-matrix.js +++ b/ci/build-test-matrix.js @@ -13,7 +13,35 @@ const GENERIC_BUCKETS = 3; // Crates which are their own buckets. These are the very slowest to // compile-and-test crates. -const SINGLE_CRATE_BUCKETS = ["wasmtime", "wasmtime-cli", "wasmtime-wasi"]; +// +// An entry may be a string (the crate name, producing one bucket) or an +// object `{ crate, sub: [{ name, args }, ...] }` to split a single crate's +// tests across multiple buckets. Sub-bucketing is reserved for crates whose +// integration tests dominate the wall clock and split cleanly along +// `--test` boundaries; the build of the crate's dependencies and test +// binaries is duplicated across the sub-buckets, but the test-execution +// portion (the longer half on slow targets like QEMU) parallelizes. +const SINGLE_CRATE_BUCKETS = [ + "wasmtime", + { + "crate": "wasmtime-cli", + "sub": [ + // The two largest integration suites; each gets its own bucket. + { "name": "all", "args": "--test all" }, + { "name": "wast", "args": "--test wast" }, + // Everything else in the crate: library, binaries, and the smaller + // integration tests. Cargo doesn't have an "exclude test" flag, so the + // remaining `--test` targets are enumerated explicitly. + { "name": "other", "args": "--lib --bins --test disable_host_trap_handlers --test disas --test rlimited-memory --test wasi" }, + ], + }, + "wasmtime-wasi", +]; + +// Helper: get just the crate name from a SINGLE_CRATE_BUCKETS entry. +function singleBucketCrateName(entry) { + return typeof entry === "string" ? entry : entry.crate; +} const ubuntu = 'ubuntu-24.04'; const windows = 'windows-2025'; @@ -57,22 +85,29 @@ const FAST_MATRIX = [ // * `sde` - if `true`, indicates this test should use Intel SDE for instruction // emulation. SDE will be set up and configured as the test runner. // +// * `crates` - if a string, this config is not sharded across the workspace +// and instead runs against only the named crate. If an array of strings, +// the config produces one job per named crate (each job's name suffixed +// with the crate name) and skips the generic buckets entirely. Used to +// restrict env-var-toggled test variants to only the crates that observe +// that env var (e.g. MPK). +// // * `rust` - the Rust version to install, and if unset this'll be set to // `default` const FULL_MATRIX = [ ...FAST_MATRIX, { - "name": "Test MSRV", - "os": ubuntu, - "filter": "linux-x64", - "isa": "x64", - "rust": "msrv", - }, - { + // MPK is only observed at test time by code that reads + // WASMTIME_TEST_FORCE_MPK (the `wasmtime` runtime crate and the + // root-level integration tests under `wasmtime-cli`). Restricting MPK + // testing to those two crates eliminates four redundant shards + // (3 generic + wasmtime-wasi) that produce identical results to + // `Test Linux x86_64`. "name": "Test MPK", "os": ubuntu, "filter": "linux-x64", - "isa": "x64" + "isa": "x64", + "crates": ["wasmtime", "wasmtime-cli"], }, { "name": "Test ASAN", @@ -219,40 +254,72 @@ async function shard(configs) { // Divide the workspace crates into N disjoint subsets. Crates that are // particularly expensive to compile and test form their own singleton subset. + // A bucket is either a Set of crate names (used as-is for `cargo test + // --workspace --exclude ...`) or an object `{ crate, sub }` describing a + // single-crate bucket that should be further split by `--test` filter. + const singleBucketCrateNames = new Set(SINGLE_CRATE_BUCKETS.map(singleBucketCrateName)); const buckets = Array.from({ length: GENERIC_BUCKETS }, _ => new Set()); let i = 0; for (const crate of members) { - if (SINGLE_CRATE_BUCKETS.indexOf(crate) != -1) continue; + if (singleBucketCrateNames.has(crate)) continue; buckets[i].add(crate); i = (i + 1) % GENERIC_BUCKETS; } - for (crate of SINGLE_CRATE_BUCKETS) { - buckets.push(new Set([crate])); + for (const entry of SINGLE_CRATE_BUCKETS) { + if (typeof entry === "string") { + buckets.push(new Set([entry])); + } else { + // A crate with sub-buckets: push one bucket per `sub` entry, retaining + // the crate name and extra-args for naming and bucket-arg expansion. + for (const sub of entry.sub) { + buckets.push({ crate: entry.crate, name: sub.name, args: sub.args }); + } + } } // For each config, expand it into N configs, one for each disjoint set we // created above. const sharded = []; for (const config of configs) { - // If crates is specified, don't shard, just use the specified crates + // If `crates` is specified, don't shard against the generic buckets. + // A string value produces a single job for that crate; an array value + // produces one job per crate with the crate name appended to `name`. if (config.crates) { - sharded.push(Object.assign( - {}, - config, - { - bucket: members - .map(c => c === config.crates ? `--package ${c}` : `--exclude ${c}`) - .join(" ") - } - )); + const cratesList = Array.isArray(config.crates) ? config.crates : [config.crates]; + const useSuffix = Array.isArray(config.crates); + for (const crate of cratesList) { + sharded.push(Object.assign( + {}, + config, + { + name: useSuffix ? `${config.name} (${crate})` : config.name, + bucket: members + .map(c => c === crate ? `--package ${c}` : `--exclude ${c}`) + .join(" "), + } + )); + } continue; } let nbucket = 1; for (const bucket of buckets) { - let bucket_name = `${nbucket}/${buckets.length}`; - if (bucket.size === 1) - bucket_name = Array.from(bucket)[0]; + let bucket_name; + let bucket_args; + if (bucket instanceof Set) { + bucket_name = bucket.size === 1 + ? Array.from(bucket)[0] + : `${nbucket}/${buckets.length}`; + bucket_args = members + .map(c => bucket.has(c) ? `--package ${c}` : `--exclude ${c}`) + .join(" "); + } else { + // Sub-bucket of a single crate. + bucket_name = `${bucket.crate}-${bucket.name}`; + bucket_args = members + .map(c => c === bucket.crate ? `--package ${c}` : `--exclude ${c}`) + .join(" ") + " " + bucket.args; + } sharded.push(Object.assign( {}, @@ -262,9 +329,7 @@ async function shard(configs) { // We run tests via `cargo test --workspace`, so exclude crates that // aren't in this bucket, rather than naming only the crates that are // in this bucket. - bucket: members - .map(c => bucket.has(c) ? `--package ${c}` : `--exclude ${c}`) - .join(" "), + bucket: bucket_args, } )); nbucket += 1; From 4804bc4521e5408e7bbc835b1b9d16b0d4222338 Mon Sep 17 00:00:00 2001 From: Till Schneidereit Date: Tue, 12 May 2026 19:01:38 +0200 Subject: [PATCH 2/4] Exclude preview1 adapter from MSRV check --- .github/workflows/main.yml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index fc36c502d8eb..6c8d19ac37a5 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -514,6 +514,12 @@ jobs: with: toolchain: msrv - run: | + # wasi-preview1-component-adapter has mutually-exclusive features + # (command/reactor/proxy) that compile-error under --all-features. + # It's a wasm32-only artifact built separately by + # ci/build-wasi-preview1-component-adapter.sh against + # wasm32-unknown-unknown, not user-facing on the host toolchain, + # so MSRV doesn't apply. cargo check --workspace --all-targets --all-features --locked \ --exclude test-programs \ --exclude wasmtime-wasi-nn \ @@ -521,7 +527,8 @@ jobs: --exclude wasmtime-fuzzing \ --exclude wasm-spec-interpreter \ --exclude veri_engine \ - --exclude calculator + --exclude calculator \ + --exclude wasi-preview1-component-adapter # Similar to `micro_checks` but where we need to install some more state # (e.g. Android NDK) and we haven't factored support for those things out into From 49f68be5b1f3559336d663c0419d550f4b6736db Mon Sep 17 00:00:00 2001 From: Till Schneidereit Date: Wed, 13 May 2026 15:12:22 +0200 Subject: [PATCH 3/4] Address review feedback Most importantly, change the MPK availability check to use the same mechanism Wasmtime itself uses, to prevent potential future drift. prtest:full --- .github/workflows/main.yml | 7 +-- crates/core/examples/mpk-available.rs | 17 +++++++ crates/core/src/lib.rs | 1 + crates/core/src/mpk.rs | 44 +++++++++++++++++++ crates/wasmtime/src/runtime/vm/mpk/enabled.rs | 2 +- crates/wasmtime/src/runtime/vm/mpk/pkru.rs | 26 ----------- 6 files changed, 64 insertions(+), 33 deletions(-) create mode 100644 crates/core/examples/mpk-available.rs create mode 100644 crates/core/src/mpk.rs diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 6c8d19ac37a5..0abd0dca32f0 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -493,12 +493,7 @@ jobs: - run: rustup component add clippy - run: cargo clippy --workspace --all-targets --features p3,component-model-async - # Verify that the workspace compiles cleanly under the MSRV toolchain. This - # job replaces a previous `Test MSRV` matrix entry which ran the full test - # suite under MSRV; running tests adds no coverage beyond running them on - # current stable on the same target, while `cargo check` catches the - # behavior we actually care about: "did we use a Rust feature too new for - # our MSRV?". + # Verify that the workspace compiles cleanly under the MSRV toolchain. # # The excludes mirror `ci/run-tests.py` so that crates which require # nightly toolchains or external dependencies (OCaml, SMT solvers) are diff --git a/crates/core/examples/mpk-available.rs b/crates/core/examples/mpk-available.rs new file mode 100644 index 000000000000..934f920691cb --- /dev/null +++ b/crates/core/examples/mpk-available.rs @@ -0,0 +1,17 @@ +//! Exit 0 if Wasmtime's MPK runtime support is available on this host, +//! 1 otherwise. This shares the detection logic with `wasmtime`'s runtime +//! `is_supported()` check via [`wasmtime_internal_core::mpk::is_supported`], +//! so CI's "should we set `WASMTIME_TEST_FORCE_MPK=1`?" decision can never +//! drift from what the runtime itself reports. + +use std::process::exit; + +fn main() { + if wasmtime_internal_core::mpk::is_supported() { + eprintln!("MPK is available"); + exit(0); + } else { + eprintln!("MPK is not available"); + exit(1); + } +} diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs index 0e55cba034bb..46ab22b02b5c 100644 --- a/crates/core/src/lib.rs +++ b/crates/core/src/lib.rs @@ -21,6 +21,7 @@ extern crate std; pub mod alloc; pub mod error; pub mod math; +pub mod mpk; pub mod non_max; pub mod slab; pub mod undo; diff --git a/crates/core/src/mpk.rs b/crates/core/src/mpk.rs new file mode 100644 index 000000000000..07e572195431 --- /dev/null +++ b/crates/core/src/mpk.rs @@ -0,0 +1,44 @@ +//! Detection of Memory Protection Keys (MPK) support on the host system. +//! +//! This is the single source of truth for whether Wasmtime's MPK +//! implementation can be used on the current target. The runtime in the +//! `wasmtime` crate consults [`is_supported`], and `examples/mpk-available.rs` +//! exposes it as a CLI exit code so CI can decide whether to set +//! `WASMTIME_TEST_FORCE_MPK=1`. + +/// Returns `true` if Wasmtime's MPK support can be used on this host. +pub fn is_supported() -> bool { + cfg!(target_os = "linux") && cpuid_pku_bit_set() +} + +/// Check the `ECX.PKU` flag (bit 3, zero-based) of the `07h` `CPUID` leaf; see +/// the Intel Software Development Manual, vol 3a, section 2.7. This flag is +/// only set on Intel CPUs, so this function also checks the `CPUID` vendor +/// string. +#[cfg(target_arch = "x86_64")] +fn cpuid_pku_bit_set() -> bool { + is_intel_cpu() && { + #[allow( + unused_unsafe, + reason = "rust is transitioning to `__cpuid` being a safe function" + )] + let result = unsafe { core::arch::x86_64::__cpuid(0x07) }; + (result.ecx & 0b1000) != 0 + } +} + +#[cfg(not(target_arch = "x86_64"))] +fn cpuid_pku_bit_set() -> bool { + false +} + +/// Check the `CPUID` vendor string for `GenuineIntel`; see the Intel Software +/// Development Manual, vol 2a, `CPUID` description. +#[cfg(target_arch = "x86_64")] +fn is_intel_cpu() -> bool { + #[allow(unused_unsafe, reason = "see above about __cpuid")] + let result = unsafe { core::arch::x86_64::__cpuid(0) }; + result.ebx == u32::from_le_bytes(*b"Genu") + && result.edx == u32::from_le_bytes(*b"ineI") + && result.ecx == u32::from_le_bytes(*b"ntel") +} diff --git a/crates/wasmtime/src/runtime/vm/mpk/enabled.rs b/crates/wasmtime/src/runtime/vm/mpk/enabled.rs index f4844a68d821..14ddba4d628f 100644 --- a/crates/wasmtime/src/runtime/vm/mpk/enabled.rs +++ b/crates/wasmtime/src/runtime/vm/mpk/enabled.rs @@ -6,7 +6,7 @@ use std::sync::OnceLock; /// Check if the MPK feature is supported. pub fn is_supported() -> bool { - cfg!(target_os = "linux") && cfg!(target_arch = "x86_64") && pkru::has_cpuid_bit_set() + wasmtime_core::mpk::is_supported() } /// Allocate up to `max` protection keys. diff --git a/crates/wasmtime/src/runtime/vm/mpk/pkru.rs b/crates/wasmtime/src/runtime/vm/mpk/pkru.rs index df901eb70225..50f1f69b1d47 100644 --- a/crates/wasmtime/src/runtime/vm/mpk/pkru.rs +++ b/crates/wasmtime/src/runtime/vm/mpk/pkru.rs @@ -55,32 +55,6 @@ pub fn write(pkru: u32) { } } -/// Check the `ECX.PKU` flag (bit 3, zero-based) of the `07h` `CPUID` leaf; see -/// the Intel Software Development Manual, vol 3a, section 2.7. This flag is -/// only set on Intel CPUs, so this function also checks the `CPUID` vendor -/// string. -pub fn has_cpuid_bit_set() -> bool { - #[allow( - unused_unsafe, - reason = "rust is transitioning to `__cpuid` being a safe function" - )] - let result = unsafe { core::arch::x86_64::__cpuid(0x07) }; - is_intel_cpu() && (result.ecx & 0b1000) != 0 -} - -/// Check the `CPUID` vendor string for `GenuineIntel`; see the Intel Software -/// Development Manual, vol 2a, `CPUID` description. -pub fn is_intel_cpu() -> bool { - // To read the CPU vendor string, we pass 0 in EAX and read 12 ASCII bytes - // from EBX, EDX, and ECX (in that order). - #[allow(unused_unsafe, reason = "see above about __cpuid")] - let result = unsafe { core::arch::x86_64::__cpuid(0) }; - // Then we check if the vendor string matches "GenuineIntel". - result.ebx == u32::from_le_bytes(*b"Genu") - && result.edx == u32::from_le_bytes(*b"ineI") - && result.ecx == u32::from_le_bytes(*b"ntel") -} - #[cfg(test)] mod tests { use super::*; From 1b13f147e136b814cb858d10281c0dd32e0b428e Mon Sep 17 00:00:00 2001 From: Till Schneidereit Date: Wed, 13 May 2026 17:49:27 +0200 Subject: [PATCH 4/4] Apply MPK check updates to workflow --- .github/workflows/main.yml | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 0abd0dca32f0..21079302a4d4 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -962,22 +962,14 @@ jobs: # Since MPK (PKU) is not present on some GitHub runners, we check if it is # available before force-enabling it. This occasional testing is better than # none at all; ideally we would test in a system-mode QEMU VM. - # - # We probe via /proc/cpuinfo rather than `cargo run --example - # mpk-available` because the latter previously cost ~3.5 minutes by - # building wasmtime with a feature set that did not match the subsequent - # `cargo test --all-features` step, forcing a rebuild. The conditions - # checked here mirror what `wasmtime::vm::mpk::is_supported` does on - # Linux x86_64: Intel CPU with the PKU CPUID bit set. - name: Force-run with MPK enabled, if available if: ${{ contains(matrix.name, 'MPK') }} run: | - if grep -q '^vendor_id.*GenuineIntel' /proc/cpuinfo \ - && grep -qE '^flags.*\bpku\b' /proc/cpuinfo; then + if cargo run -q -p wasmtime-internal-core --example mpk-available; then echo "::notice::This CI run will force-enable MPK; this ensures tests conditioned with the \`WASMTIME_TEST_FORCE_MPK\` environment variable will run with MPK-protected memory pool stripes." echo WASMTIME_TEST_FORCE_MPK=1 >> $GITHUB_ENV else - echo "::warning::This CI run will not test MPK; it has been detected as not available on this machine (no Intel CPU with PKU flag in /proc/cpuinfo)." + echo "::warning::This CI run will not test MPK; it has been detected as not available on this machine." fi # Install VTune, see `cli_tests::profile_with_vtune`.