Skip to content

Compare pull requests against a baseline measured on the same machine - #1894

Merged
jprendes merged 6 commits into
hyperlight-dev:mainfrom
jprendes:bench-same-machine-baseline
Oct 8, 2026
Merged

jprendes merged 6 commits into
hyperlight-dev:mainfrom
jprendes:bench-same-machine-baseline

Conversation

@jprendes

@jprendes jprendes commented Oct 7, 2026

Copy link
Copy Markdown
Contributor

A pull request is measured against the daily benchmark run at the commit it
branched from. That run came from another machine. The runner pools hold more
than one processor model per vendor and are shared with whatever else is
scheduled there, so the difference between two machines reads as a difference
between two commits.

PR #1893 touches one file of CI tooling and cannot alter a benchmark. It was
reported as a 1.58x regression on hyperv-ws2025 / intel, because its baseline
landed on a processor the rest of the week did not:

run virtq_readwrite/slot_pool_segmented/262144 processor
2 Oct 43.2k Model 106
3 Oct 43.0k Model 106
4 Oct 45.2k Model 106
5 Oct 43.1k Model 106
6 Oct, the baseline 37.6k Model 143
the pull request 59.5k Model 106

Cause

Two machines, not two commits. Of the six configurations, two draw from pools
holding more than one processor model, where the models differ by 10% and 11.9%
while runs on the same model differ by 2% and 4.4%.

Shared tenancy moves the rest. payload_allocation/slot_pool_segmented/262144
sat at 524, 526, 526, 533 and 593 ns across the week on one processor model,
and the same commit measured 835 ns on the pull request's runner.

Criterion's confidence interval does not see either. It measures the samples
within one run, which vary by 0.90%, while a run measured again the next day
moves 7.65%, more than the interval allows in 111 of 114 benchmarks.

What this does

cargo ci bench --baseline-ref <COMMIT> measures that commit before the one
checked out, saving its results as the baseline criterion then compares
against. The two sit in one criterion directory, so a report reads the
comparison the run carries and names no baseline of its own.

The baseline is built in a git worktree, so the checkout in hand is never moved
and a run that fails leaves nothing to restore. Each benchmark binary resolves
its guests relative to its own source tree, so the baseline reads the worktree
while the current tree keeps its own, with no swapping between passes.

The guests the baseline needs are built by the job that already holds the
toolchain for them. HEAD^1 names the branch point because the checkout is the
merge its validation builds, and both jobs assert that before relying on it,
since on anything else HEAD^1 is the previous commit and would measure
against it without saying so.

Also here

criterion-markdown 0.2.2. A report shows criterion's typical estimate, the
regression slope wherever linear sampling fits one, while the change beside it
read the mean. The mean weights every sample alike and so carries the warm-up
the slope leaves behind: slot_pool/alloc_dealloc_1500 reported 1.56x slower
while its slope held 7% faster, from 32 samples that ran cold before the rest
settled.

Verified

The pass and its comparison run end to end, leaving base, new and change
in one directory for the report to read, and the worktree removed after it.
cargo ci bench without --baseline-ref is unchanged, which is what the daily
runs use.

The benchmark report on this pull request is the first one measured this way.

jprendes and others added 2 commits October 7, 2026 14:40
A report shows criterion's typical estimate, the regression slope wherever
linear sampling fits one, while the change beside it read the mean. The mean
weights every sample alike and so carries the warm-up the slope leaves behind,
which read as a regression on benchmarks whose first samples ran cold.

0.2.2 compares the estimate it displays, and holds a verdict back until the
whole confidence interval clears its threshold.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Jorge Prendes <jorge.prendes@gmail.com>
A pull request was measured against the daily run its branch point carried.
That run came from another machine, and the pools hold more than one processor
model per vendor and are shared with whatever else is scheduled there. The
difference between two runners read as a difference between two commits:
one report announced a 1.58x regression for a change that touches only CI
tooling, from a baseline that happened to land on faster silicon.

