The marquee registry stops being observable — reflow writes cost a dictionary store and nothing else

Drops @Observable from MarqueeTargetRegistry (MarqueeSession keeps it —
its rect genuinely renders the band). The audit found no body read
anywhere: the begin guard and sample loop read from inside the drag
gesture, the arrows from inside a key handler, so nothing ever needed
invalidating when a frame moved — while every make-room reflow had each
sliding face re-firing onGeometryChange per display frame, each write
paying Observation registrar bookkeeping on top of the reflow's own
render work (the confirmed A/B culprit of 2026-07-31). Write-gating on
drag-active was rejected: a suppressed write never replays, leaving the
band and arrows navigating stale rectangles. A tripwire test pins the
registry against anyone re-adding the macro.

Drag-perf confirmed culprit, card eb7b75ce.

Claude-Session: https://claude.ai/code/session_01CqjXB7ASoWtbyoGod68k97
This commit is contained in:
2026-08-01 19:14:11 -04:00
parent a51ad750ad
commit f6105d4389
2 changed files with 84 additions and 1 deletions
+19 -1
View File
@@ -93,8 +93,26 @@ final class MarqueeSession {
/// It is also the **begin guard's** universe: a drag that starts inside a registered frame belongs
/// to that item's own gesture (a card drag, a drag out of the trash), never to the band. Deciding
/// that geometrically rather than by gesture priority is what keeps the two from fighting.
///
/// **Deliberately not `@Observable`** `LaneDropRegistry`'s rule, for the same reason and with the
/// same proof: nothing renders off it. Every reader asks at *event* time the band's begin guard and
/// its sample loop from inside a drag gesture (`MarqueeControl.gesture(in:)`), the arrows from inside
/// a key handler (`BoardView.step`, `.extend`, over `NavigationMath`) so no view body ever reads a
/// frame from here and none needs invalidating when one moves.
///
/// What observing it cost is the whole reason the rule is stated: every card face registers here
/// through `onGeometryChange`, and a drag's make-room reflow *animates positions*, so for the ~0.18 s
/// of every proposal change each sliding face re-fires its observer once per display frame. Observed,
/// each of those writes was an `@Observable` mutation with registrar bookkeeping dozens of cards at
/// the display's refresh rate, invalidating the strip on top of the reflow's own render work (an A/B
/// on 2026-07-31 confirmed it: removing the card-face registration alone made dragging visibly
/// smoother). Unobserved, a registration is a dictionary store and nothing else.
///
/// The alternative suppressing the writes while a drag is in flight was rejected: a suppressed
/// write never replays if that card's geometry does not change again after the drag ends, which
/// leaves the band and the arrows navigating by stale rectangles. The frames must stay live; only
/// their observation had to go.
@MainActor
@Observable
final class MarqueeTargetRegistry {
private(set) var targets: [ItemID: MarqueeTarget] = [:]