Skip to content
design
Components

Trace waterfall

A trace's timed spans as bars on a shared time axis, nested under their parents.

On this page
Voice turn · "how is the anchor holding?" · running
0µs0.25µs0.5µs0.75µs1µsaudio.vad1µs
One voice turn replayed live, fourteen spans three levels deep across every component color, with running spans in amber, a failed tool call in red and attributes on hover.
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

0µs13ms25ms38ms50mspipeline.mainLoop50msnlu.classify12msllm.generate30mstts.synthesize4.0ms
A voice pipeline run as four spans, three of them inside the first, each bar placed by its start and length.
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

Voice Pipeline
0µs13ms25ms38ms50mspipeline.mainLoop50msnlu.classify12msllm.generate30mstts.synthesize4.0ms
The same four spans under a title, which names the trace above the bars.
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

PropTypeDefaultDescription
spansrequiredreadonly TraceSpan[]–Spans to render
titlestring | undefined–Total trace duration label (e.g. "234ms")
rowHeightnumber | undefined28Height 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.