Drag instrumentation — render counters, hot-path signposts, and the invariants that prove the gates

BoardRenderMetrics (DEBUG-only, the pathfinder's counter bag plus a
strip-body discriminator that tells a failed gate from a direct
Observation invalidation) counted at BoardView/LaneView/CardFaceView/
TrashLaneView bodies and MasonryLayout's callbacks. DragSignposts wraps
dropUpdated, retargetCards, commitDrop, and the commit-to-covering-
snapshot release pause; input latency reports honestly against
NSApp.currentEvent's mach base or labels itself base=none — no event
timestamp rides the drop path. BoardRenderPerformanceTests hosts the
real BoardView off-screen: a value-equal reload runs zero lane and zero
card bodies, a one-card edit repaints one card of 180. The lane-body
budget is <= laneCount with the headerInk chain documented and a
two-way tripwire that fails when the fix lands. Methodology in
RENDER-INSTRUMENTATION.md. Release build proves it all compiles out.

Drag-perf card a450ad09.

Claude-Session: https://claude.ai/code/session_01CqjXB7ASoWtbyoGod68k97
This commit is contained in:
2026-08-01 20:04:01 -04:00
parent 409f430813
commit 0218ae4c21
11 changed files with 861 additions and 0 deletions
+16
View File
@@ -210,6 +210,10 @@ struct MasonryLayout: Layout {
// change, which is the only way a card's measured height can change at a fixed column width
// (its content changed its view tree was rebuilt the layout's content is new). Clearing
// there means the cache never outlives the content it measured.
//
// Both halves are counted in DEBUG builds (`BoardRenderMetrics.masonryMeasurements` /
// `masonryCacheHits`), which is what makes "the cache absorbs SwiftUI's repeat probes" a number
// a test can read rather than a claim see RENDER-INSTRUMENTATION.md.
struct Cache {
var columnWidth: CGFloat = .nan
var heights: [CGFloat] = []
@@ -229,8 +233,14 @@ struct MasonryLayout: Layout {
cache.heights = [CGFloat](repeating: .nan, count: subviews.count)
}
if !cache.heights[index].isNaN {
#if DEBUG
BoardRenderMetrics.masonryCacheHits += 1
#endif
return cache.heights[index]
}
#if DEBUG
BoardRenderMetrics.masonryMeasurements += 1
#endif
let height = subviews[index].sizeThatFits(ProposedViewSize(width: column, height: nil)).height
cache.heights[index] = height
return height
@@ -248,6 +258,9 @@ struct MasonryLayout: Layout {
}
func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout Cache) -> CGSize {
#if DEBUG
BoardRenderMetrics.masonrySizeThatFitsCalls += 1
#endif
let width = proposal.width ?? 0
let placement = placement(width: width, origin: .zero)
let heights = measuredHeights(of: subviews, at: placement.columnWidth, cache: &cache)
@@ -255,6 +268,9 @@ struct MasonryLayout: Layout {
}
func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout Cache) {
#if DEBUG
BoardRenderMetrics.masonryPlaceCalls += 1
#endif
let placement = placement(width: bounds.width, origin: bounds.origin)
let heights = measuredHeights(of: subviews, at: placement.columnWidth, cache: &cache)
for (index, frame) in placement.frames(heights: heights).enumerated() {