Skip to content

ci: cut OpenVMM and CLI work from the critical path and the WHP pool - #436

Merged
Pedro Henrique Penna (ppenna) merged 3 commits into
devfrom
agents/fix-ci-turnaround-time-issue-430
Oct 8, 2026
Merged

Pedro Henrique Penna (ppenna) merged 3 commits into
devfrom
agents/fix-ci-turnaround-time-issue-430

Conversation

@ppenna

@ppenna Pedro Henrique Penna (ppenna) commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Refs #430. This PR targets the two constraints that the investigation in #430 identified: build-heavy OpenVMM jobs on the critical path, and the scarce WHP pool. It frees self-hosted capacity on every run, and it takes the WHP OpenVMM jobs off the critical path whenever a pull request cannot affect them. It keeps every required suite, backend, guest, and gate. It doesn't narrow dev pushes: they keep the classification they had before this PR, so both OpenVMM suites still run on every push that changes a test input. Only pull requests use the per-suite outputs. (As before this PR, a documentation-only push still skips the hypervisor matrix.)

Three commits:

  1. Run the OpenVMM unit tests once per OS (workstream 4). The Linux unit-test suite ran on both a KVM and an MSHV runner. The hardware tests are #[ignore = "requires /dev/kvm"] or #[ignore = "requires /dev/mshv"], test-openvmm-unit never passes --run-ignored, and no other test probes the device. In run 37635396339, both legs ran the same 4,820 tests across 414 binaries, with the same 28 skipped. One leg now runs on the MSHV pool, the largest one, and one on WHP.
  2. Run each OpenVMM suite only when its inputs change (workstream 5). A tested Python classifier, nvx.py classify-ci-changes, replaces the inline bash detector and adds run-openvmm-unit-tests and run-openvmm-vmm-tests outputs. A pull request runs a suite only when it changes one of that suite's inputs: the OpenVMM pin, the Rust pin, runner setup, ci.yml, the job's actions, nvx.py, and the modules that ci.py imports. The VMM tests also depend on every input of the kernel and Alpine initramfs cache keys, since they boot those guests. Pushes keep both suites on run-tests, as before this PR. The detector runs the full matrix whenever it can't classify a change. That includes a missing base or commit, a failed diff or classifier, and a missing or duplicated output. This commit also fixes a gap: a change only to azurelinux/packages.lock.json used to skip the microVM tests that boot that guest.
  3. Validate the NVX CLI once per OS on hosted runners (workstream 4). validate-nvx ran in up to 15 hypervisor jobs per workflow: about 37 s per Linux job and 50 s per WHP job, adding up to 10.4 self-hosted runner-minutes per dev run. It now runs in nvx-cli-linux and nvx-cli-windows on GitHub-hosted runners, after the OpenVMM checkout that a CPU-profile test needs. Those jobs use Python 3.12 (the runners' release) and, on Windows, the pinned Rust release with the minimal profile. The microVM and platform jobs of each OS wait for that OS's CLI result, as the issue asks. The OpenVMM unit and VMM tests don't wait, because they set the critical path. Required status, release, and performance persistence require both CLI jobs.

doc/ci.md now describes the job graph and change classification. It records the runner pools (5 WHP, 6 KVM, 9 MSHV) in place of the stale "three runners per backend".

Evidence from the current pipeline

Measured over 29 recent full-matrix runs (job API data):

Lane Typical duration Notes
OpenVMM VMM tests / WHP 20.4 to 26.2 min about 15 min of serial compilation before 4.8 min of tests; critical path on every run
OpenVMM unit tests / WHP 16.4 to 22.0 min about 13 min of compilation, 45 s of tests
NVX microVM tests / WHP 12.5 min (median) critical only when the OpenVMM binary cache misses
Validate NVX CLI 37 s Linux, 50 s WHP in every hypervisor job

The WHP pool (5 runners) carries about 71 runner-minutes per run, against about 45 for KVM (6 runners) and 40 for MSHV (9 runners). Runs that overlap other PRs took 40 to 45 minutes, mostly waiting for WHP runners: queue times of 10 to 20 minutes per WHP job.

Expected impact

  • Every run that tests OpenVMM: removes the 37 to 50 s CLI step from every hypervisor job's critical path, about 10 self-hosted runner-minutes (4 to 5 of them WHP), and a 7 to 8 minute Linux unit-test job.
  • Pull requests that change no OpenVMM suite input: both WHP OpenVMM jobs and their Linux counterparts are skipped. That frees about 40 WHP runner-minutes and moves the critical path from the WHP VMM tests (about 23 min) to the WHP microVM tests (about 15 min). Replaying the classifier over the 29 merged PRs among the last 40 that ran tests, 11 (38%) skip the VMM tests and 13 (45%) skip the unit tests.
  • This PR's own run: it edits ci.yml, so it runs the full matrix. It should show the per-job savings, but not the classification win.

Measured results

Run 37768232087 on head 79966a6ce, against pre-change full-matrix runs without contention (37637843120, 37508330384, 37527959862, 37635396339, 37503416598):

Before Attempt 1 Attempt 2
Conditions idle runners, OpenVMM binary cache miss new OpenVMM pin from #261 (cold builds), overlapping the #261 dev push idle runners, binary cache hit
Wall time 23.6 to 26.6 min (#430: 25.6 median) 30.6 min, including a 9.1 min WHP queue 22.0 min
Longest lane WHP VMM tests, about 22 min WHP VMM tests, 20.2 min after the queue WHP VMM tests, 20.6 min
Self-hosted runner-minutes (KVM / MSHV / WHP) 46-49 / 38-47 / 72-80 35.9 / 34.5 / 67.5 35.1 / 29.4 / 59.8
Longest queue wait up to 3.2 min 9.1 min (contention) 0.2 min
  • Under the same cold-cache conditions as the baselines, attempt 1 used 13% fewer self-hosted runner-minutes (137.9 against about 158): 23% fewer on KVM and 7% fewer on WHP. Under load, those minutes are the queue time that other pull requests wait.
  • The hosted CLI jobs take 0.7 min (Linux) and 1.6 min (Windows), and run the OpenVMM CPU-profile test instead of skipping it. Gating WHP jobs on them cost 0.1 min in attempt 2.
  • Per-job: WHP microVM tests 11.6 min (was 12.3 median), WHP platform 8.3 to 8.7 min (was 9.6), one 5.5 to 6.5 min Linux unit-test job (was 7.0 + 8.4).
  • A full-matrix run improves by about 1 to 2 minutes, because both OpenVMM suites still compile for about 15 minutes on Windows. That lane needs workstream 2, or a larger sccache on the WHP runners.
  • Pull requests that change no OpenVMM suite input skip that lane entirely. In attempt 2, every other job finished by 13.8 min, so such a pull request would take about 14 minutes instead of 22 to 26. That is the CI: maximize workflow parallelism and reduce turnaround time #430 cache-hit target of 12 to 15 minutes, for 38% of recent pull requests that ran tests.

Validation

  • scripts/test_nvx_tools.py (430 tests) on Windows. New tests cover:
    • every path class for pull requests and pushes;
    • the required-result policy for the new outputs and CLI jobs;
    • the job summary and the CLI command;
    • a drift guard that derives each suite's inputs from its jobs' actions, the ci.py import graph, and the guest cache keys.
  • A POSIX-only bash test runs the real detector script through every fallback, and once with the real classifier. It passes on Linux (WSL Ubuntu).
  • The full validate-nvx command set on Windows and on Linux (WSL), including the gcc-based guest tests.
  • ruff check, ruff format --check, and pyright (strict) for Linux and Windows; actionlint on the four changed workflows.
  • Each of the three commits passes the test module on its own.

Not in this PR (remaining #430 work)

  • Workstream 2, the build-once/run-sharded OpenVMM VMM-test bundle: vmm-tests-run --build-only with vmm-tests-run-target. Earlier target-directory caching was deliberately replaced by sccache, so this needs bundle-size measurements first.
  • Workstream 3, microVM sharding: the suite prints its Running ... lines through buffered stdout, so per-scenario durations can't be recovered from logs yet.
  • Workstream 6, splitting the cold guest-artifact build.
  • Runner-side: both Windows and Linux runners report Cache size 10 GiB / Max cache size 10 GiB, so sccache is full and evicting. That likely explains the 74 to 80% WHP hit rate, against 97 to 99.7% on Linux. Raising SCCACHE_CACHE_SIZE is a provisioning change (and validate-runner checks the value), so it's left for a follow-up once disk headroom is known.

The openvmm-unit-tests matrix ran the same Linux suite on a KVM runner
and on an MSHV runner. The OpenVMM unit tests that need a hypervisor
device are #[ignore]d ("requires /dev/kvm", "requires /dev/mshv"),
test-openvmm-unit runs nextest without --run-ignored, and no other unit
test or doctest probes the device. Run 37635396339 shows the result:
both Linux legs ran the identical 4,820 tests across 414 binaries, with
the same 28 skipped. The second leg added no coverage but held a
hypervisor runner for 7 to 8 minutes of every run that tests OpenVMM.

Run the suite once per OS instead, on the MSHV pool and on WHP. The MSHV
pool is the largest (nine runners, against six KVM and five WHP), so the
remaining Linux leg lands where capacity is most available, and it still
validates its runner as MSHV. The legs are now named "Linux" and
"Windows", since neither stands for a backend any more; the dev ruleset
requires only "Required status check", so renaming them needs no
ruleset change.

The OpenVMM VMM tests keep one leg per backend, because Petri boots real
guests on each hypervisor.

Refs #430

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The change detector exposed only run-tests and run-workloads, so every
pull request that touched any NVX script, guest, or workflow file ran
the OpenVMM unit and VMM tests, although those suites test a pinned
OpenVMM revision that the change did not alter. On WHP they are the
longest jobs in the matrix: the VMM tests compile OpenVMM, its tests,
pipette, and the UEFI test image for about 15 minutes before testing,
and the unit tests compile for about 13. Together they hold a WHP runner
for about 40 minutes per run, and the WHP pool has only five runners.

Move the classification from inline bash into a tested Python
classifier, `nvx.py classify-ci-changes`, and add two outputs:

- run-openvmm-unit-tests: the OpenVMM pin, rust-toolchain.toml, the
  runner setup scripts, ci.yml, the actions that the unit-test job uses,
  nvx.py, and the modules that ci.py imports.
- run-openvmm-vmm-tests: those inputs plus every input of the kernel and
  Alpine initramfs cache keys, which the VMM tests boot, and the
  build-guest-artifacts action that produces them.

Pull requests run each suite only when one of its inputs changed, and
the suite outputs imply run-tests. Pushes set both to run-tests, so every
dev push keeps running the full required matrix. Of the 29 merged pull
requests among the last 40 that ran tests, 11 changed no VMM-test input
and 13 no unit-test input, so these skip one or both suites, along with
the critical path that the WHP VMM tests set.

The detector stays fail-safe: it runs the full matrix when the base or a
commit is missing, when the diff or the classifier fails, or when the
classifier omits a suite or reports it twice, since a missing output
would otherwise skip that suite's jobs silently. It forwards only
well-formed name=true|false lines to GITHUB_OUTPUT. Required status
check passes the new outputs to check-required-ci, which now takes the
classification as one CiChanges value and expects each OpenVMM job to
succeed exactly when its suite is selected.

The classifier also adds azurelinux/ to the test inputs. The Azure Linux
package lock is an input of the Azure Linux initramfs that the microVM
tests boot, but a change to it alone skipped those tests.

Tests cover each path class for pull requests and pushes, the job
summary, the CLI command, and the required-result policy for the new
outputs. A bash test (POSIX only) runs the detector script with stubbed
git and classifier output through every fallback, and once with the real
classifier. A drift test derives each suite's inputs from the actions its
jobs use, the modules that ci.py imports, and the guest cache key inputs,
and fails when a pattern misses one.

Refs #430

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Every hypervisor job ran the validate-nvx action: the OpenVMM unit and
VMM tests, the microVM tests, and the platform benchmarks, on all three
backends. That is up to 15 runs of the same backend-independent unit
tests per workflow, about 37 s on a Linux runner and 50 s on a WHP
runner, or 10.4 runner-minutes of self-hosted time per dev run, and
each run sat on the critical path of its job.

Validate the CLI once per OS instead, in the GitHub-hosted nvx-cli-linux
and nvx-cli-windows jobs, and remove the action from the hypervisor jobs
and the two reusable workflows. The jobs start with the workflow, since
they do not depend on change detection, and keep the coverage of the
runs they replace:

- They check out OpenVMM first, because a test compares NVX's CPU
  profile catalog with the profiles of the pinned submodule revision
  and skips without it.
- They use Python 3.12, the release that the self-hosted runners
  provide.
- The Windows job installs the Rust release that rust-toolchain.toml
  pins, with the minimal profile, for the test that compiles the
  Windows curl shim; rustup would otherwise install the default profile
  on first use.
- Like the hypervisor jobs, they run only for pushes and same-repository
  pull requests, because fork pull requests lack the deploy key that the
  OpenVMM checkout uses.

The microVM and platform jobs of each OS now wait for that OS's CLI job,
so a broken CLI no longer occupies the hypervisor runners. They already
wait for their OpenVMM producer, which a hosted CLI job usually
outlasts. The OpenVMM unit and VMM tests do not wait: they start right
after change detection and set the critical path. Required status check,
the development release, and performance persistence require both CLI
jobs to succeed. With validate-nvx gone from every test job,
classification no longer treats changes to it as test inputs.

The CI documentation now describes the job graph, records the runner
pools (five WHP, six KVM, and nine MSHV runners) in place of the stale
"three runners per backend", and explains why the WHP pool bounds the
time of overlapping runs.

Refs #430

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI balanced review requested due to automatic review settings October 8, 2026 11:09

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Dev pushes remain path-classified instead of running the full matrix required by the stated acceptance criteria.

1 open finding
What changed in this PR

This PR restructures NVX CI to reduce duplicated validation and conserve self-hosted runner capacity.

Changes:

  • Adds per-suite change classification for OpenVMM tests.
  • Runs unit and CLI validation once per OS.
  • Updates CI policy tests and documentation.
File Description
.github/​workflows/​ci.yml Revises job topology, classification, and gating.
.github/​workflows/​run-nvx-microvm-tests.yml Removes duplicated CLI validation.
.github/​workflows/​run-platform.yml Removes duplicated CLI validation.
scripts/​nvx_tools/​ci.py Implements suite classification and result policy.
scripts/​nvx.py Exposes classification and expanded CI checks.
scripts/​test_nvx_tools.py Tests classification, topology, and fallback behavior.
doc/​ci.md Documents the new graph and runner capacity.
doc/​usage.md Documents the new CLI interface.
doc/​benchmarks.md Documents CLI gating for persistence and releases.

🧠 Review effort: Balanced


Give feedback about Copilot approvals in this survey to enter a drawing for a $150 gift card.

Comment thread scripts/nvx_tools/ci.py
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants