Latency Breakdown
Per-request phase bars — queue, TTFT, generation and tool time on one shared scale, with a floor that keeps sub-pixel phases visible, a legend that ranks the runs by a phase, and a p50/p95 footer that names the population it came from.
Loading preview…
Installation
npx shadcn@latest add https://ui.zyeon.ai/r/latency-breakdown.jsonPrompt
Build a React + TypeScript + Tailwind "LatencyBreakdown" component with zod and
lucide-react. It draws one horizontal stacked bar per request — queue, TTFT,
generation, tool time — on a scale shared by every row, and closes with a
p50 / p95 footer.
Contract
- A zod schema in a sibling contract file is the single source of truth. One
request: { id, label, meta?, phases: { queueMs, ttftMs, genMs, toolMs } }, all
four phases `z.number().min(0)`.
- THERE IS NO totalMs FIELD. The four phases are disjoint and exhaustive, so
their sum IS the total. A separately reported total is a second source of
truth and the day it disagrees with the bar the panel stops being believed.
- Export the phase order as one `as const satisfies readonly (keyof Phases)[]`
array. The bar, the legend, the detail panel and the keyboard cursor all read
it, so adding a phase to the schema and to that array is the only edit needed
to make it appear everywhere.
- An optional `stats: { sampleSize, p50Ms, p95Ms }` carries percentiles computed
where the whole population lives. The rows on screen are a sample; a p95 over
eight rows is "the second slowest of eight", which is a different claim.
- Envelope status: "loading" | "empty" | "error" | "ready", independent of any
request's timings.
- Props: requests; status; stats?; scale ("absolute" | "share", default
"absolute"); totalMs? (pin the shared extent); budgetMs?; phase? /
defaultPhase? / onPhaseChange? (the isolated phase, controlled or not, where
null means none); order ("given" | "slowest", default "given"); maxRows (8);
skeletonRows (5); showLegend; showFooter; onSelectRequest?(id, phase);
onRetry?; emptyState?; errorMessage?; formatDuration?; label
("Latency breakdown"); className plus the rest of the element props, ref
forwarded to the <section>.
- Every numeric prop is defended: non-finite or negative milliseconds render as
0, maxRows below 1 clamps to 1, a non-finite skeletonRows falls back.
Behavior
- ONE SHARED SCALE. In "absolute" the extent is `totalMs ?? the slowest run`,
and it is measured over EVERY request, never over the visible slice —
expanding the "Show all" disclosure must not silently rescale bars the reader
has already read. In "share" each bar is divided by its own total and fills
the row: composition only, magnitude discarded.
- SUB-PIXEL PHASES NEVER VANISH. A 4 ms queue inside a 9 s run is 0.04% of the
track — a third of a pixel — and a phase that isn't drawn reads as a phase
that didn't happen. Give every non-zero phase a floor of ~3px (converted to a
percentage from the measured track width via one ResizeObserver, with a
static fallback for SSR) and take the difference proportionally from the
segments that have room to spare. Clamp the floor to `rowWidth / segments`
first, so the borrowing always has somewhere to borrow from and the bar's
total width — the thing that carries the scale — is never inflated.
- SIZES ARE NUDGED, NUMBERS ARE NOT. Each segment keeps two figures: the drawn
width (floored) and the true share, computed from the raw milliseconds over
the row's own total. Every label, tooltip and accessible name reads the true
share; a share that rounds below 0.1% prints as "under 0.1%", never as 0.
- ISOLATING A PHASE RE-READS THE WHOLE PANEL. Clicking a legend entry (an
aria-pressed toggle) dims the other phases, ranks the rows by that phase
descending, switches each row's number from its total to that phase — with
the total kept underneath — and points the footer percentiles at it. Clicking
it again releases everything. The sort is a copy: the caller's array is never
mutated, and Array#sort is stable, so ties keep the given order.
- SEGMENT DETAIL ON HOVER, FOCUS *AND* CLICK. Hover and focus preview; a click
pins, which is the only way to read the detail on a touch screen. Precedence
is hover, then focus, then pin, so the pin survives a preview elsewhere and
comes back when the pointer leaves; the pinned segment keeps a ring of its
own and Escape releases it. Position the panel by arithmetic on percentages
already in hand (centre of the segment, flipped to the near edge inside a
fifth of either end, flipped above the bar on the last row of a multi-row
chart) — no measurement, no layout read, and it can never be pushed outside
the track.
- THE KEYBOARD COSTS ONE TAB, NOT ONE PER SEGMENT. Roving tabindex: exactly one
segment is tabbable, arrows move the cursor (left/right within a row, up/down
to the same phase in the neighbouring row, falling back to the nearest phase
present there because a row with no tool time has no tool segment), Home/End
jump to the ends of the row. Only swallow the key once it is known to be one
of ours AND to have somewhere to go, so an arrow at the edge still scrolls the
page.
- THE FOOTER NAMES ITS POPULATION. Percentiles are nearest-rank — the number
shown is a real request's duration you can point at, not an interpolation
between two of them. With `stats` and no phase isolated, report those and say
"over 12,480 runs"; otherwise compute from the rows and say "over these 8
runs" (or "over this one run"). `stats` describes total latency only, so an
isolated phase always falls back to computing from the rows.
- A BUDGET IS A THRESHOLD, NOT A SCALE. `budgetMs` draws a dashed marker down
the chart, marks the rows past it (destructive number plus an icon and an
sr-only "over budget") and adds a footer chip with the breach count. A budget
beyond the extent still counts breaches — there are none — but draws no
marker, because stretching the ruler to reach a 30 s budget would squash every
real bar to a stub.
- Four states are four first-class branches: a skeleton on the SAME grid
template (so nothing jumps when data lands) behind aria-busy and an sr-only
status; an empty slot; a role="alert" panel with an optional Retry; and the
chart. `status="ready"` with an empty array is a contract slip and says so in
its own words rather than drawing an empty frame.
- Stale keys are ignored at render, not cleared by an effect: a pin or a cursor
whose row left the visible slice simply stops being active, and comes back to
life if that row does. The single ResizeObserver is disconnected on unmount.
Rendering & styling
- Semantic tokens only. Phases use var(--chart-1..4) — hue as identity, not a
lightness ramp, because the four phases are kinds of time, not degrees of one
thing. Chrome is border / bg-muted for the empty track / text-muted-foreground
/ bg-popover for the detail panel / text-destructive and border-destructive
for the budget. No hard-coded colours.
- NOTHING IS PRINTED ON A SEGMENT. Chart tokens are tuned for the 3:1 contrast a
filled shape needs, not the 4.5:1 body text needs, so a label baked into a
fill would fail AA in one of the two themes whichever foreground it picked.
Numbers live in the row's own column, in the legend and in the detail panel,
all on the card surface.
- One grid template, shared by the ruler, the rows and the skeleton, inside an
@container: wide, it is label | track | number on one line with both flexible
columns as `fr`; narrow, the track drops to its own full-width line instead of
squeezing the label and the chart into nothing.
- No overflow-hidden on the bar: the corners are rounded by the first and last
segment instead, so a focus ring is never clipped by the bar that owns it.
- Each segment is a real <button aria-pressed> whose accessible name carries the
phase, its duration, its true share and the row's total; the detail panel is
therefore aria-hidden and pointer-events-none — a passive duplicate, not a
second announcement. The bar is a role="group" named after its row.
- Width and opacity transitions are motion-reduce-guarded; the skeleton's pulse
stops under prefers-reduced-motion and still reads as loading. cn() merges the
consumer's className into the root, which also spreads the remaining element
props and forwards its ref.
Customization levers
- Reading mode: `scale="absolute"` to compare magnitudes, `scale="share"` to
compare composition. `totalMs` pins the ruler so several panels (before/after,
model A/model B) can be read against each other.
- Ranking: `order="given"` keeps chronology, `order="slowest"` puts the worst
first; `phase` / `onPhaseChange` let a URL parameter or a parent toolbar own
the isolated phase instead of the legend.
- Density: `maxRows` decides how many bars are drawn before the disclosure,
`skeletonRows` matches the skeleton to the shape you usually load, and
`showLegend` / `showFooter` strip the panel down to bare bars for a dense
dashboard cell.
- Thresholds: `budgetMs` is the only place the destructive tone is used — set it
to your SLO, or leave it out for exploratory views where "slow" isn't defined
yet.
- Vocabulary: `formatDuration` swaps the units (seconds throughout, "×budget",
a locale-aware formatter), the phase label / hint / tone maps rename or
recolour phases, and `label` names the region for assistive tech.
- Wiring: `onSelectRequest(id, phase)` is where you open the full trace —
clicking a segment always pins it locally, so the callback is additive and
never the only feedback.Concepts
- One scale, or none at all — bars only compare if they share a denominator, so the extent is the slowest run over the whole array, not over the rows currently drawn; expanding the disclosure can't quietly rescale what you already read. Switching to
sharegives up magnitude on purpose: every bar fills the row and the only question left is where the time went. - A floor that borrows, never inflates — a 4 ms queue inside a 9 s run is a third of a pixel, and a phase you can't see reads as a phase that didn't happen. Each non-zero phase is granted ~3px taken proportionally from its own row's roomier segments, with the floor itself capped at
rowWidth / segmentsso the bar's overall width — the thing carrying the scale — never grows to fit its minimums. - Sizes are nudged, numbers are not — every percentage on screen is computed from the raw milliseconds over the row's own total, never from the drawn width, and a share that rounds below 0.1% says "under 0.1%" instead of "0%". The moment a chart's labels start describing its own rounding, it is measuring itself.
- Isolating a phase re-reads the panel — picking Generation in the legend is not a highlight: it ranks the runs by generation, swaps each row's number to generation (keeping the total underneath) and points the footer percentiles at it. That is the difference between "which run was slow" and "which run was slow at this".
- The footer names its own population — percentiles are nearest-rank, so the number is a real run you can point at; and it always says what it was computed over, because "p95 of eight sampled rows" and "p95 of twelve thousand runs" are different claims that look identical once printed.
- Pin is the touch story — hover and focus preview, a click pins, Escape releases. Precedence runs hover, then focus, then pin, so a pinned segment survives while you preview another one and comes back the moment the pointer leaves; without the pin, a phone would have no way to read the detail at all.
Agent Permissions
A capability-scoping panel for one agent — per-tool switches with risk bands, a spend cap metered against what is already spent, an add-and-remove domain allowlist, an ordered file-access scale with policy ceilings, and an unsaved-changes bar that counts every change that expands the agent's authority.
Prompt Arena
Pairwise prompt results as a scoreboard you can act on — an A/tie/B split bar, a verdict gated by a Wilson interval instead of a raw lead, sampled matchups with their own vote splits, the two outputs for one input on demand, and a rerun that fires exactly once.