`cargo ci bench --baseline-ref` measures a second commit before the one checked
out, in a git worktree so the checkout in hand is never moved and a run that
fails leaves nothing to restore. Each benchmark binary resolves its guests
relative to its own source tree, so the baseline reads the worktree and the
current tree keeps its own, and criterion compares the two from one directory.

The guests the baseline needs are built by the job that already has the
toolchain for them. `HEAD^1` names the branch point because the checkout is
the merge its validation builds, which both jobs assert before relying on.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Jorge Prendes <jorge.prendes@gmail.com>
Copilot AI balanced review requested due to automatic review settings October 7, 2026 13:59
@jprendes jprendes added C-bug For PRs that fix bugs A-testing Related to tests or testing infrastructure S-needs-review PR is ready for (re-)review labels Oct 7, 2026

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.

Copilot review overview

🔵 Needs a closer look

Benchmark comparison issues remain unresolved, and the workflow changes need validation across the CI matrix.

Review effort: Balanced
Findings: 6 Medium severity

Open (6)
What changed in this PR

Measures baseline and PR benchmarks on the same runner to reduce machine-dependent noise in Hyperlight’s CI comparisons.

Changes:

  • Adds paired measurements using a baseline worktree and matching guest binaries.
  • Connects baseline builds and measurements to PR validation.
  • Reads embedded comparisons and updates criterion-markdown to 0.2.2.
File Description
src/​hyperlight_ci/​src/​manifest.rs Exposes the Criterion results path.
src/​hyperlight_ci/​src/​main.rs Registers the baseline module.
src/​hyperlight_ci/​src/​bench.rs Adds baseline options and paired execution.
src/​hyperlight_ci/​src/​bench_report.rs Defaults to embedded comparisons.
src/​hyperlight_ci/​src/​baseline.rs Manages worktrees and baseline guests.
src/​hyperlight_ci/​Cargo.toml Updates criterion-markdown.
Justfile Adds the paired benchmark recipe.
Cargo.lock Locks the dependency update.
.github/​workflows/​ValidatePullRequest.yml Coordinates baseline jobs and reporting.
.github/​workflows/​dep_build_guests.yml Supports revision-specific guest artifacts.
.github/​workflows/​dep_benchmarks.yml Runs both measurements on one runner.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/hyperlight_ci/src/baseline.rs Resolved
Comment thread src/hyperlight_ci/src/bench.rs Resolved
Comment thread src/hyperlight_ci/src/bench.rs Resolved
Comment thread src/hyperlight_ci/src/bench.rs Resolved
Comment thread src/hyperlight_ci/src/bench.rs Resolved
@hyperlight-gh-bot

This comment has been minimized.

jprendes and others added 3 commits October 7, 2026 15:38
A commit may add a benchmark the one it is measured against never had, which
criterion's strict `--baseline` treats as a failure, so every pull request that
extends the suite would fail its benchmark job. The comparison reads leniently
now, and the baseline it is about to write is cleared first, so a copy left by
an earlier run cannot stand in for a benchmark this one could not measure.

A configuration named by a relative path was read after entering the worktree,
from whatever the commit being measured carried at the same path. It is
resolved before either pass runs.

A pre-built `--binary` holds one commit and cannot represent two, so it is
refused alongside a baseline rather than measuring the same executable twice.
Worktrees are pruned before one is added, since `cargo clean` takes the
directory while leaving the registration behind.

`HEAD^1` is the commit a pull request merges into rather than where it
branched, which is what the comparison wants and what the comments now say.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Jorge Prendes <jorge.prendes@gmail.com>
`HEAD^1` is the commit a pull request merges into, which carries whatever
landed on the branch it targets since the pull request left it. Those commits
are not the pull request's to answer for.

`HEAD^1...HEAD^2` names where the two parents of the validation merge branched
apart, which is where the pull request did. `git checkout` and `git diff`
already read a revision that way. `git worktree add` takes a reference rather
than a revision, so the naming is done before it is handed a commit.

The form also says when it does not hold: without a second parent there is
nothing to branch apart from, where `HEAD^1` would quietly name the commit
before.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Jorge Prendes <jorge.prendes@gmail.com>
A report named its baseline from the second set of results it read. A run that
measures its own baseline reads no second set, so the report said only which
commit it measured and left the reader to guess the other.

