Files
benilla-octo/crates
gromandClaude Opus 5 a8b6fc8ad6 perf(layout): the resolve's seed pass walked the graph, not the change
Each solve chooses which nodes to solve and which clean rects to seed it
with. It did that by asking **every id in the graph** — 18 203 frames and
42 284 regions on the player's interface — "is one of your dependents
dirty?", for a dirty closure whose steady-state median is 2 frames and 4
regions.

And nothing was measuring it. `[layout-pre]`'s last lap ends at the change
gate; `[layout-prof]`'s `frame_us`/`region_us` start at the round loop. This
pass sits between them, so neither line charged it — which is how a
`frame:GetLeft` that measures 17.5–18.4 us a call in a live session (six a
frame, through stock `MouseIsOver`'s four edge reads) could force a resolve
the profiler called 3 us. A `seedsel_us` column now closes that gap, and it
stays: a phase between two timers is a phase nobody owns.

Measured on the player's own 80-addon interface, last-quarter solves:

              median   p75   p90   mean    max
    before       458    498   607    488    882
    after          0      3     9     38   3788

`solved` and `swept` come out identical to the digit (median 2/4, mean
156.3/408.3, max 18183/41415): the same work is selected. Before, 202 of 549
solves cost over 400 us and the dearest of them had dirty sets of 0–13 nodes,
which is the pathology itself; after, 29 of 549 exceed it and every one is
solving 14 000–16 600 frames at 0.48 us a node, against a `frame_us` of
10 170 us on the same pass. The cost moved from the size of the graph to the
size of the change, which was the point; the higher max is that, not a
regression.

**What it is not: an fps fix.** The bench's per-frame totals do not move —
1.294 -> 1.293 ms/frame grouped, `vm-tick` 0.667 -> 0.676 (noise, single
runs). The one column that shifts is `resolve`, 0.011 -> 0.006 ms/frame. The
458 us is per *solve*, and a steady frame does a fraction of one; the live
bound is the whole of `frame:GetLeft`, 105-110 us/f, against a 7.6-9.5 ms
frame. So this removes real waste and buys about 1%, which is worth having
and is not what grom asked for. Chasing it at all was a misread of mine:
`GetLeft` is 54% of the *binding* bill, and the binding bill is ~200 us of a
~9000 us frame.

The same set, by the same relation read the other way round: a clean node is
seeded iff some dirty node reads it, and a node's dependencies are exactly
the three `scope.edge` arguments in the roster walk — its anchors' targets,
plus a region's owner frame, whose scale it reads. `LayoutScope::dirty_ids`
carries the dirty set as a list, written wherever `dirty` turns true so the
two cannot disagree, and `seed_dep` enumerates the dependencies. A
`full_scope` pass skips the walk: everything is dirty, so there are no seeds
to find.

Order is not a property here, which is what makes the rewrite safe. Both
rosters are built from `HashMap` iteration (`frame_to_id`, `region_data`), so
no pass ever had a canonical order, and the region sweep is a fixpoint whose
round cap covers any chain — order moves `rounds`, not rects. Only the seed
lists are sorted, to drop duplicates a shared dependency would otherwise
re-`set_external` every round.

`verify_selection` is the falsifier: under `WOW_LAYOUT_VERIFY` it recomputes
all four lists the old way and requires the same sets. That is a tighter
question than `resolve_layout`'s rect comparison — it isolates this pass, so
a selection bug cannot hide behind an unrelated red, and an unrelated red
cannot be pinned on the selection. Verified non-vacuous by dropping the
region-owner dependency and watching it fail (`0 vs 1 entries`).

Note for whoever runs it: `WOW_LAYOUT_VERIFY=1` over `tick_bench` is **red at
HEAD**, independently of this change — an incremental pass ends with 48 fewer
region rects than a full derivation of the same frame (`regions 29597 ->
29645`), byte-identical with and without this commit. That is a missing
`touch_layout()` somewhere, not a selection bug, and it had never been run
before because `tick_bench` is `--ignored` and the flag is in no gate.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
2026-10-19 22:05:01 -06:00
..