Trace waterfall
A trace's timed spans as bars on a shared time axis, nested under their parents.
On this page
Code
import { TraceWaterfall, type TraceSpan } from "@bosunkit/components/trace-waterfall";
import { useEffect, useState, type Dispatch, type ReactElement, type SetStateAction } from "react";
export const description: string =
"One voice turn at the helm replayed live, from the microphone to the spoken answer: spans run amber while they " +
"last, a failed tool call turns red, and hovering a row shows its attributes.";
/** The whole turn, as bosund recorded it. Times are microseconds from the start of the turn. */
const TURN: readonly TraceSpan[] = [
{
id: "turn",
name: "pipeline.turn",
component: "pipeline",
startUs: 0,
durationUs: 1_684_000,
attrs: { utterance: "Bosun, how is the anchor holding?", speaker: "Maya" },
},
{ id: "vad", parentId: "turn", name: "audio.vad", component: "audio", startUs: 0, durationUs: 42_000 },
{
id: "asr",
parentId: "turn",
name: "asr.transcribe",
component: "asr",
startUs: 42_000,
durationUs: 268_000,
attrs: { model: "parakeet-tdt", words: 6, confidence: 0.96 },
},
{ id: "decode", parentId: "asr", name: "asr.decode", component: "asr", startUs: 190_000, durationUs: 118_000 },
{
id: "nlu",
parentId: "turn",
name: "nlu.classify",
component: "nlu",
startUs: 312_000,
durationUs: 24_000,
attrs: { intent: "anchor.status", confidence: 0.97 },
},
{ id: "ner", parentId: "nlu", name: "nlu.entities", component: "nlu", startUs: 318_000, durationUs: 640 },
{
id: "route",
parentId: "turn",
name: "orchestrator.route",
component: "orchestrator",
startUs: 338_000,
durationUs: 9_000,
},
{
id: "skill",
parentId: "turn",
name: "skill.anchorWatch",
component: "skill",
startUs: 348_000,
durationUs: 182_000,
attrs: { swing_radius_m: 45, chain_out_m: 40 },
},
{
id: "position",
parentId: "skill",
name: "tool.vesselPosition",
component: "tool",
startUs: 352_000,
durationUs: 850,
attrs: { lat: 39.4799, lon: 2.4931, fix: "3D" },
},
{
id: "wind",
parentId: "skill",
name: "tool.windHistory",
component: "tool",
startUs: 354_000,
durationUs: 120_000,
status: "error",
attrs: { error: "no data from WS320 for 95 s", fallback: "forecast" },
},
{
id: "cep",
parentId: "skill",
name: "cep.query",
component: "cep",
startUs: 476_000,
durationUs: 48_000,
attrs: { pattern: "anchor.drag", matches: 0 },
},
{
id: "llm",
parentId: "turn",
name: "llm.generate",
component: "llm",
startUs: 532_000,
durationUs: 640_000,
attrs: { model: "qwen3.5-4b", tokens_out: 41, local: true },
},
{
id: "tts",
parentId: "turn",
name: "tts.synthesize",
component: "tts",
startUs: 1_180_000,
durationUs: 210_000,
attrs: { voice: "en-GB", chars: 168 },
},
{ id: "play", parentId: "turn", name: "voice.playback", component: "voice", startUs: 1_274_000, durationUs: 410_000 },
];
/** How far the replay moves per tick, how often it ticks, and how long the finished turn stays before it replays. */
const STEP_US: number = 60_000;
const TICK_MS: number = 150;
const HOLD_TICKS: number = 20;
const TURN_END_US: number = 1_684_000;
/** The turn as it stood `nowUs` into it: spans not yet started are left out, and those still going are running. */
function turnAt(nowUs: number): TraceSpan[] {
return TURN.filter((span: TraceSpan): boolean => span.startUs <= nowUs).map((span: TraceSpan): TraceSpan => {
const end: number = span.startUs + span.durationUs;
if (end <= nowUs) {
return { ...span, status: span.status ?? "ok" };
}
return { ...span, durationUs: Math.max(nowUs - span.startUs, 1), status: "running" };
});
}
function formatSeconds(us: number): string {
return `${(us / 1_000_000).toFixed(2)} s`;
}
export default function Showcase(): ReactElement {
const [tick, setTick]: [number, Dispatch<SetStateAction<number>>] = useState<number>(0);
useEffect((): (() => void) => {
const timer: ReturnType<typeof setInterval> = setInterval((): void => {
setTick((current: number): number => (current + 1) % (Math.ceil(TURN_END_US / STEP_US) + HOLD_TICKS));
}, TICK_MS);
return (): void => {
clearInterval(timer);
};
}, []);
const nowUs: number = Math.min(tick * STEP_US, TURN_END_US);
const finished: boolean = nowUs >= TURN_END_US;
return (
<div style={{ width: "100%" }}>
<TraceWaterfall
spans={turnAt(Math.max(nowUs, 1))}
title={`Voice turn · "how is the anchor holding?" · ${finished ? formatSeconds(TURN_END_US) : "running"}`}
rowHeight={24}
/>
</div>
);
}
A trace waterfall shows where the time went in one run. Each span is a row: its name on the left, and a bar on a
shared time axis whose position is when the span started and whose length is how long it took. A span with a
parentId sits indented under its parent. The axis runs from zero to the end of the last span, and a bar is red for an
error, amber while running and otherwise the color of its component. A span's status is ok, error or
running, and hovering a row shows the span's
attributes. With no spans the component renders nothing.
Examples
Untitled
Code
import { TraceWaterfall, type TraceSpan } from "@bosunkit/components/trace-waterfall";
import type { ReactElement } from "react";
export const description: string =
"A voice pipeline run as four spans, three of them inside the first, each bar placed by its start and length.";
const SPANS: readonly TraceSpan[] = [
{ id: "1", name: "pipeline.mainLoop", component: "pipeline", startUs: 0, durationUs: 50000, status: "ok" },
{ id: "2", parentId: "1", name: "nlu.classify", component: "nlu", startUs: 1000, durationUs: 12000, status: "ok" },
{ id: "3", parentId: "1", name: "llm.generate", component: "llm", startUs: 14000, durationUs: 30000, status: "ok" },
{ id: "4", parentId: "1", name: "tts.synthesize", component: "tts", startUs: 45000, durationUs: 4000, status: "ok" },
];
export default function Untitled(): ReactElement {
return <TraceWaterfall spans={SPANS} />;
}
Titled
Code
import { TraceWaterfall, type TraceSpan } from "@bosunkit/components/trace-waterfall";
import type { ReactElement } from "react";
export const description: string = "The same four spans under a title, which names the trace above the bars.";
const SPANS: readonly TraceSpan[] = [
{ id: "1", name: "pipeline.mainLoop", component: "pipeline", startUs: 0, durationUs: 50000, status: "ok" },
{ id: "2", parentId: "1", name: "nlu.classify", component: "nlu", startUs: 1000, durationUs: 12000, status: "ok" },
{ id: "3", parentId: "1", name: "llm.generate", component: "llm", startUs: 14000, durationUs: 30000, status: "ok" },
{ id: "4", parentId: "1", name: "tts.synthesize", component: "tts", startUs: 45000, durationUs: 4000, status: "ok" },
];
export default function Titled(): ReactElement {
return <TraceWaterfall spans={SPANS} title="Voice Pipeline" />;
}
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| spansrequired | readonly TraceSpan[] | – | Spans to render |
| title | string | undefined | – | Total trace duration label (e.g. "234ms") |
| rowHeight | number | undefined | 28 | Height per row in pixels; 28 by default. |
Interpret the timeline
Parent spans include their children: summing every row double-counts overlapping work. Compare the end-to-end elapsed time with the critical sequence and explain parallel work separately. Keep all timestamps on one clock and state whether a running span's duration is provisional. Missing spans are a coverage gap, not zero time.
Hover attributes are supplementary. Put the finding, important errors and timing basis in visible text; provide a table or source trace when exact attributes matter to keyboard users, touch readers or a static report. With no spans, the caller supplies an explicit empty, loading or failed state because the waterfall renders nothing. Label replayed and illustrative traces so animation cannot be mistaken for a live vessel connection.
Related
- Data visualization: how measured data is drawn.