The run records the commit it measured against in its manifest, beside what it
measured and the machine it ran on, and the report names it from there.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Jorge Prendes <jorge.prendes@gmail.com>

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.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

Note

This error may be related to your runner configuration. You can now configure runners for Copilot code review separately from Copilot cloud agent by creating a copilot-code-review.yml file with your setup steps. Read the docs for details.

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

Baseline cleanup can delete unrelated results, and comparison handling has unresolved correctness issues.

1 open finding
Previously missed (3)

In code that hasn't changed since last review

Medium severity Quote guest paths containing spaces in baseline copying

Justfile:467

A guest directory containing spaces is split by the recipe shell, so --baseline-guests receives a truncated path and guest copying fails. The workflow's quotes protect the argument passed to just, but not this interpolation. Quote the path here too.

Medium severity Use the selected baseline name when rendering benchmark reports

src/​hyperlight_ci/​src/​bench.rs:106

A non-default --baseline-name saves the paired baseline somewhere bench-report does not read. Its renderer defaults to base/estimates.json, so the report either omits comparisons or uses an unrelated existing base, even though the manifest names the paired commit. Persist the selected dataset name and pass it to the renderer, or keep paired runs fixed to base.

Medium severity Allow missing benchmarks during lenient baseline selection

src/​hyperlight_ci/​src/​bench.rs:155

If --config-file allowlists a benchmark introduced by the current commit, the baseline pass fails in BenchConfig::select() before measuring the current tree. Selection rejects unmatched patterns and empty filtered suites, so Criterion's --baseline-lenient cannot help. Allow absent benchmarks and an empty selection in the baseline pass while retaining strict validation for the current suite.

🧠 Review effort: Balanced

Comment thread src/hyperlight_ci/src/bench.rs Resolved
A benchmark may be grouped under the name a baseline is saved under, and
clearing the baseline took the group with it: its benchmarks, their last run
and every other baseline saved beside them.

Measurements tell the two apart. A saved baseline holds them, a group holds the
benchmarks below it, so the group is walked into rather than removed.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Jorge Prendes <jorge.prendes@gmail.com>
@hyperlight-gh-bot

Copy link
Copy Markdown

Benchmark Results

Measured commit: 832a44fe7779
Baseline commit: 5ddb37af894e

kvm / amd (Linux) (➖ stable)

No benchmark improved or regressed.

Benchmark Results

function_call_codec

encode_control decode_vec_bytes_copy
byte_chunks 806.05 ns (➖ 1.05x slower)
vec_bytes 597.45 ns (➖ 1.02x faster)
375.34 µs (➖ 1.01x slower)

payload_allocation

slot_pool_segmented
262144 521.59 ns (➖ 1.02x faster)
65536 141.31 ns (➖ 1.01x faster)

sandboxes

create_initialized_and_drop
medium 78.26 ms (➖ 1.01x faster)

slot_pool

alloc_dealloc_1500 alloc_dealloc_4096 alloc_dealloc_128
7.74 ns (➖ 1.02x faster) 7.71 ns (➖ 1.00x faster) 7.82 ns (➖ 1.01x slower)

snapshot_files

load_snapshot_unverified
small 92.81 µs (➖ 1.01x faster)

virtq_readonly

slot_pool_segmented_fragmented slot_pool_segmented
262144 7.41 µs (➖ 1.03x slower) 7.04 µs (➖ 1.07x faster)
65536 2.07 µs (➖ 1.08x slower) 1.92 µs (➖ 1.11x faster)

virtq_readwrite

slot_pool_segmented_fragmented slot_pool_segmented
65536 6.31 µs (➖ 1.02x faster) 6.23 µs (➖ 1.01x faster)
8192 1.08 µs (➖ 1.07x slower) 1.04 µs (➖ 1.02x slower)
262144 28.02 µs (➖ 1.03x slower)
kvm / intel (Linux) (➖ stable)

No benchmark improved or regressed.

Benchmark Results

function_call_codec

