docs(notes): separate the three nixbot findings and price the check set - #2915
Open
cameronraysmith wants to merge 2 commits into
Open
docs(notes): separate the three nixbot findings and price the check set#2915cameronraysmith wants to merge 2 commits into
cameronraysmith wants to merge 2 commits into
Conversation
| `nix eval --json .#checks.x86_64-linux --apply 'builtins.attrNames'` returns exactly 117 names, matching the 117 attributes nixbot recorded for builds 161 through 172 and 176. | ||
| The match holds across different branches, which strengthens rather than weakens it: 117 is the count both locally and on the service. | ||
| Every emitter in `modules/checks/` binds flat derivations, so `--force-recurse` finds no nested attribute sets to descend into; this is inferred, because forcing values locally triggers the import-from-derivation described above. | ||
|
|
There was a problem hiding this comment.
The parenthetical lists three distinct path counts (19,165; 15,342; 15,338) and they already account for all six users (2 + 3 + 1), so "four distinct closure shapes" doesn't match the evidence in the same sentence. If the enumeration is complete this should be three; the same "four" is reused in the Recommendations section ("the four distinct home closure shapes") and would need to match.
Suggested change
| They are reducible in *multiplicity* rather than in kind: the six home activations occupy only three distinct closure shapes (crs58 and cameron identical at 19,165; christophersmith, janettesmith and raquel identical at 15,342; tara at 15,338), so a canary per shape per pull request with the full set on scheduled or main-branch runs preserves build coverage at a fraction of the per-pull-request exposure. |
Restructures the note around three independent findings with three different owners, because presenting them as one story misattributes two of them. Per-evaluation cost is the substituter fan-out through the daemon and is the only one that is a code change. Global serialization is architectural in both build services with no safe knob at our pinned revision, and is accepted rather than tuned. Queue load is a policy choice. Queue load is new here and its count is corrected upward. The window that prompted it showed eight open pull requests with all but two automated; `gh-axi pr list` at 02:10 shows thirty open, of which twenty-three are bot-authored: seventeen from vanixiets-flake-updater and six from renovate, against seven human-authored. Measured evaluator throughput is the eval-gcroots index advancing 171 to 186 between 01:15 and 02:00, fifteen evaluations in forty-five minutes, about 180 s each. Clearing one evaluation per automated pull request therefore occupies the single evaluator for sixty-nine minutes at that mean and three hours eight minutes at build 172's measured 490 s, while build 171 alone consumed fifty-five minutes and build 168 the full 3600 s timeout before failing permanently. That is why PR 2890's check sat in progress for a hundred minutes without its own evaluation being slow. The three findings compound rather than coexist, which is the part worth stating precisely: a flake-input bump invalidates the closure and converts warm paths into all-miss paths, so the automated pull requests are each drawn from the expensive class of finding one and are metered through the one-at-a-time queue of finding two. Seventeen of them are input bumps by construction. The check set is now priced rather than counted, since it is the input that makes finding three expensive. 117 attributes matching the service's recorded count, divided into 11 FULL-ACT, 5 FULL-eval, 12 MODULE and 89 CHEAP. Measured: home-manager-crs58 evaluates in 9.26 s allocating 2.38 GB, nixos-magnetite in 10.78 s and 2.85 GB, against a 1.45 s flake floor every check pays because the eval cache is disabled. Derivation-closure paths per check via nix-store -q --requisites: 20,065-22,170 for the NixOS toplevels, 15,338-19,165 for the home activations, 715 for every structural check. Summed over 113 measured checks that is 362,816 paths against a distinct union of 29,244, and the union is the real fan-out ceiling because the daemon deduplicates per path within a run. The eleven FULL-ACT checks are 9.4% of attributes, 57.2% of summed paths, and 13,872 paths reachable only through them: 47.4% of the union. Reducibility is answered in both directions rather than as a recommendation. FULL-ACT asserts that a closure builds and no module-level evaluation can establish that, so it is genuinely full in kind and reducible only in multiplicity: six home activations occupy four distinct closure shapes, so a canary per shape costs per-user cache fill on pull request runs, which is a tradeoff and not a free win. The five FULL-eval checks should be kept, since they assert real wiring of the emitted configurations at 715-3,121 path closures and a minimal re-composition would assert a reconstruction instead. The repository already holds the cheaper idiom in hm-sops-bridge-assertion-neg and structure/*. PR 2890's devin-worker.nix is corrected on this axis: its two full home-manager evaluations at lines 76-98 and 125-145 evaluate minimal probe configurations rather than this repository's homeConfigurations and serialize only names, counts and booleans, so on the closure-fan-out axis it is a cheap check. The follow-up proposing a third was not located in the pull request body, its seven commit messages, either comment, the openspec corpus on that branch, or the devin files' own markers, and is recorded as not found rather than assumed. Also discloses a constraint breach: the check enumeration initially forced IFD, and this workstation delegates builds to ssh-ng://builder@magnetite, so about ten small derivations built there over roughly four minutes before the process was killed and --option builders '' was pinned for every subsequent invocation. No service state was mutated and nothing was cancelled, but it was host contact the investigation was told not to make. Documentation only; treefmt and gitleaks are the applicable checks and both pass.
The automated share is twenty-three of thirty open pull requests, which this note already carried from its own measurement; what it lacked was the working, so the tradeoff read as a judgement rather than as arithmetic. One full cycle of the automated set, one evaluation each, on measured inputs: 23 x 180 s = 4,140 s = 1 h 09 m at the throughput mean, 23 x 200 s = 4,600 s = 1 h 17 m at the fast class, 23 x 490 s = 11,270 s = 3 h 08 m at build 172's healthy rate. One slow-class member displaces most of a cycle by itself: substituting build 171's 3,312 s for a healthy 490 s adds 2,822 s, 47 minutes, and build 168 spent its full 3,600 s timeout to produce no verdict at all. A queue item is one evaluation per PUSH rather than per pull request, which multiplies that depth rather than adding to it. PR 2890 alone was force-pushed four times. The bot population's push rate was not measured, so it is carried as a parameter rather than asserted: at build 172's rate a cycle costs 3 h 08 m at m = 1, 6 h 16 m at m = 2 and 12 h 31 m at m = 4. The measured service rate bounds what the queue can absorb at twenty evaluations per hour, so any arrival rate above that grows the queue without bound rather than merely lengthening it. The consequence is now stated plainly rather than left to be inferred: at this depth a human pull request can wait behind hours of automated evaluation, 3 h 08 m at the healthy rate arriving behind one full cycle and 6 h 16 m if each of those is pushed twice. That is why PR 2890's check sat in progress for a hundred minutes without its own evaluation ever being slow. Batching is quantified rather than merely named. Collapsing the flake updater's seventeen pull requests into one takes a cycle from twenty-three evaluations to seven, the six dependency-bot ones plus the combined one: before 23 x 490 s = 11,270 s, after 7 x 490 s = 3,430 s, saved 7,840 s = 2 h 11 m per cycle, a 69.6% reduction, scaling with any push multiplier above one. Inferred and stated as such: the saving on the fan-out axis is larger than the 17-to-1 evaluation ratio, because seventeen separate bumps each pay their own cold fan-out over overlapping regions of the 29,244-path union while one combined bump pays that union once. The shape is unchanged deliberately. Batching and rate-limiting remain policy levers for whoever sets this repository's automation policy, presented with their costs -- lost per-input bisection, slower dependency freshness -- against accepting the queue knowingly, which stays a legitimate answer. Every figure above was recomputed from the measured inputs rather than carried over. Documentation only; treefmt and gitleaks pass.
cameronraysmith
force-pushed
the
fm/vx-nixbot-eval-throughput
branch
from
September 2, 2026 02:46
3ff2270 to
c76b948
Compare
|
Tick the box to add this pull request to the merge queue (same as
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Documentation only, following #2914 which merged while this was being written. #2914 shipped the
nix.settingschange and an initial version of the note; this restructures the note around what the later measurements established, and adds the two sections #2914 could not contain because their evidence arrived after it merged.No code changes.
modules/machines/nixos/magnetite/default.nixis untouched by this PR and stays exactly as #2914 landed it.Why the note is restructured
The original note told one story. There are three independent findings with three different owners, and presenting them as one misattributes two of them.
Finding 1, per-evaluation cost — the evaluator parked on the nix-daemon socket while every attribute's closure resolves against nine substituters. This is the only one that is a code change, and #2914 is it.
Finding 2, global serialization — both build services evaluate one pull request at a time by design (
util.MasterLock("nix-eval")andeval_concurrency = 1), and neither is exposed by nixbot's NixOS module at pinned rev660d7182. Architectural, accepted, not tuned.Finding 3, queue load — a serial evaluator behind a standing queue of automated pull requests produces the observed wall clock arithmetically. This is a policy lever, not a configuration one.
New: finding 3, with its numbers
Measured:
eval-gcrootsadvanced 171 → 186 between 01:15 and 02:00 — fifteen evaluations in forty-five minutes, ~180s each, separate runs with gaps rather than a retry loop.The queue is larger than the prompting window suggested, and the count is corrected upward.
gh-axi pr list --state openat 02:10: thirty open PRs, twenty-three bot-authored — seventeenvanixiets-flake-updater(2891, 2892, 2895-2909) and sixrenovate(2613, 2881, 2910-2913) — against seven human-authored. So 23 of 30, not the six of eight the window showed.On a strictly serial evaluator, one evaluation per automated PR is 4140s ≈ 69 min at the measured mean, or 11270s ≈ 3h08m at build 172's 490s. A single slow-class member beats the whole fast sweep: build 171's 3312s consumed 55 minutes of the only evaluator, and build 168 consumed the full 3600s timeout then failed permanently. That is why PR 2890's check sat in progress for 100 minutes without its own evaluation being slow.
The findings compound rather than coexist. A flake-input bump invalidates the closure and converts warm paths to all-miss, so the automated PRs are each drawn from finding 1's expensive class and metered through finding 2's one-at-a-time queue. Seventeen of them are input bumps by construction. Options are stated as a genuine choice: batch the seventeen (loses per-input bisection), schedule or rate-limit whatever opens them (slower dependency freshness), or accept the queue knowingly — in which case a human PR's check latency is set by bot queue depth at landing and should not be read as a regression.
New: the check set, priced rather than counted
This is the input that makes finding 3 expensive, and it is in our hands rather than the tool's.
117 attributes, matching the service's recorded count: 11 FULL-ACT (6 home-manager
activationPackageinhome.nix:38; 5 NixOSsystem.build.toplevelinmachines.nix:32), 5 FULL-eval, 12 MODULE, 89 CHEAP.Measured evaluation cost, warm,
--option eval-cache falseto match nixbot:home-manager-crs589.26s / 2.38 GB / 20.4M thunks;nixos-magnetite10.78s / 2.85 GB / 27.1M thunks; FULL-eval 2.58-3.62s; MODULE and CHEAP 1.43-1.63s against a 1.45s flake floor every check pays because the eval cache is disabled.Derivation-closure paths via
nix-store -q --requisites, 113 of 117 measurable on darwin: NixOS toplevels 20,065-22,170, home activations 15,338-19,165, every structural check 715. Summed 362,816, distinct union 29,244 — the union is the real fan-out ceiling because the daemon deduplicates per path within a run, and the sum overstates. The eleven FULL-ACT checks are 9.4% of attributes, 57.2% of summed paths, and 13,872 paths reachable only through them: 47.4% of the union. At ~1.27s per all-miss path a fully cold union bounds at ~10.3h; build 172's ~416s wait implies only a few hundred path-equivalents were actually unknown. Dropping FULL-ACT would cap that ceiling at 15,372 paths, −47%.crs58's home config is evaluated 4x and magnetite's NixOS config 3x.
nix-eval-jobsdispatches each attribute independently, so no memoisation is possible across attributes — only reducing what is bound as a check reduces fan-out.Reducibility is answered in both directions rather than as a recommendation. FULL-ACT asserts that a closure builds, which no module-level evaluation can establish, so it is genuinely full in kind and reducible only in multiplicity: the six home activations occupy four distinct closure shapes, so canary-per-shape with the full set on scheduled runs costs per-user cache fill on PR runs — a tradeoff, not a free win. The five FULL-eval checks should be kept: they assert real cross-module wiring of the emitted configurations at 715-3,121 path closures, and a minimal re-composition would assert a reconstruction instead of the deployed configuration. The repo already holds the cheaper idiom in
hm-sops-bridge-assertion-negandstructure/*, so the activation bindings are the outlier and were introduced deliberately for cache fill.Correction on #2890's
devin-worker.nix: it does contain two full home-manager evaluations (lines 76-98 and 125-145, probingaarch64-darwinandx86_64-linux), but they evaluate minimal probe configurations rather than this repository'shomeConfigurations, and the check serializes only names, counts and booleans. On the closure-fan-out axis it is a cheap check. The follow-up proposing a third was not located — the PR body, all seven branch commit messages, both comments, the openspec corpus on that branch, and the devin files' own markers were searched. Recorded as not found rather than assumed; if it lives in Linear a pointer would let it be quoted.Also revised
The storage audit verdict is unchanged and restated with its evidence intact: GC and optimise each fired successfully 63 of 63 retained journal days, tuning commits
aca5c708bande456caf62are live and holding,/nixis 62.8G of a 250G quota, and storage is firmly not implicated.The "will this restore 9m41s" answer is sharpened by finding 3: it compresses the wait that is 85% of a healthy evaluation, but it cannot return check latency to 9m41s while twenty-three automated PRs are queued ahead, because latency is queue depth × per-evaluation cost and #2914 addresses only the second factor.
Disclosure
The note now discloses a constraint breach during investigation. The check-set enumeration initially forced import-from-derivation, and this workstation delegates builds to
ssh-ng://builder@magnetite, so about ten small derivations (cilium CRD YAML conversions) built there over roughly four minutes before the process was killed and--option builders ''was pinned for every subsequent invocation. No service state was mutated, nothing was cancelled, and no store-wide operation ran, but it was host contact the investigation was told not to make and it consumed a few minutes of the machine the operator was waiting on. Recorded in the note rather than omitted.Verification
Documentation only, so
treefmtandgitleaksare the applicable checks and both pass via pre-commit. No derivation changed:modules/is untouched by this PR, whichgit diff --stat origin/mainconfirms as a single file underdocs/notes/. The full check set is deliberately not run, since nothing it covers is modified.Update: queue arithmetic shown, against twenty-three and per push
The automated share was already twenty-three of thirty here, measured independently via
gh-axi pr list --state openrather than a truncated listing. What was missing is the working, so the tradeoff read as a judgement rather than as arithmetic. Added in3ff227061.One full cycle of the automated set, one evaluation per bot-authored PR, on measured inputs:
One slow-class member displaces most of a cycle by itself: substituting build 171's 3,312s for a healthy 490s adds 2,822s / 47 min, and build 168 spent its full 3,600s timeout to produce no verdict at all.
A queue item is one evaluation per PUSH, not per PR, which multiplies that depth rather than adding to it. PR 2890 alone was force-pushed four times. The bot population's push rate was not measured, so it is carried as a parameter rather than asserted:
The measured service rate bounds what the queue can absorb — fifteen evaluations in forty-five minutes is twenty per hour — so any arrival rate above twenty evaluations/hour grows the queue without bound rather than merely lengthening it.
Stated plainly, because it is the consequence worth deciding on: at this depth a human pull request can wait behind hours of automated evaluation. A human PR arriving behind one full automated cycle waits 3h08m at the healthy rate before its own evaluation begins, and 6h16m if each of those PRs is pushed twice. That is why PR 2890's check sat in progress for 100 minutes without its own evaluation ever being slow.
What batching saves, per cycle. Collapsing the flake updater's seventeen PRs into one combined PR takes a cycle from twenty-three evaluations to seven — the six dependency-bot PRs plus the combined one:
At the 180s mean the same collapse saves 16 x 180 = 2,880s / 48 min per cycle, and the saving scales with any push multiplier: 2h11m becomes 4h21m at m = 2.
Inferred and marked as such, because it makes batching cheaper than the evaluation count alone suggests: seventeen separate input bumps each pay their own cold fan-out over overlapping regions of the 29,244-path union, whereas one combined bump pays that union once — so the saving on finding 1's axis exceeds the 17-to-1 ratio in evaluation count.
The shape is deliberately unchanged. Batching and rate-limiting remain policy levers owned outside this repository, presented with their costs — lost per-input bisection, slower dependency freshness — against accepting the queue knowingly, which stays a legitimate answer. Recommendations, not decisions.