Percentile summary The mean is the number nobody experiences. Latency is reported at p50, p95 and p99 because the tail is what users feel, and an average hides exactly the part that pages someone. Tokens this needs: --foreground, --muted-foreground, --border, --status-warn, --chart-1 The status, chart, scale, state and direction names are an extension, not a rename. shadcn has --destructive and five --chart-* and nothing else in this territory. ──────────────────────────────────────────────────────────────────────── // LatencyPercentiles.tsx ──────────────────────────────────────────────────────────────────────── /** * Percentiles from a histogram are estimates, and the panel should say so. * * This computes quantiles the way Prometheus's histogram_quantile does: * find the bucket the rank falls in, then interpolate linearly across it. The * error is bounded by that bucket's width and by nothing about the data, so * the component prints the bucket beside any estimate whose bucket is wide * relative to the value. A p95 of 443ms from a 300–450ms bucket has three * significant figures and one of them is real. */ export type Bucket = { le: number; count: number }; const ms = (v: number) => (v >= 1000 ? `${v / 1000}s` : `${Math.round(v)}ms`); /** Which bucket holds the rank, and where in it the quantile interpolates to. */ export function quantile(buckets: Bucket[], q: number) { const total = buckets.reduce((n, b) => n + b.count, 0); const rank = q * total; let below = 0, lo = 0; for (const b of buckets) { if (below + b.count >= rank) { const frac = b.count ? (rank - below) / b.count : 0; return { value: lo + frac * (b.le - lo), lo, hi: b.le }; } below += b.count; lo = b.le; } const last = buckets[buckets.length - 1]; return { value: last.le, lo, hi: last.le }; } /** An estimate whose bucket spans more than a quarter of its value is thin. */ const WIDE = 0.25; export function LatencyPercentiles({ buckets, quantiles = [0.5, 0.95, 0.99], ticks, width = 400, height = 160 }: { buckets: Bucket[]; quantiles?: number[]; /** Axis labels, in ms. Defaults to every bucket boundary. */ ticks?: number[]; width?: number; height?: number; }) { const max = buckets[buckets.length - 1].le; const peak = Math.max(...buckets.map((b) => b.count)); const x = (v: number) => (v / max) * width; const plotH = height - 24; const estimates = quantiles.map((q) => { const e = quantile(buckets, q); return { q, ...e, wide: (e.hi - e.lo) / e.value > WIDE }; }); const flagged = estimates.find((e) => e.wide); return (
p{Math.round(flagged.q * 100)} is interpolated across the {ms(flagged.lo)}–{ms(flagged.hi)} bucket, which holds most of the observations. Anywhere in that bucket is consistent with the data.
)}