encode_control decode_vec_bytes_copy
byte_chunks 860.02 ns (➖ 1.06x slower)
vec_bytes 614.30 ns (➖ 1.01x faster)
781.36 µs (➖ 1.02x slower)

payload_allocation

slot_pool_segmented
262144 605.15 ns (➖ 1.00x slower)
65536 163.33 ns (➖ 1.03x faster)

sandboxes

create_initialized_and_drop
medium 86.39 ms (➖ 1.02x slower)

slot_pool

alloc_dealloc_1500 alloc_dealloc_4096 alloc_dealloc_128
8.50 ns (➖ 1.01x slower) 8.38 ns (➖ 1.01x slower) 8.54 ns (➖ 1.01x slower)

snapshot_files

load_snapshot_unverified
small 53.51 µs (➖ 1.01x faster)

virtq_readonly

slot_pool_segmented_fragmented slot_pool_segmented
262144 9.02 µs (➖ 1.00x faster) 9.07 µs (➖ 1.00x slower)
65536 2.54 µs (➖ 1.01x faster) 2.54 µs (➖ 1.01x slower)

virtq_readwrite

slot_pool_segmented_fragmented slot_pool_segmented
65536 8.52 µs (➖ 1.00x faster) 8.42 µs (➖ 1.01x faster)
8192 892.67 ns (➖ 1.01x faster) 867.42 ns (➖ 1.02x slower)
262144 36.49 µs (➖ 1.01x slower)
mshv3 / amd (Linux) (➖ stable)

No benchmark improved or regressed.

Benchmark Results

function_call_codec

encode_control decode_vec_bytes_copy
byte_chunks 956.98 ns (➖ 1.03x slower)
vec_bytes 706.96 ns (➖ 1.00x slower)
321.58 µs (➖ 1.01x slower)

payload_allocation

slot_pool_segmented
262144 673.36 ns (➖ 1.00x faster)
65536 182.18 ns (➖ 1.00x slower)

sandboxes

create_initialized_and_drop
medium 60.36 ms (➖ 1.02x faster)

slot_pool

alloc_dealloc_1500 alloc_dealloc_4096 alloc_dealloc_128
10.15 ns (➖ 1.02x slower) 9.96 ns (➖ 1.00x slower) 9.93 ns (➖ 1.00x faster)

snapshot_files

load_snapshot_unverified
small 85.57 µs (➖ 1.00x faster)

virtq_readonly

slot_pool_segmented_fragmented slot_pool_segmented
262144 9.03 µs (➖ 1.09x faster) 9.25 µs (➖ 1.00x slower)
65536 2.65 µs (➖ 1.01x faster) 2.47 µs (➖ 1.00x slower)

virtq_readwrite

slot_pool_segmented_fragmented slot_pool_segmented
65536 8.05 µs (➖ 1.01x faster) 8.02 µs (➖ 1.03x faster)
8192 1.32 µs (➖ 1.01x faster) 1.32 µs (➖ 1.00x slower)
262144 34.46 µs (➖ 1.06x faster)
mshv3 / intel (Linux) (➖ stable)

No benchmark improved or regressed.

Benchmark Results

function_call_codec

encode_control decode_vec_bytes_copy
byte_chunks 998.36 ns (➖ 1.08x slower)
vec_bytes 667.95 ns (➖ 1.00x faster)
682.14 µs (➖ 1.00x faster)

payload_allocation

slot_pool_segmented
262144 621.71 ns (➖ 1.00x slower)
65536 171.22 ns (➖ 1.03x slower)

sandboxes

create_initialized_and_drop
medium 64.48 ms (➖ 1.00x faster)

slot_pool

alloc_dealloc_1500 alloc_dealloc_4096 alloc_dealloc_128
9.42 ns (➖ 1.02x faster) 8.78 ns (➖ 1.00x slower) 8.78 ns (➖ 1.00x faster)

snapshot_files

load_snapshot_unverified
small 43.88 µs (➖ 1.00x slower)

virtq_readonly

