docs: record lessons 91 and 92 and two rules - #1378
Conversation
dekobon
left a comment
There was a problem hiding this comment.
Code Review: docs: record lessons 91 and 92 and two rules
Verdict: REQUEST CHANGES
| Severity | Count |
|---|---|
| HIGH | 1 |
| MEDIUM | 3 |
Findings
| # | Severity | File:Line | Title | Category |
|---|---|---|---|---|
| 1 | HIGH | docs/development/lessons_learned.md:3603 |
This lesson currently lands before the fix it describes | correctness |
| 2 | MEDIUM | docs/development/lessons_learned.md:3573 |
Actix also supports plaintext HTTP/2 via H2C | correctness |
| 3 | MEDIUM | docs/development/lessons_learned.md:3604 |
The slot lifecycle omits the paths that needed separate freeing | correctness |
| 4 | MEDIUM | docs/development/lessons_learned.md:3614 |
This attribution paragraph sharpens the evidence past what #1375 established | correctness |
Review passes executed
- Documentation correctness and logic
- Factual verification against issue, PR, and source history
- Documentation consistency and test-guidance review
Files reviewed: 3 changed files, plus the referenced source and issue history.
| moves whether they are freed or kept. | ||
|
|
||
| **The nesting map's reserve was sized to the leak it should have | ||
| prevented** (#1069, fixed in #1375 / PR #1377). `NestingMap` carries one |
There was a problem hiding this comment.
Location: docs/development/lessons_learned.md:3603
HIGH -- This lesson currently lands before the fix it describes
PR #1377 is still open, and #1378 is based directly on main rather than on perf/1375-nesting-map-lifetime. In this branch src/spaces/compute.rs still allocates NestingMap::with_capacity_and_hasher(node.descendant_count(), ...), still uses get in propagate_nesting_to_children, and has no nesting_slots_retained or final debug_assert!. If this merges first, both this lesson and the new testing rule state that the leak is fixed while main still contains the old behavior. Please stack/rebase this branch on #1377 or ensure #1377 lands first.
There was a problem hiding this comment.
Fixed — you are right, and my split reasoning was wrong. I branched off main to keep the unrelated h2 lesson out of the code fix, but that traded a topical problem for a correctness one: merged first, both the lesson and the testing rule would describe a fix main does not contain.
Rebased onto perf/1375-nesting-map-lifetime and retargeted this PR's base to it, so #1378 is now stacked on #1377 and its diff is just the two docs commits. It cannot land first.
| alert #776; `3df3b2c3`, `b60943b5` — direct pushes to `main`, no PR). | ||
| The crate reached the tree only through `actix-web`'s default `http2` | ||
| feature, and no patched 0.3 release exists, so the fix was to drop the | ||
| feature: `bca-web` binds plaintext and actix negotiates HTTP/2 only over |
There was a problem hiding this comment.
Location: docs/development/lessons_learned.md:3573
MEDIUM -- Actix also supports plaintext HTTP/2 via H2C
HttpServer 4.15 exposes bind_auto_h2c() and listen_auto_h2c() for plaintext HTTP/2; only bca-web's current .bind((host, port)) call keeps HTTP/2 unreachable here. Saying Actix negotiates HTTP/2 only over TLS ALPN makes the rationale false and would mislead a future switch to H2C. Please tie the statement to this daemon using .bind() rather than to Actix generally.
There was a problem hiding this comment.
Fixed, and thank you — the claim was false and I inherited it from 3df3b2c3's own commit message rather than checking it.
Verified: bind_auto_h2c is at actix-web-4.14.0/src/server.rs:513 (listen_auto_h2c at 685), so actix does speak plaintext HTTP/2. The entry now ties the rationale to this daemon's plain HttpServer::bind at web/server.rs:303 instead.
One thing the check turned up that strengthens the fix: bind_auto_h2c is itself #[cfg(feature = "http2")], so dropping the feature removes the H2C entry point along with the vulnerable crate. That is now in the entry, since it means the guard does not rest on the bind call staying as it is.
|
|
||
| **The nesting map's reserve was sized to the leak it should have | ||
| prevented** (#1069, fixed in #1375 / PR #1377). `NestingMap` carries one | ||
| `Nesting` per node id, seeded by the parent, read once by the node's own |
There was a problem hiding this comment.
Location: docs/development/lessons_learned.md:3604
MEDIUM -- The slot lifecycle omits the paths that needed separate freeing
Before #1377, a leaf slot was not read again during child seeding because propagate_nesting_to_children returned before looking it up when children.is_empty(), and an exclude_tests-pruned node skipped both compute and propagation. Those are exactly why #1377 removes the leaf bail-out and adds a separate prune-arm removal. As written, this says every slot had the same two reads and hides the edge cases the fix has to preserve.
There was a problem hiding this comment.
Fixed. The simplification hid exactly the two shapes the fix has to treat specially, which is the opposite of what the entry should leave a reader with.
It now names both: a leaf returned early on children.is_empty(), and an exclude_tests-pruned node reached neither compute nor propagation — so freeing in the last reader means dropping the leaf bail-out and adding a removal on the prune arm.
| the live set bounded by the traversal stack: 735 MB, and 3.3 s against | ||
| 5.1 s. | ||
|
|
||
| **All three hypotheses in the issue were wrong** (#1375). It proposed |
There was a problem hiding this comment.
Location: docs/development/lessons_learned.md:3614
MEDIUM -- This attribution paragraph sharpens the evidence past what #1375 established
#1375's body does not propose the twice-materialized path list as one of its three suspects; that came from #1158. Its follow-up says the body suspects were secondary, not all wrong, and specifically records MALLOC_ARENA_MAX=1 lowering the peak to 3.4 GB, so glibc arena retention was real even if it was not the dominant term. The same table reports stdout at 1,265 MB and --output-dir at 1,266 MB, not "byte-for-byte" identical. Please preserve those measured distinctions.
There was a problem hiding this comment.
Fixed, all three. Checked each against the issue body and the follow-up:
- The twice-materialized path list was perf(cli): stream walked paths into the worker pool instead of materializing them #1158's suspect, not perf(cli): peak RSS scales with jobs × largest file (5.1 GB over a 100k-file tree) #1375's. perf(cli): peak RSS scales with jobs × largest file (5.1 GB over a 100k-file tree) #1375 listed the per-file working set, the reorder buffer, and arena retention. Corrected.
- "All three wrong" overstated it.
MALLOC_ARENA_MAX=1did reach 3.4 GB, so arena retention was real. The heading is now "every suspect the issue named turned out to be secondary", and the paragraph says none was wrong to suspect and the last was demonstrably real. --output-dirwas 1,266 MB against stdout's 1,265 MB. "Byte-for-byte" is now "within a megabyte", with both numbers.
Worth noting this finding is lesson 84 catching the entry that sits two above it in the same file.
64fa25e to
57af48e
Compare
Two mechanisms from the 2026-08-29/30 window that nothing in the file named, plus the two takeaways that are standing obligations rather than bug classes. Lesson 91 — a gate can filter out its own subject before the check runs. The `[bans] deny` on `h2 <0.4.16` that RUSTSEC-2026-0258 invites is dead by construction: krates drops the crate from the graph before cargo-deny's advisory and ban checks run, on 0.19 and on the 0.20 that CI's action bundles, so the entry stays silent even with `actix-web/http2` re-enabled. The lockfile test is the guard that actually fails. Lesson 92 — an optimization's rationale can encode the waste it optimizes for. #1069 measured that the cognitive nesting map converges on one entry per visited node and reserved that much up front, which is a real speedup for growth that should not have existed: 540 MB of a 1,265 MB peak on a 28 MB generated `parser.c`. It carries the attribution technique as its second mechanism, because all three hypotheses in #1375 were wrong and only bisecting by selection settled it. `AGENTS.md` gains the issue-tracker form of lesson 84: re-verify a deferred issue's premises, since #1158 proposed an ordering contract that #1303 had shipped the opposite of and its own go/no-go measurement had never been run. `.claude/rules/testing.md` gains the whole-run-invariant rule: when an invariant holds at the end of every execution of a hot path, a `debug_assert!` there recruits the whole corpus as input, which is a different order of coverage from any fixture list. Both guards, and keep the assertion O(1) — a per-node one is how the exact ancestor check went quadratic.
Ties the h2 rationale to this daemon's plain `HttpServer::bind` rather than to actix, which does speak plaintext HTTP/2 through `bind_auto_h2c` — itself gated on the feature being dropped. Names the two slot shapes that skip the last read, since the leaf bail-out and the `exclude_tests` prune arm are exactly what the fix has to handle separately. Corrects the attribution paragraph: the twice-materialized path list was #1158's suspect, not #1375's; arena retention was real but not dominant (`MALLOC_ARENA_MAX=1` reached 3.4 GB); and `--output-dir` came within a megabyte of stdout, 1,266 against 1,265 MB, rather than matching it.
57af48e to
9c93e05
Compare
|
Landed on
#1377 registered as merged because its head was reachable from its base. This PR could not: its base was the stacked branch, and once Both commits from this PR are on |
Two lessons and two rules from the 2026-08-29/30 window. Both lessons
name a mechanism nothing in the file covers; both non-qualifying
takeaways were routed to rules instead, per the file's own "standing
obligations live in rules, not here" preamble.
Lesson 91 — a gate can filter out its own subject before the check runs
The belt-and-braces guard for RUSTSEC-2026-0258 would be a
[bans] denyon
h2 <0.4.16. It is dead by construction: krates, cargo-deny's graphbuilder, filters the crate out before the advisory and ban checks run
(
-L debuglogsfiltered h2 0.3.27), on 0.19 and on the 0.20 thatCI's action bundles. Re-enabling
actix-web/http2under both versionsleft cargo-deny silent — so the entry would have looked like coverage
and could never have fired.
What makes it worth an entry is the shape: a config entry naming the
crate and the version range reads as a guard to every future
maintainer, and the tool exits 0, which is exactly what it prints when
the tree is genuinely clean. The working guard reads
Cargo.lockfrom atest, and
deny.tomlcarries a comment where the dead entry would havegone.
Cited by commit hash rather than PR — both are direct pushes verified
reachable on
mainwith no PR, which is the one case the skill'sguardrail permits.
Lesson 92 — an optimization's rationale can encode the waste it optimizes for
#1069 measured that the cognitive nesting map "converges on one entry
per visited node" and reserved
descendant_count()up front to skip thedoubling chain. That is a genuine speedup — for growth that should not
have existed. The slot is dead after its children are seeded and was
never removed, so the reserve was sized to the leak: 540 MB of a
1,265 MB peak on a 28 MB generated
parser.c, fixed in #1375 / PR#1377.
The second mechanism is how it was found. All three hypotheses in the
issue (reorder buffer, path list materialized twice, glibc arenas) were
wrong, and code reading could not discriminate them because every
allocation suspected really is present. Bisecting by selection settled
it:
dumpmeasured 722 MB and every non-cognitive metric 723–725 MBagainst cognitive's 1,264 MB,
--output-dirmatched stdout in peak, andbca check— which has no reorder buffer at all — still peaked at4.5 GB.
Routed to rules instead
AGENTS.md, GitHub workflow: re-verify a deferred issue'spremises before building on them. perf(cli): stream walked paths into the worker pool instead of materializing them #1158 proposed an output-ordering
contract that fix(cli): metrics/ops stdout document order is nondeterministic at --jobs > 1 #1303 had shipped the opposite of, with tests, and its
own go/no-go measurement had never been run; running it closed the
issue. This is the issue-tracker form of lesson 84.
.claude/rules/testing.md: assert a whole-run invariant in therun, not in a fixture list. A
debug_assert!at the end of a hot pathrecruits the entire corpus as input — 3,590 lib tests plus every
integration suite, against the four fixtures a named test lists — and
that is what established perf(cli): peak RSS scales with jobs × largest file (5.1 GB over a 100k-file tree) #1375's invariant across all twenty
languages. Both guards, not either, and keep the assertion O(1): a
per-node one is how the exact ancestor-chain check went quadratic
(test(perf): Ancestors::checked re-derives every parent, making debug-build walks quadratic #1122).
Notes
~60-line two-mechanism budgets.
rumdlpasses; both index anchorsresolve.
parser_cachedoc-claim instance into lesson 84 (one more instance ofa mechanism it already documents with eight), and lesson 59's 81-line
overrun against the 75-line stop.