From a6c907dd129de1d2afb253fa701aedbb84277d24 Mon Sep 17 00:00:00 2001 From: Jorge Prendes Date: Mon, 28 Sep 2026 16:17:52 +0100 Subject: [PATCH 1/4] test: shrink virtqueue segment tests under Miri Miri interprets every allocation, so tests that build thousands of `Bytes` dominate the suite. Four of them take 40% of its runtime, three of those over four minutes each. Under Miri these use 64 segments, which still exceeds the SmallVec inline capacity of 4. The backing store spills, so the assertions about inline suffixes and in-place remainders stay meaningful. Native runs use the full sizes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: Jorge Prendes --- src/hyperlight_common/src/transport.rs | 9 ++++-- src/hyperlight_common/src/virtq/buffer.rs | 34 +++++++++++++++++------ 2 files changed, 32 insertions(+), 11 deletions(-) diff --git a/src/hyperlight_common/src/transport.rs b/src/hyperlight_common/src/transport.rs index c0a6aed29..8e8cd46f1 100644 --- a/src/hyperlight_common/src/transport.rs +++ b/src/hyperlight_common/src/transport.rs @@ -565,11 +565,16 @@ mod tests { #[test] fn segments_source_reads_fragmented_values_in_order() { + #[cfg(miri)] + let count = 64; + #[cfg(not(miri))] + let count = 1024; + let mut source = Segments::new( - (0..1024).flat_map(|_| [Bytes::from_static(b"ab"), Bytes::from_static(b"cd")]), + (0..count).flat_map(|_| [Bytes::from_static(b"ab"), Bytes::from_static(b"cd")]), ); - for _ in 0..1024 { + for _ in 0..count { assert_eq!(source.take_bytes(1).unwrap(), b"a"); assert_eq!( source.take_chunks(2).unwrap(), diff --git a/src/hyperlight_common/src/virtq/buffer.rs b/src/hyperlight_common/src/virtq/buffer.rs index 7fc82b473..c66fe80d0 100644 --- a/src/hyperlight_common/src/virtq/buffer.rs +++ b/src/hyperlight_common/src/virtq/buffer.rs @@ -459,8 +459,13 @@ mod tests { #[test] fn segments_split_to_keeps_small_suffix_inline() { - let mut segments = Segments::new((0..8192).map(|_| Bytes::from_static(b"x"))); - drop(segments.split_to(8191).unwrap()); + #[cfg(miri)] + let count = 64; + #[cfg(not(miri))] + let count = 8192; + + let mut segments = Segments::new((0..count).map(|_| Bytes::from_static(b"x"))); + drop(segments.split_to(count - 1).unwrap()); let suffix = segments.split_to(1).unwrap(); @@ -473,23 +478,34 @@ mod tests { #[test] fn segments_split_to_keeps_large_remainder_in_place() { - let mut segments = Segments::new((0..8192).map(|_| Bytes::from_static(b"x"))); + #[cfg(miri)] + let count = 64; + #[cfg(not(miri))] + let count = 8192; + let half = count / 2; + + let mut segments = Segments::new((0..count).map(|_| Bytes::from_static(b"x"))); let storage = segments.chunks.as_ptr(); - let prefix = segments.split_to(4096).unwrap(); + let prefix = segments.split_to(half).unwrap(); - assert_eq!(prefix.segment_count(), 4096); + assert_eq!(prefix.segment_count(), half); assert!(prefix.iter().all(|segment| segment.as_ref() == b"x")); - assert_eq!(segments.segment_count(), 4096); - assert_eq!(segments.as_slice().as_ptr(), storage.wrapping_add(4096)); + assert_eq!(segments.segment_count(), half); + assert_eq!(segments.as_slice().as_ptr(), storage.wrapping_add(half)); assert!(segments.chunks[..segments.head].iter().all(Bytes::is_empty)); } #[test] fn segments_split_to_repeatedly_consumes_fragments() { - let mut segments = Segments::new((0..4096).map(|_| Bytes::from_static(b"ab"))); + #[cfg(miri)] + let count = 64; + #[cfg(not(miri))] + let count = 4096; + + let mut segments = Segments::new((0..count).map(|_| Bytes::from_static(b"ab"))); - for _ in 0..4096 { + for _ in 0..count { assert_eq!(segments.split_to(1).unwrap().into_bytes().as_ref(), b"a"); assert_eq!(segments.split_to(1).unwrap().into_bytes().as_ref(), b"b"); } From ab623c98d15b1b6b2518dedc0e269c597fe3a640 Mon Sep 17 00:00:00 2001 From: Jorge Prendes Date: Mon, 28 Sep 2026 16:17:58 +0100 Subject: [PATCH 2/4] ci: run Miri tests under nextest A Miri interpreter is single-threaded, so libtest's --test-threads only interleaves emulated threads onto one core. Nextest spawns one Miri process per test, which spreads them across cores. MIRI_JOBS sets the process count and defaults to one per core. Memory grows with it at roughly 0.4GB per job. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: Jorge Prendes --- .github/workflows/RustNightly.yml | 5 +++++ .github/workflows/dep_build_test.yml | 8 ++++++++ Justfile | 16 +++++++++++++--- 3 files changed, 26 insertions(+), 3 deletions(-) diff --git a/.github/workflows/RustNightly.yml b/.github/workflows/RustNightly.yml index a3ae9c55f..19a1933d9 100644 --- a/.github/workflows/RustNightly.yml +++ b/.github/workflows/RustNightly.yml @@ -92,6 +92,11 @@ jobs: - name: Build run: just build ${{ matrix.config }} + - name: Install cargo-nextest + uses: taiki-e/install-action@4cef1412cce204788f482e778a0b9187f9626a29 # v2.87.21 + with: + tool: cargo-nextest + - name: Run Miri tests run: env -u RUSTC_WRAPPER just miri-tests diff --git a/.github/workflows/dep_build_test.yml b/.github/workflows/dep_build_test.yml index 95d4c3928..efde2af01 100644 --- a/.github/workflows/dep_build_test.yml +++ b/.github/workflows/dep_build_test.yml @@ -113,6 +113,14 @@ jobs: - name: Build run: just build ${{ inputs.config }} + - name: Install cargo-nextest + # Same condition as the Miri step below, so the download is skipped + # wherever Miri does not run. + if: runner.os == 'Linux' && (inputs.arch == 'X64' || inputs.full_aarch64 == 'true') + uses: taiki-e/install-action@4cef1412cce204788f482e778a0b9187f9626a29 # v2.87.21 + with: + tool: cargo-nextest + - name: Run Miri tests # Linux-only. Skipped on aarch64 PRs to conserve the limited runners; # the daily schedule (full_aarch64 == 'true') restores it. On X64 Linux diff --git a/Justfile b/Justfile index 577e24b8b..55fb3a75b 100644 --- a/Justfile +++ b/Justfile @@ -313,11 +313,21 @@ test-rust-tracing target=default-target features="": test-doc target=default-target features="": {{ cargo-cmd }} test --profile={{ if target == "debug" { "dev" } else { target } }} {{ target-triple-flag }} {{ if features =="" {''} else { "--features " + features } }} --doc -miri-tests: +# Nextest runs one Miri process per job, each with its own interpreter, so +# memory grows with the job count at roughly 0.4GB per job. Lower MIRI_JOBS on +# a machine with many cores but little RAM. +miri-jobs := env('MIRI_JOBS', num_cpus()) + +ensure-cargo-nextest: + {{ if os() == "windows" { "if (-not (cargo nextest --version 2>$null)) { cargo install --locked cargo-nextest }" } else { "cargo nextest --version >/dev/null 2>&1 || cargo install --locked cargo-nextest" } }} + +miri-tests: (ensure-cargo-nextest) + @# A Miri interpreter is single-threaded, so libtest's --test-threads cannot + @# use more than one core. Nextest spawns one Miri process per test. rustup +nightly component list | grep -q "miri.*installed" || rustup component add miri --toolchain nightly # We can add more as needed - cargo +nightly miri test -p hyperlight-common -F trace_guest - cargo +nightly miri test -p hyperlight-host --lib -- mem::shared_mem::tests + cargo +nightly miri nextest run -p hyperlight-common -F trace_guest -j {{miri-jobs}} + cargo +nightly miri nextest run -p hyperlight-host --lib -E 'test(/mem::shared_mem::tests/)' -j {{miri-jobs}} ################ ### LINTING #### From e06f3fe044ff8966212df38615173c9d19ce2bca Mon Sep 17 00:00:00 2001 From: Jorge Prendes Date: Mon, 28 Sep 2026 16:31:41 +0100 Subject: [PATCH 3/4] ci: run Miri on aarch64 pull requests Miri takes about 90 seconds, cheap enough for every pull request even on the limited aarch64 runners. Architecture is a real axis for Miri, which interprets code for a specific target. arch/aarch64 carries no tests of its own yet, so an aarch64 run covers a subset of the x86_64 one until some land. full_aarch64 governs only the single-driver tests. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: Jorge Prendes --- .github/workflows/DailyArm64.yml | 6 +++--- .github/workflows/dep_build_test.yml | 21 +++++++++------------ 2 files changed, 12 insertions(+), 15 deletions(-) diff --git a/.github/workflows/DailyArm64.yml b/.github/workflows/DailyArm64.yml index 14e4d80f4..53f3f256f 100644 --- a/.github/workflows/DailyArm64.yml +++ b/.github/workflows/DailyArm64.yml @@ -4,8 +4,8 @@ name: Daily aarch64 # Full aarch64 coverage runs here on a schedule rather than on every PR, because # there are only limited aarch64 runners. PRs run a reduced aarch64 -# build-test (build + default Rust tests only) and skip aarch64 examples; this -# workflow restores Miri, the single-driver tests, and the examples once a day. +# build-test (build, Miri and default Rust tests) and skip aarch64 examples; +# this workflow restores the single-driver tests and the examples once a day. on: schedule: @@ -37,7 +37,7 @@ jobs: arch: arm64 config: ${{ matrix.config }} - # Full aarch64 build-and-test. full_aarch64: "true" re-enables the Miri and + # Full aarch64 build-and-test. full_aarch64: "true" re-enables the # single-driver steps that are skipped on PRs. build-test: needs: build-guests diff --git a/.github/workflows/dep_build_test.yml b/.github/workflows/dep_build_test.yml index efde2af01..77bc8dfcf 100644 --- a/.github/workflows/dep_build_test.yml +++ b/.github/workflows/dep_build_test.yml @@ -28,11 +28,11 @@ on: type: string full_aarch64: description: >- - When "true", also run the Miri and single-driver tests on aarch64. - PRs leave this "false" so the aarch64 runner only builds and runs the - default Rust tests; the daily schedule (DailyArm64.yml) sets it "true". - This input only affects aarch64: it does not change X64, where those - tests already run on Linux and are skipped on Windows regardless. + When "true", also run the single-driver tests on aarch64. PRs leave + this "false" so the aarch64 runner only builds and runs the default + Rust tests; the daily schedule (DailyArm64.yml) sets it "true". This + input only affects aarch64: it does not change X64, where those tests + already run on Linux and are skipped on Windows regardless. required: false type: string default: "false" @@ -114,18 +114,15 @@ jobs: run: just build ${{ inputs.config }} - name: Install cargo-nextest - # Same condition as the Miri step below, so the download is skipped - # wherever Miri does not run. - if: runner.os == 'Linux' && (inputs.arch == 'X64' || inputs.full_aarch64 == 'true') + if: runner.os == 'Linux' uses: taiki-e/install-action@4cef1412cce204788f482e778a0b9187f9626a29 # v2.87.21 with: tool: cargo-nextest - name: Run Miri tests - # Linux-only. Skipped on aarch64 PRs to conserve the limited runners; - # the daily schedule (full_aarch64 == 'true') restores it. On X64 Linux - # it always runs. - if: runner.os == 'Linux' && (inputs.arch == 'X64' || inputs.full_aarch64 == 'true') + # Linux on both architectures. arch/aarch64 carries no tests of its own + # yet, so aarch64 covers a subset until some land. + if: runner.os == 'Linux' run: env -u RUSTC_WRAPPER just miri-tests - name: Run Rust tests From 5ad1ddff672f3ee568697e8cd61b08310582eea6 Mon Sep 17 00:00:00 2001 From: Jorge Prendes Date: Mon, 28 Sep 2026 16:55:13 +0100 Subject: [PATCH 4/4] Update aarch64 test description in workflow Clarified description for aarch64 test execution behavior. Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Signed-off-by: Jorge Prendes --- .github/workflows/dep_build_test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/dep_build_test.yml b/.github/workflows/dep_build_test.yml index 77bc8dfcf..6271e7993 100644 --- a/.github/workflows/dep_build_test.yml +++ b/.github/workflows/dep_build_test.yml @@ -29,7 +29,7 @@ on: full_aarch64: description: >- When "true", also run the single-driver tests on aarch64. PRs leave - this "false" so the aarch64 runner only builds and runs the default + this "false" so the aarch64 runner builds and runs Miri and the default Rust tests; the daily schedule (DailyArm64.yml) sets it "true". This input only affects aarch64: it does not change X64, where those tests already run on Linux and are skipped on Windows regardless.