AI

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.

Preview in your theme

Loading preview…

"use client"

import * as React from "react"
import { AlertCircle, Gauge, Pin, RefreshCcw, TriangleAlert } from "lucide-react"

import { cn } from "@/lib/utils"
import {
  LATENCY_PHASE_ORDER,
  type LatencyBreakdownRequest,
  type LatencyBreakdownStats,
  type LatencyBreakdownStatus,
  type LatencyPhaseKey,
} from "./latency-breakdown.contract"

Installation

npx shadcn@latest add https://ui.zyeon.ai/r/latency-breakdown.json

Prompt

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 share gives 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 / segments so 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.

On This Page