Skip to content

Fine-tune CI caching: nix phase is ~32 of 35 min in the fixture gate; budget 88% full #348

Description

@vicondoa

Summary

CI caching now works but is not tuned. Measured on PR #343, the repository cache sits at 8.78 GB across 19 entries against GitHub's ~10 GB LRU budget, roughly 88% full, and a meaningful share of that is dead weight. Separately, the test-fixture-contracts nix cache is demonstrably working but still leaves 27 derivations to build, and the most likely cause is a size cap that is pruning the store before save.

This issue records the measurements so the tuning can be done against numbers rather than intuition.

What already works

Scoping the nix store cache per job (PR #343) fixed a real defect: all twelve nix jobs previously shared one key, and because actions/cache never overwrites, whichever trivial job finished first froze a 90 MB entry that every other job then "hit". Measured before and after on test-fixture-contracts:

signal shared key job-scoped
cache entry size 90 MB 2217 MB
restore step duration 8s 51s
derivations to build 166, then 411 27

last_accessed_at on the 2217 MB entry matches the restore step timestamp exactly, confirming the job consumes it.

Worth stating for anyone reading a build log: cache-nix-action restores the /nix store directory, it is not a substituter. Nix therefore never prints copying path ... from ... for cached content - it simply finds the paths present and drops them from the build plan. The observable evidence is the action's own restore, last_accessed_at, and the derivation count. Absence of substituter lines is not absence of caching.

Current cache inventory

2217MB  nix-Linux-test-fixture-contracts-e28042def...
2094MB  v0-rust-test-rust-Linux-Linux-x64-21820c02-b94d5870
1835MB  v0-rust-test-rust-Linux-Linux-x64-4ece3d53-7027ccd5
1818MB  v0-rust-test-rust-Linux-Linux-x64-4ece3d53-0a5bbf36
 130MB  nix-Linux-test-nix-unit-e28042def...
 126MB  nix-Linux-test-rust-e28042def...              <- stale
 110MB  nix-Linux-flake-eval-x86-e28042def...         <- stale
  95MB  nix-Linux-flake-eval-x86-outputs-e28042def... <- stale
  90MB  nix-Linux-test-drift-e28042def...             <- stale
  90MB  nix-Linux-flake-eval-discover-e28042def...    <- stale
  90MB  nix-Linux-e28042def...                        <- stale (original shared key)
  89MB  nix-Linux-test-flake-aarch64-e28042def...     <- stale

total: 8.78 GB across 19 entries

Findings to tune

1. gc-max-store-size-linux: 4G is probably truncating the fixture store

test-fixture-contracts still builds 27 derivations on a warm cache. The obvious explanation - that a source change invalidated them - was tested and falsified: git diff <seed-commit>..HEAD -- packages/ is empty, so the fixture derivation is byte-identical between the seeding run and the consuming run, and the cache should have covered it entirely.

The remaining hypothesis is the size cap. gc-max-store-size-linux prunes the store before save, so anything beyond the ceiling is dropped and must rebuild. The entry is 2217 MB compressed, consistent with an uncompressed store at or above the 4G cap.

The cap was lowered from 8G to 4G during review specifically to protect the aggregate budget, so raising it is a genuine tradeoff, not a straightforward fix.

To confirm before changing anything: read the 27 derivation names from a completed test-fixture-contracts log. If they are Rust host-tool derivations, the cap is truncating. If they are per-run inputs, the cap is correct and 27 is the floor.

2. Stale per-job nix entries occupy ~730 MB

Seven nix-Linux-* entries exist for jobs that are no longer in NIX_CACHED_JOBS (test-rust, flake-eval-x86, flake-eval-x86-outputs, test-drift, flake-eval-discover, test-flake-aarch64), plus the original shared nix-Linux-<hash> key. They will never be refreshed and will not be read. They occupy budget until LRU reclaims them.

purge-prefixes only purges within a job's own scope, so a job that no longer renders a cache step never purges its own leftovers.

Options: delete them once via gh api --method DELETE, or add a scheduled workflow that reaps nix-* entries whose prefix is not in the current NIX_CACHED_JOBS.

3. rust-cache is the dominant consumer at ~5.7 GB

Three generations at ~1.9 GB each, keyed on Cargo.lock hash. This is the single largest claim on the budget and was not examined during the nix cache work.

Worth asking whether three live generations is the right number, or whether the key should be narrowed so old lockfile generations retire faster.

4. MATRIX_CHECK_SCOPE has no consumer

flake-eval-x86 shards share one ciJobId, so per-job scoping alone would let them clobber each other. A scope_suffix was added and threaded through, but that job is not in NIX_CACHED_JOBS, so the code path is currently dead. It was verified by hand (temporarily adding the job, regenerating, confirming the key renders nix-${{ runner.os }}-flake-eval-x86-${{ matrix.check }}-...) but nothing pins it.

If those shards are ever added to the cached set, that path needs a test. Note the shards each complete in under a minute today, so there is currently no case for caching them.

5. sccache is deliberately absent from CI

Recorded here so it is not re-attempted without knowing the outcome. Enabling sccache in CI breaks the build: its server forwards only a whitelist of environment variables to rustc, and CARGO_BIN_EXE_<name> - which Cargo injects so an integration test can locate the binary it exercises - is not among them. The failure is:

error: environment variable `CARGO_BIN_EXE_d2b-exec-runner` not defined at compile time
   --> d2b-exec-runner/tests/tty_pty_integration.rs:359

It does not reproduce locally because CARGO_INCREMENTAL is on there, so sccache marks those units non-cacheable and passes them through untouched. CI sets CARGO_INCREMENTAL=0, sccache genuinely caches, and the variable goes missing.

sccache remains enabled for local development through packages/.cargo/config.toml and the .cargo/rustc-wrapper.sh shim, where it was verified to cache properly (miss, then 100% hit on an identical rebuild).

If CI sccache is ever wanted, the lever is excluding the CARGO_BIN_EXE_*-dependent test targets from caching - a real tradeoff needing its own decision.

Suggested order

  1. Read the 27 derivation names from a completed fixture log and decide whether the 4G cap is the cause.
  2. Reap the seven stale nix-Linux-* entries and decide whether reaping should be automated.
  3. Re-measure total usage; only then adjust NIX_CACHE_MAX_STORE.
  4. Separately review the rust-cache generation count, which is the largest consumer.

Context

Caching was introduced and scoped in PR #343. The .gitignore/scratch-path cleanup is tracked separately in #346.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions