WIP: make nix flake check use the eval cache for derivation outputs#1
WIP: make nix flake check use the eval cache for derivation outputs#1Andrew Gazelka (andrewgazelka) wants to merge 1 commit into
Conversation
Adversarial review (max-effort, blank-context agent)Generated by Claude Code (Opus 4.8) as an independent reviewer; relayed verbatim. Bottom line: approach is sound, diff is small and reviewable. Dirty-tree fallback is correct (null fingerprint → null BlockersB1 — warm-run errors for broken cached outputs are opaque. The eval cache stores failures ( B2 — no tests. Add functional tests: cache-hit on 2nd run (assert via MajorM3 — M4 — blanket Minor / nits
Thread-safety
Top 3 before upstreamable
|
…WIP) Route checks/packages/devShells/formatter through eval-cache attribute cursors (isDerivation/forceDerivation) so a repeat `nix flake check` on an unchanged committed tree is served from ~/.cache/nix/eval-cache-*, like nix build and nix flake show already are. Cheap structural outputs keep the existing value-based path. Addresses the long-standing FIXME in CmdFlakeCheck. See flake-check-eval-cache-DESIGN.md. Refs indexable-inc/index#405, NixOS#4279. Made with Claude Code (Opus 4.8).
4301c89 to
9541de3
Compare
Benchmark note — large flake (indexable-inc/index)I couldn't capture a clean cold→warm number on the large flake, and it's worth saying why rather than posting a bogus figure. The index flake's full check is ~25 min cold, and the repo was being actively committed to during that window (the macos-vm PR landed mid-run), so the store snapshot/fingerprint shifted and the cold leg hit an unrelated eval error ( The small-flake numbers above stand as the proof (cold 6.9s → warm 0.35s, fully cache-served, identical derivations). The mechanism is identical at scale: after one clean cold run on a committed, unchanging tree, the warm run is served from the cache. The honest caveat from the PR body still applies — the win is for repeat checks at the same commit; any new commit is cold again. |
Status: WIP / draft. Compiles, correct on passing and failing flakes, and the eval-cache speedup is demonstrated (numbers below). Opened as a draft so review happens through a protected-branch PR.
What
CmdFlakeCheck::run(src/nix/flake.cc) carried// FIXME: rewrite to use EvalCache.for ~5 years and force-evaluated every output on every run, so a repeatnix flake checkon an unchanged committed tree paid the full eval cost again, unlikenix build/nix flake show, which read the eval cache viaeval_cache::AttrCursor.This routes the heavy derivation-bearing outputs (
checks,packages,devShells,formatter) through the eval cache (isDerivation()+forceDerivation()), and lets the existing value-based traversal handle the cheap structural outputs (apps,overlays,modules, …), which it skips for the names the cache handled (cachedOutputs).Benchmark
Built from this branch (meson/ninja), nix 2.35, aarch64-darwin.
Small flake (3 nixpkgs derivations + a check), committed tree,
nix flake check:using cached attribute=10,evaluating uncached=0~20× on the warm run; identical derivations checked; exit parity with stock.
Large flake (
indexable-inc/index, ~140 outputs incl. cross-system OCI closures): cold full check is ~20–25 min (intrinsic eval cost, unrelated to this change); the warm cold→warm number from a no-cache-wipe run is being measured and will be appended here.Important caveat (inherent nix eval-cache limitation, not this patch)
The speedup applies only to a repeat check on an unchanged, committed tree. A dirty working tree — or any new commit — changes the flake fingerprint, so the cache misses and the run is cold again (same as
nix build/flake show). So the winners are CI re-checking the same commit, or running the check twice without changes; the active edit→check loop is unchanged.Correctness validation
throw): shows the real error on cold and warm, with and without--keep-going(cached failure of attribute …never leaks). Fixed by catchingCachedEvalErrorin the cursor path and callinge.force()to re-surface the real error, mirroringsrc/nix/main.cc.--keep-going: a malformed/failed system-level attrset is reported and skipped, not fatal (cursor traversal wrapped in try/reportError).Why hybrid, not a full rewrite
Forcing a
Valueis the cost, so the traversal must be cursor-based; you cannot bolt caching onto the existingforceValueloop. The cursor API serves derivation checks cleanly but not structural ones (anappstype/programshape, a 2-argoverlaysfunction, importable modules), which still need a realValueviaAttrCursor::forceValue().Follow-ups before upstreamable
addTrace(nullptr, …),noPos→at «none»); the eval cache exposes noPosIdx. Decide: extend the cache, or accept/document. (This was the sticking point on prior attempts, Cache evaluation for eval and flake check NixOS/nix#4279, Flake schemas NixOS/nix#8892.)make_ref<flake::LockedFlake>(flake)deep-copies; holdflakeas areflikeCmdFlakeShow.reportError'sthrow e;on aconst Error&slices; minor, fix when adding tests.Refs: indexable-inc/index#405, NixOS#4279, NixOS#8892.
Made with Claude Code (Opus 4.8).