Selected-ness rides down as a compared parameter — a marquee crossing repaints its faces, not the board
A selection change re-ran every CardFaceView on the board (180 bodies ≈ 85 ms on the 6×30 fixture, 515 ≈ 233 ms on a real 515-card board, debug): the face's body read store.selection in three places — isSelected, the drag replica's count, and the context menu's styleTarget — and Observation invalidates every reader of the property, past the equatable gate entirely. The band overlay stayed cheap, which is why the marquee tracked the cursor while the highlight lagged ~0.4 s behind. Now LaneView and TrashLaneView hoist one selection read per body and hand each face isSelected/selectedCount as compared parameters; StyleMenuItems takes its target as a deferred closure; TrashLaneRowView gains the same treatment plus the Equatable gate it never needed before. Select-one-card: 180 bodies → 1. A growing band costs the selection's own running size; the real board's crossing fell 233 → 112 ms — the remainder is lane bodies re-measuring their masonry, a separate lane-level finding recorded in RENDER-INSTRUMENTATION.md. Also: select() gains defaultsSoleMember — the marquee's explicit nils never avoided the sole-member default, so a one-card band acquired a selectionHead and could scroll the lane out from under its own drag. MarqueeRenderCostTests pins the shape: redundant samples cost zero bodies, a growing band pays per crossing, and selectionStillRepaints holds a ≤8 budget.
This commit is contained in:
@@ -389,12 +389,20 @@ struct BoardRenderPerformanceTests {
|
||||
// run. The other half of the gate: too strict a `==` would show up here as a zero.
|
||||
#expect(selected.cards > 0, "selecting a card repainted nothing")
|
||||
|
||||
// What it actually costs is **every face on the board**, and that is the design rather than a
|
||||
// defect: `CardFaceView.body` reads `store.selection` (`isSelected`), so a selection change
|
||||
// invalidates all of them directly — the Observation half the gates explicitly do not cover.
|
||||
// Recorded here as a number rather than asserted as a budget: narrowing it would mean each
|
||||
// face taking its own selected-ness as a compared parameter, which is a design change and not
|
||||
// this card's. See RENDER-INSTRUMENTATION.md ▸ What the first run found.
|
||||
// **And it costs a handful of faces, not the board.** This used to be "every face on the
|
||||
// board" — `CardFaceView.body` read `store.selection` for its own `isSelected`, so under
|
||||
// Observation's property-level tracking one click invalidated all \(laneCount * cardsPerLane)
|
||||
// of them *directly*, past the gate entirely (RENDER-INSTRUMENTATION.md ▸ Selection is
|
||||
// O(board) in card bodies). Selected-ness is a compared parameter now — the lane hoists the
|
||||
// selection once and hands each face its answer — so what re-runs is the lane bodies that
|
||||
// were subscribed anyway plus the faces whose flag actually flipped.
|
||||
//
|
||||
// The budget is the one-card-edit budget above and it is loose for the same reason: SwiftUI
|
||||
// evaluates a body more than once per update, so a single flipped face is worth several
|
||||
// counts. What it rules out is the old shape, which was two orders of magnitude over this.
|
||||
#expect(selected.cards <= 8,
|
||||
"selecting one card re-rendered \(selected.cards) of \(laneCount * cardsPerLane) card faces")
|
||||
|
||||
#expect(selected.strips >= 1, "the strip did not re-run for a selection change")
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user