slot_pool_segmented_fragmented slot_pool_segmented
262144 7.93 µs (➖ 1.00x slower) 7.94 µs (➖ 1.00x faster)
65536 2.26 µs (➖ 1.02x faster) 2.29 µs (➖ 1.03x slower)

virtq_readwrite

slot_pool_segmented_fragmented slot_pool_segmented
65536 7.55 µs (➖ 1.00x slower) 7.45 µs (➖ 1.01x faster)
8192 909.64 ns (➖ 1.03x faster) 911.56 ns (➖ 1.01x slower)
262144 40.04 µs (➖ 1.06x slower)
hyperv-ws2025 / amd (Windows) (➖ stable)

No benchmark improved or regressed.

Benchmark Results

function_call_codec

encode_control decode_vec_bytes_copy
byte_chunks 1.18 µs (➖ 1.01x slower)
vec_bytes 825.05 ns (➖ 1.07x slower)
2.19 ms (➖ 1.02x faster)

payload_allocation

slot_pool_segmented
262144 822.16 ns (➖ 1.04x slower)
65536 232.07 ns (➖ 1.06x faster)

sandboxes

create_initialized_and_drop
medium 103.80 ms (➖ 1.22x slower)

slot_pool

alloc_dealloc_1500 alloc_dealloc_4096 alloc_dealloc_128
10.20 ns (➖ 1.04x faster) 10.21 ns (➖ 1.01x slower) 9.94 ns (➖ 1.00x faster)

snapshot_files

load_snapshot_unverified
small 722.54 µs (➖ 1.09x faster)

virtq_readonly

slot_pool_segmented_fragmented slot_pool_segmented
262144 9.57 µs (➖ 1.06x slower) 9.11 µs (➖ 1.01x slower)
65536 2.39 µs (➖ 1.03x slower) 2.38 µs (➖ 1.02x slower)

virtq_readwrite

slot_pool_segmented_fragmented slot_pool_segmented
65536 8.83 µs (➖ 1.02x slower) 9.02 µs (➖ 1.01x faster)
8192 1.45 µs (➖ 1.01x faster) 1.31 µs (➖ 1.02x slower)
262144 37.94 µs (➖ 1.01x faster)
hyperv-ws2025 / intel (Windows) (➖ stable)

No benchmark improved or regressed.

Benchmark Results

function_call_codec

encode_control decode_vec_bytes_copy
byte_chunks 1.22 µs (➖ 1.01x slower)
vec_bytes 807.98 ns (➖ 1.10x faster)
3.15 ms (➖ 1.01x faster)

payload_allocation

slot_pool_segmented
262144 737.16 ns (➖ 1.02x faster)
65536 215.97 ns (➖ 1.05x faster)

sandboxes

create_initialized_and_drop
medium 108.57 ms (➖ 1.08x faster)

slot_pool

alloc_dealloc_1500 alloc_dealloc_4096 alloc_dealloc_128
10.05 ns (➖ 1.00x faster) 10.59 ns (➖ 1.01x slower) 9.96 ns (➖ 1.01x slower)

snapshot_files

load_snapshot_unverified
small 606.81 µs (➖ 1.08x faster)

virtq_readonly

slot_pool_segmented_fragmented slot_pool_segmented
262144 7.79 µs (➖ 1.01x slower) 7.79 µs (➖ 1.00x slower)
65536 2.33 µs (➖ 1.00x slower) 2.34 µs (➖ 1.01x faster)

virtq_readwrite

slot_pool_segmented_fragmented slot_pool_segmented
65536 8.39 µs (➖ 1.02x faster) 8.52 µs (➖ 1.05x faster)
8192 1.36 µs (➖ 1.10x faster) 1.11 µs (➖ 1.03x faster)
262144 39.98 µs (➖ 1.09x faster)

Reported by cargo ci bench-report --candidate run:37650339172 --config-file bench_report.toml.

@jprendes
jprendes merged commit 8a381a6 into hyperlight-dev:main Oct 8, 2026
59 checks passed
@github-actions github-actions Bot removed the S-needs-review PR is ready for (re-)review label Oct 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-testing Related to tests or testing infrastructure C-bug For PRs that fix bugs

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants