Skip to content

Make PrimeCaches the only Rust cache writer and remove sccache - #1963

Open
ludfjig wants to merge 11 commits into
hyperlight-dev:mainfrom
ludfjig:ci-cache-priming
Open

ludfjig wants to merge 11 commits into
hyperlight-dev:mainfrom
ludfjig:ci-cache-priming

Conversation

@ludfjig

@ludfjig ludfjig commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

Makes PrimeCaches the only workflow that writes Rust caches, and removes sccache.

PR CI hit sccache rate limits (429/503), and caches on main kept growing until they filled the 10 GB repository quota. rust-cache keeps every dependency version it restores, so entries saved after builds on restored caches grow with each lockfile change.

  • Remove sccache. The jobs it served (fuzz, non-PIE guests, bench baseline) get rust-cache entries.
  • PrimeCaches looks up each key and builds from a cold target only on a miss. Every other workflow restores only.
  • Scheduled, Miri and Copilot jobs no longer write caches.
  • A final PrimeCaches job deletes superseded v1-rust-* and sysroot-* entries on main.
  • Bump rust-cache to v2.9.3 and start under the v1-rust prefix, so the first run replaces the grown v0-rust entries.

A cold PrimeCaches run on this branch saved about 8.6 GB in total. The largest entries are code-checks (1.4 GB Linux, 1.0 GB Windows) and the eight host entries (540–620 MB each).

Follow-ups: caches for Miri X64, Windows arm64, macOS and trace fuzz guests.

Easiest to review commit-by-commit

v2.9.3 fixes the cleanup that runs after a partial cache restore.
It returned after the first directory entry, so artifacts older
than a week were kept and saved again in the next cache entry.
See Swatinem/rust-cache#375.

Signed-off-by: Ludvig Liljenberg <4257730+ludfjig@users.noreply.github.com>
sccache stores one GitHub Actions cache entry per compiled crate.
Jobs hit the cache service rate limits (HTTP 429 and 503), which
turned cache reads into slow misses. Its many small entries also
share the 10 GB repository cache quota with the rust-cache entries
and push them out.

rust-cache restores whole target directories with one request per
job, so it covers the same dependency builds. Later commits extend
it to the jobs sccache served alone: fuzzing, the non-PIE guests
and the benchmark baseline.

Signed-off-by: Ludvig Liljenberg <4257730+ludfjig@users.noreply.github.com>
@ludfjig ludfjig added the kind/enhancement For PRs adding features, improving functionality, docs, tests, etc. label Oct 21, 2026
@hyperlight-gh-bot

This comment has been minimized.

@ludfjig
ludfjig marked this pull request as ready for review October 21, 2026 01:37
Copilot AI balanced review requested due to automatic review settings October 21, 2026 01:37

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

Cache cleanup and fuzz priming have unresolved issues that undermine cache freshness and quota control.

3 open issues
What changed in this PR

Centralizes Rust cache writes in PrimeCaches to limit cache growth and avoid sccache rate limits.

Changes:

  • Adds cold cache priming and superseded-entry cleanup.
  • Removes sccache and makes other cache consumers restore-only.
  • Adds benchmark baseline target reuse and fuzz build priming.
File Description
src/​hyperlight_ci/​src/​bench.rs Supports a separate baseline target directory.
Justfile Adds priming recipes and shares the guest sysroot.
.github/​workflows/​RustNightly.yml Removes nightly caching and sccache.
.github/​workflows/​PrimeCaches.yml Coordinates priming and cache cleanup.
.github/​workflows/​dep_run_examples.yml Restores versioned caches without sccache.
.github/​workflows/​dep_miri.yml Makes Miri caches restore-only.
.github/​workflows/​dep_fuzzing.yml Adds fuzz cache priming.
.github/​workflows/​dep_code_checks.yml Restricts cache writes to priming.
.github/​workflows/​dep_build_test.yml Restores host caches without writing.
.github/​workflows/​dep_build_guests.yml Adds guest priming and non-PIE caching.
.github/​workflows/​dep_benchmarks.yml Seeds the baseline target from restored artifacts.
.github/​workflows/​Coverage.yml Removes coverage caching and sccache.
.github/​workflows/​copilot-setup-steps.yml Disables toolchain caching.
.github/​actions/​setup-sccache/​action.yml Deletes the sccache setup action.

Review effort: Balanced

Comment thread .github/workflows/PrimeCaches.yml Outdated
Comment thread .github/workflows/dep_fuzzing.yml
Comment thread Justfile Outdated
@hyperlight-gh-bot

Copy link
Copy Markdown

Benchmark Results

Measured commit: 13a312bfda0e
Baseline commit: 5e2d3eb72e6b

kvm / amd (Linux) (➖ stable)

No benchmark improved or regressed.

Benchmark Results

function_call_codec

encode_control decode_vec_bytes_copy
byte_chunks 752.38 ns (➖ 1.03x faster)
vec_bytes 589.72 ns (➖ 1.01x slower)
373.01 µs (➖ 1.00x faster)

payload_allocation

slot_pool_segmented
65536 142.31 ns (➖ 1.05x faster)
262144 816.75 ns (➖ 1.48x slower)

sandboxes

create_initialized_and_drop
medium 83.82 ms (➖ 1.02x slower)

slot_pool

alloc_dealloc_4096 alloc_dealloc_128 alloc_dealloc_1500
7.72 ns (➖ 1.00x faster) 7.73 ns (➖ 1.00x slower) 7.75 ns (➖ 1.02x faster)

snapshot_files

load_snapshot_unverified
small 93.93 µs (➖ 1.00x slower)

virtq_readonly

slot_pool_segmented_fragmented slot_pool_segmented
65536 2.06 µs (➖ 1.07x slower) 1.94 µs (➖ 1.08x faster)
262144 7.15 µs (➖ 1.02x faster) 7.41 µs (➖ 1.01x faster)

virtq_readwrite

slot_pool_segmented_fragmented slot_pool_segmented
65536 6.30 µs (➖ 1.02x faster) 6.18 µs (➖ 1.07x faster)
8192 1.09 µs (➖ 1.04x slower) 1.05 µs (➖ 1.02x faster)
262144 27.28 µs (➖ 1.02x faster)
kvm / intel (Linux) (➖ stable)

No benchmark improved or regressed.

Benchmark Results

function_call_codec

encode_control decode_vec_bytes_copy
byte_chunks 831.83 ns (➖ 1.07x slower)
vec_bytes 616.88 ns (➖ 1.09x slower)
741.37 µs (➖ 1.04x slower)

payload_allocation

slot_pool_segmented
65536 151.06 ns (➖ 1.00x faster)
262144 582.57 ns (➖ 1.01x faster)

sandboxes

create_initialized_and_drop
medium 77.34 ms (➖ 1.06x faster)

slot_pool

alloc_dealloc_4096 alloc_dealloc_128 alloc_dealloc_1500
8.13 ns (➖ 1.02x slower) 8.04 ns (➖ 1.03x slower) 8.04 ns (➖ 1.04x slower)

snapshot_files

load_snapshot_unverified
small 52.14 µs (➖ 1.03x slower)

virtq_readonly

slot_pool_segmented_fragmented slot_pool_segmented
65536 2.46 µs (➖ 1.05x slower) 2.48 µs (➖ 1.06x slower)
262144 8.69 µs (➖ 1.04x slower) 8.64 µs (➖ 1.04x slower)

virtq_readwrite

slot_pool_segmented_fragmented slot_pool_segmented
65536 8.31 µs (➖ 1.04x slower) 8.38 µs (➖ 1.06x slower)
8192 874.19 ns (➖ 1.05x slower) 856.01 ns (➖ 1.07x slower)
262144 34.46 µs (➖ 1.08x slower)
mshv3 / amd (Linux) (➖ stable)

No benchmark improved or regressed.

Benchmark Results

function_call_codec

encode_control decode_vec_bytes_copy
byte_chunks 929.70 ns (➖ 1.00x slower)
vec_bytes 710.39 ns (➖ 1.00x faster)
269.42 µs (➖ 1.01x slower)

payload_allocation

slot_pool_segmented
65536 190.30 ns (➖ 1.01x faster)
262144 717.99 ns (➖ 1.01x faster)

sandboxes

create_initialized_and_drop
medium 58.08 ms (➖ 1.06x slower)

slot_pool

alloc_dealloc_4096 alloc_dealloc_128 alloc_dealloc_1500
9.70 ns (➖ 1.01x slower) 9.91 ns (➖ 1.05x slower) 9.40 ns (➖ 1.01x faster)

snapshot_files

load_snapshot_unverified
small 84.01 µs (➖ 1.01x faster)

virtq_readonly

slot_pool_segmented_fragmented slot_pool_segmented
65536 2.41 µs (➖ 1.02x faster) 2.23 µs (➖ 1.01x faster)
262144 10.29 µs (➖ 1.01x faster) 9.66 µs (➖ 1.01x faster)

virtq_readwrite

slot_pool_segmented_fragmented slot_pool_segmented
65536 8.88 µs (➖ 1.10x faster) 8.31 µs (➖ 1.17x faster)
8192 1.30 µs (➖ 1.03x faster) 1.31 µs (➖ 1.03x slower)
262144 36.15 µs (➖ 1.07x faster)
mshv3 / intel (Linux) (➖ stable)

No benchmark improved or regressed.

Benchmark Results

function_call_codec

encode_control decode_vec_bytes_copy
byte_chunks 926.97 ns (➖ 1.00x faster)
vec_bytes 648.25 ns (➖ 1.00x faster)
663.36 µs (➖ 1.04x faster)

payload_allocation

slot_pool_segmented
65536 172.24 ns (➖ 1.01x faster)
262144 662.89 ns (➖ 1.02x slower)

sandboxes

create_initialized_and_drop
medium 64.56 ms (➖ 1.01x faster)

slot_pool

alloc_dealloc_4096 alloc_dealloc_128 alloc_dealloc_1500
9.41 ns (➖ 1.00x faster) 9.32 ns (➖ 1.00x slower) 9.65 ns (➖ 1.01x faster)

snapshot_files

load_snapshot_unverified
small 45.53 µs (➖ 1.00x faster)

virtq_readonly

slot_pool_segmented_fragmented slot_pool_segmented
65536 2.26 µs (➖ 1.00x faster) 2.28 µs (➖ 1.00x faster)
262144 8.06 µs (➖ 1.01x slower) 7.92 µs (➖ 1.01x faster)

virtq_readwrite

slot_pool_segmented_fragmented slot_pool_segmented
65536 7.66 µs (➖ 1.01x faster) 7.64 µs (➖ 1.00x slower)
8192 923.11 ns (➖ 1.02x faster) 934.98 ns (➖ 1.02x faster)
262144 40.66 µs (➖ 1.01x faster)
hyperv-ws2025 / amd (Windows) (➖ stable)

No benchmark improved or regressed.

Benchmark Results

function_call_codec

encode_control decode_vec_bytes_copy
byte_chunks 1.15 µs (➖ 1.01x faster)
vec_bytes 788.92 ns (➖ 1.01x faster)
1.86 ms (➖ 1.02x faster)

payload_allocation

slot_pool_segmented
65536 231.97 ns (➖ 1.01x faster)
262144 813.52 ns (➖ 1.04x slower)

sandboxes

create_initialized_and_drop
medium 81.41 ms (➖ 1.02x slower)

slot_pool

alloc_dealloc_4096 alloc_dealloc_128 alloc_dealloc_1500
10.56 ns (➖ 1.02x slower) 12.58 ns (➖ 1.24x slower) 10.61 ns (➖ 1.02x faster)

snapshot_files

load_snapshot_unverified
small 774.09 µs (➖ 1.18x slower)

virtq_readonly

slot_pool_segmented_fragmented slot_pool_segmented
65536 2.41 µs (➖ 1.10x faster) 2.35 µs (➖ 1.05x faster)
262144 9.30 µs (➖ 1.01x slower) 9.42 µs (➖ 1.04x slower)

virtq_readwrite

slot_pool_segmented_fragmented slot_pool_segmented
65536 8.89 µs (➖ 1.06x faster) 9.33 µs (➖ 1.04x slower)
8192 1.58 µs (➖ 1.06x faster) 1.31 µs (➖ 1.04x faster)
262144 39.56 µ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.19 µs (➖ 1.01x slower)
vec_bytes 777.84 ns (➖ 1.00x slower)
2.97 ms (➖ 1.05x slower)

payload_allocation

slot_pool_segmented
65536 210.22 ns (➖ 1.02x faster)
262144 738.32 ns (➖ 1.00x faster)

sandboxes

create_initialized_and_drop
medium 105.91 ms (➖ 1.04x faster)

slot_pool

alloc_dealloc_4096 alloc_dealloc_128 alloc_dealloc_1500
9.97 ns (➖ 1.00x faster) 9.95 ns (➖ 1.02x faster) 9.96 ns (➖ 1.03x faster)

snapshot_files

load_snapshot_unverified
small 730.49 µs (➖ 1.36x slower)

virtq_readonly

slot_pool_segmented_fragmented slot_pool_segmented
65536 2.35 µs (➖ 1.02x slower) 2.37 µs (➖ 1.04x slower)
262144 7.82 µs (➖ 1.01x slower) 7.74 µs (➖ 1.03x slower)

virtq_readwrite

slot_pool_segmented_fragmented slot_pool_segmented
65536 8.83 µs (➖ 1.02x faster) 8.64 µs (➖ 1.01x faster)
8192 1.49 µs (➖ 1.03x faster) 1.08 µs (➖ 1.03x faster)
262144 43.14 µs (➖ 1.00x faster)

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

Coverage and RustNightly run on a schedule. RustNightly saved its
Rust and sysroot caches on every run. Their entries serve only the
next scheduled run, yet count against the 10 GB repository quota
and evict the entries pull requests restore. Both jobs now build
without a cache.

CargoAudit saved a rust-cache entry through setup-rust-toolchain,
whose cache input defaults to true. It needs no build cache, so the
input is turned off.

Miri saved its cache only from DailyArm64, under the arm64 key.
Pull requests run Miri on X64, so they never restored that entry.
Miri now only restores. Priming the X64 Miri cache is left to a
follow-up.

Signed-off-by: Ludvig Liljenberg <4257730+ludfjig@users.noreply.github.com>
setup-rust-toolchain enables rust-cache by default. Copilot code
review runs these steps on pull request branches and saves an entry
of about 100 MB per branch, such as
v0-rust-copilot-pull-request-reviewer-Linux-x64-aea9c2a7-2b9f5a70.
Branch entries serve only that branch and evict the main entries
every pull request restores.

Signed-off-by: Ludvig Liljenberg <4257730+ludfjig@users.noreply.github.com>
build-rust-guests-non-pie ran cargo hyperlight in target-non-pie to
make a sysroot. That also built simpleguest a second time, about
12 s per job. The non-PIE build then compiled into
target-non-pie/build, which rust-cache did not cover, about 11 s
more. sccache served these builds.

The non-PIE build now depends on build-rust-guests and uses its
sysroot in target/sysroot. The toolchain and target are identical,
so the sysroot is identical. rust-cache covers target-non-pie/build,
and the sysroot cache holds only target/sysroot.

The guests job builds nothing in the root workspace, so its
target entry is dropped. Its lockfile no longer invalidates the
guest cache either.

Signed-off-by: Ludvig Liljenberg <4257730+ludfjig@users.noreply.github.com>
bench-against builds the baseline commit in a worktree under
target/baseline-worktree. Its target directory is empty in CI, so
every pull request compiled the baseline from scratch. sccache
served those builds.

cargo ci bench takes --baseline-target-dir, passed to the baseline
build and the ballast helper build as --target-dir. The benchmarks job copies the restored rust
cache to a separate directory and passes it. A copy is needed
because cargo fingerprints path packages by workspace path and
mtime, so two checkouts cannot share one target directory.

Signed-off-by: Ludvig Liljenberg <4257730+ludfjig@users.noreply.github.com>
PrimeCaches built only the host library. Pull request jobs then
compiled every test binary, dev-dependency and hyperlight-ci on top
of the restored cache, and rust-cache never saved those from a
pull request.

just prime-caches builds what just test compiles and what cargo ci
needs. It sets the cargo runner for the host target to a command
that exits 0, so every test binary builds and none runs. Doctests
are left out because rustdoc compiles them only when they run.

Measured locally, a debug entry grows to 390 MB compressed and a
release entry to 330 MB.

Signed-off-by: Ludvig Liljenberg <4257730+ludfjig@users.noreply.github.com>
Every reusable workflow saved its rust-cache entry whenever it ran
on main. Scheduled jobs such as DailyArm64, Fuzzing and
DailyBenchmarks raced PrimeCaches for the same keys. Each writer
built on a restored entry, and rust-cache keeps every dependency
version it restores, so entries grew with each lockfile change.
Code checks reached 1.7 GB against 676 MB for a clean build.

PrimeCaches is the only writer. Its jobs look up their entry with
lookup-only. On an exact hit they skip the build. Otherwise they
build from a cold target and save, so an entry holds only what one
lockfile needs. dep_build_guests and dep_code_checks take a
prime_cache input for this. Priming skips C guests and uploads.

cache-on-failure is dropped, so a failed build never saves a partial
entry under the exact key. PrimeCaches never cancels a running
run, so the newest commit always saves. Its concurrency group is
per ref, so a manual run on a branch cannot replace one for main.

Signed-off-by: Ludvig Liljenberg <4257730+ludfjig@users.noreply.github.com>
PR fuzz jobs compiled every fuzz target with sccache. With sccache
removed they need a Rust cache.

PrimeCaches builds every fuzz target from a cold target and saves one
entry under the key fuzz-X64. Trace targets build with the trace
feature, matching `just fuzz`. PR and scheduled fuzz jobs only restore
it.

The fuzz workflow installs the pinned nightly before the cache step.
rust-cache hashes installed toolchains, so a pin change changes the key
and triggers a new prime.

The new `just build-fuzz-targets` recipe builds each target alone
because the trace targets do not compile without the trace feature. It
fails if `cargo fuzz list` fails or lists nothing, so an empty build is
never cached.

Signed-off-by: Ludvig Liljenberg <4257730+ludfjig@users.noreply.github.com>
Rust cache keys end in a toolchain hash and a lockfile hash. Sysroot keys
end in one hash. Every dependency or toolchain update leaves the old
entry on main until GitHub evicts it after seven days unused. Those
entries count toward the 10 GB repository quota and push out current
ones.

A final PrimeCaches job keeps the newest entry for each key without its
hashes and deletes the rest. It runs even if a prime job fails, which
keeps the old entry for that job's key.

Signed-off-by: Ludvig Liljenberg <4257730+ludfjig@users.noreply.github.com>
The v0-rust entries on main grew over many builds on restored caches.
PrimeCaches skips the build on an exact key match, so it would keep
those entries until the next lockfile change.

Every rust-cache step uses the prefix v1-rust. The first PrimeCaches
run builds every entry from a cold target. The old v0-rust entries
expire after seven days unused.

Signed-off-by: Ludvig Liljenberg <4257730+ludfjig@users.noreply.github.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

kind/enhancement For PRs adding features, improving functionality, docs, tests, etc.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants