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
97 lines
5.1 KiB
Swift
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
|