Skip to content

Commit c2ca7ba

Browse files
Brian Loveclaude
authored andcommitted
docs: triage the next perf cycle's levers; abort the confirmation round
The nightly cycle's objective 1 (controls-in-band confirmation of the #490 arc's estimate-met 120ms bar) did not run: 1-min load 6.37 against a <5 precondition, swap exhausted (8775M/9216M), a VM at 258% CPU, and port 4173 held by a parallel session. No bench, no trace, no bar claimed — the arc's verdict stays estimate-met at ~116 fit-estimate. Objective 2 is done as load-independent static analysis. Central finding: the lever list inherited from the arc is pre-M2 attribution of a 127ms window, and both #518 and M2 targeted the runner term since. In particular the "~10% snapshot HAMT reads" lever looks mis-attributed — every flat-root rows.get is viewport-sized, and the identity lane's build path takes no store get at all. One real candidate survives: under adoption the identity lane pays two WeakMap gets on the same key per survivor (filterVerdict discards the entry it looked up, then fillSortKeysFromPrevious looks it up again to hit an early return), plus two instanceof revalidations — ~500k redundant lookups per 50k keystroke settle. Fusing them is proposed with a pre-registered falsifiable prediction, gated on the re-trace. The columnar verdict cache is closed rather than deferred: it can only cache filter-predicate evaluation, which the compiled path already reduced to ~1% of the settle window, so its saving is bounded at ~1ms before paying for commit-side clears. That explains the two prior flat A/Bs rather than excusing them on regime. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 494e533 commit c2ca7ba

1 file changed

Lines changed: 211 additions & 0 deletions

File tree

Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
# Next perf cycle — brainstorm and lever triage (post-#490 arc) — 2026-08-29
2+
3+
Scheduled nightly cycle. Two objectives were set: (1) a controls-in-band
4+
confirmation round for the #490 arc's estimate-met bar, and (2) a
5+
brainstorm/spec for the next cycle. **Objective 1 was not run** — the
6+
regime preconditions failed and were still failing at the end of the
7+
window. Objective 2 was done as a load-independent static analysis, and
8+
its central finding is that the lever list inherited from the arc is
9+
**stale attribution** that must be re-traced before anything is built.
10+
11+
Worktree: `blove/perf-cycle-2026-08-29` off `origin/main` @ `bac186da`
12+
(#529). No engine code changed in this document's commit.
13+
14+
## Objective 1 — ABORTED, with the regime evidence
15+
16+
Preconditions from the cycle definition: `lsof -i :4173` empty and 1-min
17+
load < 5. Both failed at fire time and never recovered:
18+
19+
| Check | At fire (22:00) | At end of window (22:02) |
20+
| ------------ | ----------------------------------------------------- | ------------------------ |
21+
| 1-min load | **6.37** (5/15-min: 9.55 / 8.46) | **6.25** (8.70 / 8.25) |
22+
| Swap | **8775M used of 9216M**, 440M free | unchanged |
23+
| Port 4173 | held by another `Claude` PID 24077 (3 CLOSED sockets) ||
24+
| Top consumer | `Virtualization.VirtualMachine.xpc` at **258% CPU** | still running |
25+
26+
A VM pinning ~2.6 cores, a parallel Codex session, a running eslint, and
27+
exhausted swap on a 10-core Mac. Per the standing rule the port holder was
28+
**not** killed. No bench was run, no trace was captured, and **no bar is
29+
claimed**.
30+
31+
**The #490 arc's verdict is therefore unchanged and still estimate-met:**
32+
50k filter-metadata ~124.1 loaded → **~116 fit-estimate against the ≤120
33+
primary bar**, keystroke warm p50 51.2 loaded against the ≤130 bar. The
34+
confirmation round is still owed. No addendum was written to
35+
`2026-08-29-dense-flat-cooperative-candidate-results.md`, because writing
36+
one without the measurement is exactly the thing the cycle forbids.
37+
38+
**Carry-forward:** the confirmation round is the first item of the next
39+
cycle that fires on a quiet machine. It is cheap (~15 min) and it is the
40+
only thing standing between "estimate-met" and "met".
41+
42+
## Objective 2 — lever triage
43+
44+
### The headline finding: the inherited lever list is pre-M2 attribution
45+
46+
The three levers on record — snapshot HAMT reads ~10%, sort-key carry fill
47+
~8%, residual runner overhead ~15% — come from the **post-M1 trace of a
48+
127 ms settle window**. Since that trace, two things landed that changed
49+
the profile's shape:
50+
51+
- **#518** amortized the shared slice clock to a 32-unit stride (main's own
52+
50k filter went ~141.6 → ~133 in the same regime), and
53+
- **M2** (`17385023`/`28fa68f5`/`04fe0a2a`) re-denominated the identity
54+
lane's build unit from one row to one slot-vector chunk — cutting the
55+
50k sweep from ~196 scheduler hops to ~50.
56+
57+
Both edits attack the _runner overhead_ term specifically. The window is
58+
now ~124 ms, and the shares inside it have not been re-measured. **Any
59+
percentage quoted from the arc doc is a percentage of a different
60+
window.** Re-tracing is a gate, not a formality.
61+
62+
### Static confirmation that one inherited share is mis-attributed
63+
64+
Reading the flat snapshot's read path (`visible-index.ts`), every
65+
`root.rows.get(...)` on a flat root is **viewport-sized, not row-count
66+
sized**:
67+
68+
- `visible-index.ts:220` (`publicRowAt`) — one get per requested row.
69+
- `visible-index.ts:236` (`range`) — one get per row in `[start, end)`.
70+
- `visible-index.ts:267` (`ɵslotOfRowId`) — one get per call.
71+
72+
A 50k filter settle asks for a viewport (tens of rows) per commit, not
73+
50 000. Sweeping every `.rows.get(` site in `packages/` (24 hits, none in
74+
tests) finds no per-row HAMT read on the identity lane's build path at
75+
all: `sweepChunk``carryRecord` touches `filterVerdict`,
76+
`fillSortKeysFromPrevious`, a transient tree insert and a membership bit —
77+
no store get. The two candidate-lane gets
78+
(`flat-cooperative-candidate.ts:350`, `:438`) are on the **replay** and
79+
**evaluate-lane** paths, neither of which the warm identity lane runs.
80+
81+
So "snapshot `rows.get` ~10%" is unlikely to survive re-tracing as a
82+
snapshot-read cost. The re-trace's first job is to find out what that
83+
10% actually is now. **Do not build a dense read path against it.**
84+
85+
For the record, if a dense read path _is_ ever wanted, the substrate is
86+
already there and already invariant-pinned: `root.recordsBySlot` is a
87+
per-revision `SlotVector<RowRecord>` on every `RevisionRoot`, and
88+
`__tests__/records-by-slot.test.ts` pins
89+
`slotVectorGet(recordsBySlot, record.slot) === record` (identity) for
90+
every record in `rows`. Turning a `rows.get(rowId)` into a
91+
`slotVectorGet(recordsBySlot, slot)` is two array indexes against a trie
92+
descent. It is a _cheap_ change whenever a site is shown to be hot. No
93+
site has been shown to be hot.
94+
95+
### Lever A (the one concrete, real candidate): the double WeakMap get per survivor
96+
97+
This one is load-independent and readable straight off the source.
98+
99+
On the warm keystroke path the candidate takes the identity lane
100+
(`operation === "set-query"`, captured plan flat) and adoption fires,
101+
because a keystroke is a filter-only change:
102+
`flat-cooperative-candidate.ts:95`
103+
`adoptEvaluationCache(next, captured)` makes both plans share **one**
104+
evaluation-cache WeakMap.
105+
106+
Then, per surviving row, `carryRecord` does:
107+
108+
1. `filterVerdict(plan, record)` — which internally does
109+
`#evaluationCache.get(input.row)`, finds the entry, fails the
110+
`cached.verdictPlan === compiled` guard (the entry was written by the
111+
_previous_ plan, so this plan must run its own filters — correct and
112+
deliberate), **discards the entry**, and evaluates the predicate.
113+
2. `fillSortKeysFromPrevious(nextPlan, prevPlan, record)` — two
114+
`instanceof CompiledQueryPlan` re-validations, then
115+
`next.#evaluationCache.get(input.row)` **on the same key, into the same
116+
map**, hits `existing !== undefined` and returns `existing.sortKeys`
117+
immediately. Under adoption this function carries nothing and evaluates
118+
nothing; it is a lookup wrapped in revalidation.
119+
120+
That is **two WeakMap gets on the same key plus two `instanceof` checks
121+
plus two call frames, per survivor, per commit** — at 50k survivors and 5
122+
warm commits, 500 000 redundant lookups.
123+
124+
**Proposed shape:** a single fused reader used only when adoption has
125+
fired — the candidate already knows this at line 95 and can hoist it to a
126+
boolean. One `#evaluationCache.get(row)` yields both the entry to check
127+
the verdict guard against _and_ `entry.sortKeys`, with no second lookup
128+
and no re-validation. Semantics are unchanged: the verdict is still
129+
recomputed under the new plan (the `verdictPlan` guard still fails), and
130+
the keys still come from the same entry `fillSortKeysFromPrevious` would
131+
have read.
132+
133+
**Pre-registered prediction (must be falsifiable before it is built):** if
134+
the sort-key carry term is still ~8% of a ~124 ms window (~10 ms) and this
135+
removes roughly one of the two lookups plus the revalidation, the
136+
expected win is **~3–5 ms on 50k filter-metadata and proportionally more
137+
on keystroke warm p50** (which is dominated by survivor count). If a
138+
paired A/B measures flat, the harness is lying or the term has already
139+
moved — per the bench-A/B rule, a flat response across the variable is a
140+
harness signal, not a result.
141+
142+
**Gate:** the re-trace must first show the carry-fill term is still a
143+
material share of the _current_ window. If M2 already absorbed it, this
144+
lever is dead and should be dropped without ceremony.
145+
146+
### Lever B (columnar verdict cache): REJECT — and now on a ceiling argument, not a regime excuse
147+
148+
The cycle asked whether the columnar verdict cache preserved at
149+
`73f1ae24` (`blove/filter-fast-path`) has a case now that warm keystroke
150+
commits run the cooperative identity lane, and required a warm-path budget
151+
analysis to prove the saving _before_ any rebuild. The budget analysis
152+
proves the opposite, and it does not need a fit regime to do it:
153+
154+
- The columnar verdict cache exists to make **filter predicate
155+
evaluation** cheaper. That is the only term it can touch.
156+
- The post-arc trace puts compiled-query `filterVerdict` at **~1% of the
157+
settle window**. The pre-arc `evaluate` term it replaced was ~13%; the
158+
compiled predicate already collapsed it.
159+
- A cache cannot save more than the term it caches. The upper bound on
160+
this lever is therefore **~1% ≈ 1 ms of a 124 ms window**, before
161+
paying for a mutable columnar store with commit-side clears — a real
162+
ongoing maintenance cost on every commit, which is precisely the cost
163+
the two previous reverts were unwilling to carry.
164+
- This is consistent with, and now explains, the two prior measurements:
165+
the Amendment J A/B measured **flat** (50k filter-metadata 108.6 vs
166+
107.3 pooled; filter-text 104.3 vs 108.4 — both inside the ~8.3 ms
167+
frame-quantization bin, with TanStack controls at **49.7–58.0, in
168+
band**, so that round was _fit_ and the flat reading is trustworthy).
169+
It was reverted as "cold-store-invisible" twice; the sharper statement
170+
is that its target term is now ~1% and there is no version of the warm
171+
path where it pays for itself.
172+
173+
**Decision: closed, not deferred.** Recommend deleting the preserved
174+
branch's claim on the roadmap rather than carrying it forward a third
175+
time. Reopening requires a trace showing filter-predicate evaluation back
176+
above ~10% of a settle window — which would itself mean something else
177+
regressed.
178+
179+
### Lever C (runner overhead ~15%): re-measure before touching
180+
181+
M2 and #518 both targeted this directly and neither has been traced
182+
since. It is the term most likely to have already moved. No proposal
183+
until the re-trace.
184+
185+
## What the next cycle should do, in order
186+
187+
1. **Confirmation round** (objective 1, unchanged): controls-in-band
188+
check, then the honest met/missed verdict against ≤120 / ≤130, as an
189+
addendum to the arc results doc. Cheap, and it closes an open bar.
190+
2. **Re-trace** one `PLAYWRIGHT_PERF_TRACE=1` 50k filter-metadata run,
191+
`analyze-cdp.mjs --window=settle`, **shares only**. Publish the
192+
post-M2 share table beside the post-M1 one so the drift is visible.
193+
3. **Then** decide on Lever A against the _current_ shares, with the
194+
pre-registered prediction above as the falsifier.
195+
4. Lever B needs no further work — it is closed.
196+
197+
Nothing in steps 2–4 should be started before step 1, because a share
198+
table measured on a loaded machine and a bar measured on a quiet one are
199+
the same 15-minute setup cost.
200+
201+
## Honesty ledger for this run
202+
203+
- No bench executed. No trace captured. No absolute or relative
204+
performance number in this document was measured tonight; every figure
205+
is cited from a prior run with its own regime disclosed.
206+
- The static findings (call-site sweep, adoption/lookup analysis, the
207+
`filterVerdict` ceiling argument) are load-independent and stand on the
208+
source at `bac186da`.
209+
- The one substantive claim that would change the roadmap — that the
210+
inherited "~10% snapshot HAMT reads" lever is mis-attributed — is
211+
offered as a _prediction the re-trace can refute_, not as a finding.

0 commit comments

Comments
 (0)