Files
lanework/Kanban/UI/Board/BoardRenderMetrics.swift
T
rzen 0218ae4c21 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
2026-08-01 20:04:01 -04:00

97 lines
5.1 KiB
Swift

import SwiftUI
#if DEBUG
/// **Render-cost counters for the board strip — DEBUG only, and the only way a test can tell "one
/// card repainted" from "the whole board rebuilt".**
///
/// Ported from the pathfinder (`Kanban/Views/MasonryLayout.swift`'s `BoardRenderMetrics`), which is
/// where the shape comes from: a flat bag of counters, a `reset()`, and one `count…` call at the top
/// of each body being watched. What is new here is the masonry's **column** vocabulary and the
/// trash column's own counter — Lanework's strip has three body levels, not two.
///
/// ### What it is for
///
/// The 2026-07-31 drag-performance work shipped four gates — `LaneView.==` / `CardFaceView.==`
/// applied through `.equatable()` (84f909a), the resting-layout cache (a51ad75), the marquee's
/// de-observation (f6105d4) and the resize hold (409f430) — and every one of them is a claim about
/// *how many bodies run*. A claim like that is unfalsifiable from the outside: the board looks the
/// same either way, only the frame rate differs, and a frame rate is a machine fact rather than a
/// test. These counters are what turn each gate into an assertion (`BoardRenderPerformanceTests`).
///
/// The reading half is RENDER-INSTRUMENTATION.md at the repo root, beside DRAG-REORDER.md: the
/// Instruments 26 SwiftUI template, the Hitches thresholds, and how the drag signposts
/// (`DragSignposts`) line up against these numbers.
///
/// ### Why the storage is what it is
///
/// `nonisolated(unsafe) static var`, exactly as the pathfinder has it. In practice every write
/// happens on the main actor — SwiftUI runs body evaluation *and* `Layout` callbacks there — but the
/// `Layout` conformance itself is `nonisolated`, so a main-actor-isolated counter would not be
/// reachable from `MasonryLayout.sizeThatFits`. The unsafety is real and deliberately accepted: this
/// type does not exist in a release build, and the alternative is a lock on the layout hot path of a
/// board the whole point is to keep fast.
///
/// **Nothing in the app reads these.** They are written by the bodies below and read by tests. A
/// counter that branched would be instrumentation that changed the thing it measures.
enum BoardRenderMetrics {
/// Every `subview.sizeThatFits(…)` `MasonryLayout` actually performed — the number its
/// measurement cache exists to drive down.
nonisolated(unsafe) static var masonryMeasurements = 0
/// Measurement requests served from that cache instead.
nonisolated(unsafe) static var masonryCacheHits = 0
/// `MasonryLayout.sizeThatFits` calls — SwiftUI probes a layout more than once per pass, which
/// is the multiplier the cache absorbs.
nonisolated(unsafe) static var masonrySizeThatFitsCalls = 0
/// `MasonryLayout.placeSubviews` calls.
nonisolated(unsafe) static var masonryPlaceCalls = 0
/// `CardFaceView.body` evaluations — both homes, board and trash, since it is one view.
nonisolated(unsafe) static var cardBodyEvaluations = 0
/// `BoardView.body` evaluations — the strip itself.
///
/// Not one of the pathfinder's counters, and it earns its place by being the **discriminator**:
/// "a lane re-ran" means one of two completely different things depending on whether the strip
/// re-ran with it. Strip **and** lanes is a parent pass whose `.equatable()` gate did not
/// suppress; lanes with the strip *still* is a direct Observation invalidation, which no gate has
/// any say over (`LaneView.==`'s own note). Without this number the two are indistinguishable
/// from a test, and the first diagnosis this instrumentation produced turned on exactly that
/// distinction (RENDER-INSTRUMENTATION.md ▸ What the first run found).
nonisolated(unsafe) static var stripBodyEvaluations = 0
/// `LaneView.body` evaluations (the pathfinder's `columnBodyEvaluations`, renamed to Lanework's
/// vocabulary: a lane is the kanban column, and a *column* is an interior masonry track).
nonisolated(unsafe) static var laneBodyEvaluations = 0
/// `TrashLaneView.body` evaluations — counted separately from the lanes because the column is
/// not one: it renders only while shown, and it re-runs for reasons the lanes do not have.
nonisolated(unsafe) static var trashLaneBodyEvaluations = 0
/// Every container body the strip draws — what "≤ N container bodies" is asserted against, so a
/// regression cannot hide by moving from one counter to the other.
static var containerBodyEvaluations: Int {
laneBodyEvaluations + trashLaneBodyEvaluations
}
static func reset() {
masonryMeasurements = 0
masonryCacheHits = 0
masonrySizeThatFitsCalls = 0
masonryPlaceCalls = 0
cardBodyEvaluations = 0
stripBodyEvaluations = 0
laneBodyEvaluations = 0
trashLaneBodyEvaluations = 0
}
static func countStripBody() { stripBodyEvaluations += 1 }
static func countCardBody() { cardBodyEvaluations += 1 }
static func countLaneBody() { laneBodyEvaluations += 1 }
static func countTrashLaneBody() { trashLaneBodyEvaluations += 1 }
}
#endif