tick_attribution.hpp¶
TickAttribution — WHO consumed the loop budget.
This header declares 3 types (18 members) and 1 free function.
Extracted from include/shulib/diag/tick_attribution.hpp — this page is that header's documentation, reformatted, so it cannot disagree with the code. Prose about how to think about the API lives in the user guide; worked recipes live in the cookbook; this page is the complete, mechanical list of what exists.
Contents¶
class TickAttributionPhasesTickAttributionbeginTickphasephaseInPlaceendTickabandonTickhasCompletedTicklastPhaseslastTotallastAttributedlastOtherlastWorstPhaseresetclass TickAttribution::PhaseScopetickPhaseName— free function
class TickAttribution¶
Measures where one tick's time went, phase by phase, on an INJECTED clock — the "who" that LoopMonitor's "this tick blew its budget" cannot answer on its own. Only the LAST COMPLETED tick's breakdown is kept, so a record stamped mid-tick necessarily carries the previous tick's numbers; for the overrun path that lag is exactly right, because an overrun is detected on the tick AFTER the one that caused it. Needs a clock that advances DURING a tick, which is why it takes its own: the host sim clock only moves between ticks and would report every phase as zero. Single-task by contract, like the rest of diag/.
class, declared at include/shulib/diag/tick_attribution.hpp:54.
TickAttribution::Phases¶
Per-phase durations for one tick, indexed by TickPhase. Sized by kTickPhaseSlots rather than by the phases that exist today — the spare slots are what make a new phase an append to the vocabulary instead of a reshape of the telemetry wire.
alias, declared at include/shulib/diag/tick_attribution.hpp:59.
TickAttribution::TickAttribution¶
clock must outlive the instance (see header for WHICH clock).
function, declared at include/shulib/diag/tick_attribution.hpp:62.
TickAttribution::beginTick¶
Open a tick: zero the working phases, mark the start instant.
function, declared at include/shulib/diag/tick_attribution.hpp:65.
TickAttribution::phase¶
Open a scope that charges its own lifetime to p. Requires a tick to be open. The result MUST be bound to a named variable — an unnamed temporary dies at the semicolon and charges nothing, which is the whole reason this is [[nodiscard]].
function, declared at include/shulib/diag/tick_attribution.hpp:123.
TickAttribution::phaseInPlace¶
The same scope, in an optional. Exists because PhaseScope is deliberately non-movable, so phase()'s by-value return cannot be stored in one — and a caller that needs the optional shape (attribution is switchable, and must cost nothing when off) previously had to construct a PhaseScope directly with std::in_place, walking around the tick-open check. MotionScheduler was that caller, and was the only user of the bypass; with this it goes through the same precondition as everyone else. Same requirement as phase(): bind the result to a named variable, or it charges nothing.
function, declared at include/shulib/diag/tick_attribution.hpp:135.
TickAttribution::endTick¶
Close the tick: snapshot the working phases + total as the LAST COMPLETED tick (what records and overrun lines read).
function, declared at include/shulib/diag/tick_attribution.hpp:142.
TickAttribution::abandonTick¶
Discard a half-measured tick (an exception unwound through the tick body): its numbers never completed, so they are dropped rather than reported, and the instrument re-arms. The last COMPLETED tick's story is untouched.
function, declared at include/shulib/diag/tick_attribution.hpp:153.
TickAttribution::hasCompletedTick¶
False until the first endTick(), and again after reset(). Worth asking first: before any tick completes every lastX() accessor reads zero, which is indistinguishable from a tick that genuinely cost nothing.
function, declared at include/shulib/diag/tick_attribution.hpp:158.
TickAttribution::lastPhases¶
The last completed tick's per-phase durations (zeros before any tick).
function, declared at include/shulib/diag/tick_attribution.hpp:160.
TickAttribution::lastTotal¶
Seconds from beginTick() to endTick() of the last completed tick, on the attribution clock. It spans the whole tick, including work no phase scope wrapped — that remainder is what lastOther() reports rather than smearing it into a named phase.
function, declared at include/shulib/diag/tick_attribution.hpp:164.
TickAttribution::lastAttributed¶
Sum of the attributed phases of the last completed tick.
function, declared at include/shulib/diag/tick_attribution.hpp:167.
TickAttribution::lastOther¶
total − attributed: un-instrumented work. Floored at 0 (a clock that jumped mid-phase can make phases overshoot the total; the floor keeps the report coherent rather than printing a negative time).
function, declared at include/shulib/diag/tick_attribution.hpp:178.
TickAttribution::lastWorstPhase¶
The phase that consumed the most of the last completed tick — the NAME the overrun line prints. Ties resolve to the lower index (deterministic).
function, declared at include/shulib/diag/tick_attribution.hpp:185.
TickAttribution::reset¶
Forget everything (run boundary). The next tick starts a fresh story.
function, declared at include/shulib/diag/tick_attribution.hpp:196.
class TickAttribution::PhaseScope¶
Time one phase, RAII-style: the duration is credited when the scope closes. { auto scope = att.phase(TickPhase::Localization); localizer.update(); } Phases may repeat within a tick (durations accumulate); scopes must not overlap the same phase (the second-open would double-charge the overlap).
class, declared at include/shulib/diag/tick_attribution.hpp:76.
TickAttribution::PhaseScope::PhaseScope¶
Stamps the start instant. Reachable only through TickAttribution::phase() or ::phaseInPlace(), both of which check that a tick is actually open — the Key parameter is what makes that structural. It was a plain public constructor, which made the tick-open precondition advisory: a direct PhaseScope s{att, p} compiled with no tick open and its destructor still wrote into current_, crediting the interval to whatever tick happened to be open when it closed.
function, declared at include/shulib/diag/tick_attribution.hpp:96.
TickAttribution::PhaseScope::~PhaseScope¶
Credits (now − start) to the phase on scope exit, and only then: a scope still alive when endTick() runs contributes nothing to the tick it was opened in — its interval lands on whatever tick is open when it finally closes, or is discarded outright if the next beginTick() zeroes the working phases first. Repeated scopes on the same phase within one tick ACCUMULATE rather than replace.
function, declared at include/shulib/diag/tick_attribution.hpp:104.
TickAttribution::PhaseScope::PhaseScope (overload 2)¶
Non-copyable, and therefore non-movable: a scope charges exactly one interval, and a copy would charge it twice. phase() still returns one by value — that is guaranteed elision, not a move.
function, declared at include/shulib/diag/tick_attribution.hpp:111.
TickAttribution::PhaseScope::operator=¶
Covered by the comment on PhaseScope (overload 2) — one comment documents this run of special members.
function, declared at include/shulib/diag/tick_attribution.hpp:112.
class TickAttribution::PhaseScope::Key¶
Passkey. The TYPE is public so TickAttribution can name it; its CONSTRUCTOR is private with TickAttribution as the only friend, so nobody else can produce one. PhaseScope's own constructor therefore stays public — which std::optional's in-place construction requires, because optional does the constructing and cannot be made a friend — while remaining unreachable without a Key. A simple private constructor plus friend looks tidier and does not work here for exactly that reason.
class, declared at include/shulib/diag/tick_attribution.hpp:85.
No public members.
tickPhaseName¶
Short display token per phase for the overrun-attribution line ("loc"/"mot"/…).
free function, declared at include/shulib/diag/tick_attribution.hpp:215.
Design commentary, from the header¶
The header opens with the reasoning behind these shapes. It is reproduced here in full because a reference that only lists signatures teaches nobody why.
The header’s own reasoning — 32 lines
TickAttribution — WHO consumed the loop budget (diagnostics-plan D-3; WS13, C5).
LoopMonitor (A1) detects that a tick blew its budget; it cannot say WHO —
"localization 4 ms, motion 2 ms, sinks 1 ms" is the question an overrun actually
raises. This class measures named phases inside one tick on an injected clock
and keeps the LAST COMPLETED tick's breakdown, so:
* the scheduler stamps it into every DebugRecord (the D-3 schema slots), and
* an overrun line can NAME the worst consumer instead of shrugging.
── Which clock, and why it is injected separately ──────────────────────────────────
Phase timing needs a clock that ADVANCES DURING A TICK. On the robot that is
real time (R1 wires the microsecond clock — the same IClock the loop uses). In
HOST SIM the sim clock advances only between ticks (the pacer steps the world),
so phases would all read 0 — true, harmless, and useless. The attribution clock
is therefore its own injection: tests drive a deterministic one, sim runs may
leave attribution off entirely (a null clock in the scheduler config = feature
off = zero clock calls — the A1 cost contract, structurally).
── One-tick lag, stated honestly ───────────────────────────────────────────────────
Records are emitted DURING the Motion phase (by the motion itself), before that
phase's duration — or the tick's total — is knowable. So what rides a record is
the breakdown of the most recently COMPLETED tick, uniformly (documented on the
schema field). For the overrun path this lag is exactly right: the overrun is
DETECTED at tick N+1 (its dt covers tick N's work), and the last completed
breakdown at that moment IS tick N — the tick that overran.
Sum contract (pinned by test): attributed phase times never exceed the tick
total on the same clock; total − attributed = "other" (un-instrumented work +
pacing), reported as its own quantity rather than smeared into a named phase.
Single-task by contract, like the rest of diag/.
Last updated 2026-08-15 — published from the main release branch, commit fa545ff. This site tracks releases, so between them it can lag the repository — that lag is deliberate, and it is what keeps the development log off the public site. For the current state of the code, see the repository.