Skip to content
OpenPerf
Esc
↑↓navigate↵open⌘Jpreview
On this page

Waterfall

Spans of a trace on its time axis, as a tree.

import { Waterfall } from "@openperf/deft";

import { Preview } from "../../src/Preview";
import { TRACE } from "./trace";

export default function WaterfallBasic() {
  return (
    <Preview block={true}>
      <Waterfall.Root aria-label="Spans" duration={TRACE.duration}>
        <Waterfall.Ruler />
        {TRACE.spans.map((span) => (
          <Waterfall.Row
            depth={span.depth}
            duration={span.duration}
            key={span.name}
            label={`${span.duration} ms`}
            name={span.name}
            start={span.start}
            tone={span.tone}
          />
        ))}
      </Waterfall.Root>
    </Preview>
  );
}
  • Use it for one trace when its hierarchy, timing and selected span matter together.
  • Use one time scale: row start and marker at are milliseconds from the trace start, and every duration uses milliseconds on that same scale.
  • Keep rows in parent/child order and set depth to match the hierarchy.
  • The root span is tone="root", ordinary spans are child, and the failing span is failing, which also colours its name.
  • Give Waterfall.Root an aria-label. It is a tree, and the rows are one tab stop: ↑ ↓ move, → expands, ← collapses, Enter or Space selects.
  • A row scrolls into view when it becomes selected, so selection driven from outside the tree, such as a page shortcut, stays visible.
  • The duration label sits after the bar, or before it when the bar ends near the right edge, and is left out when neither side has room.

Summary

import { Waterfall } from "@openperf/deft";

import { Preview } from "../../src/Preview";
import { TRACE } from "./trace";

export default function WaterfallSummary() {
  return (
    <Preview block={true}>
      <Waterfall.Summary
        segments={TRACE.spans
          .filter((span) => span.depth === 1)
          .map((span) => ({ id: span.name, duration: span.duration }))}
      />
      <Waterfall.Root aria-label="Spans" duration={TRACE.duration}>
        {TRACE.spans.map((span) => (
          <Waterfall.Row
            depth={span.depth}
            duration={span.duration}
            key={span.name}
            name={span.name}
            start={span.start}
            tone={span.tone}
          />
        ))}
      </Waterfall.Root>
    </Preview>
  );
}

Put a Waterfall.Summary above the waterfall to show where the request spent time. Each segment is as wide as its share of the total.

Expandable rows

import { Waterfall } from "@openperf/deft";
import { useState } from "react";

import { Preview } from "../../src/Preview";
import { TRACE } from "./trace";

const PARENTS = new Set(["Span 1", "Span 3"]);

export default function WaterfallExpandableRows() {
  const [collapsed, setCollapsed] = useState<string[]>([]);

  return (
    <Preview block={true}>
      <Waterfall.Root aria-label="Spans" duration={TRACE.duration}>
        {TRACE.spans
          .filter((span, index) =>
            TRACE.spans
              .slice(0, index)
              .every(
                (ancestor, ancestorIndex) =>
                  !collapsed.includes(ancestor.name) ||
                  TRACE.spans
                    .slice(ancestorIndex + 1, index + 1)
                    .some((between) => between.depth <= ancestor.depth),
              ),
          )
          .map((span) => (
            <Waterfall.Row
              depth={span.depth}
              duration={span.duration}
              expanded={PARENTS.has(span.name) ? !collapsed.includes(span.name) : undefined}
              key={span.name}
              name={span.name}
              onToggle={() => {
                setCollapsed(
                  collapsed.includes(span.name)
                    ? collapsed.filter((name) => name !== span.name)
                    : [...collapsed, span.name],
                );
              }}
              start={span.start}
              tone={span.tone}
            />
          ))}
      </Waterfall.Root>
    </Preview>
  );
}

Set expanded and onToggle on parents to show or hide their children. Leave expanded undefined on leaves.

Critical path

import { Waterfall } from "@openperf/deft";

import { Preview } from "../../src/Preview";
import { TRACE } from "./trace";

const CRITICAL_PATH = new Set(["Span 1", "Span 3", "Span 4"]);

export default function WaterfallCriticalPath() {
  return (
    <Preview block={true}>
      <Waterfall.Root aria-label="Spans" duration={TRACE.duration}>
        {TRACE.spans.map((span) => (
          <Waterfall.Row
            depth={span.depth}
            dimmed={!CRITICAL_PATH.has(span.name)}
            duration={span.duration}
            key={span.name}
            name={span.name}
            start={span.start}
            tone={CRITICAL_PATH.has(span.name) ? "path" : "child"}
          />
        ))}
      </Waterfall.Root>
    </Preview>
  );
}

Set tone="path" on the spans that determined the trace’s latency and dimmed on the rest. This is a timing view, separate from failure status: a healthy span can be critical, and a failing span is not promoted to the path automatically.

Flat density

import { Waterfall } from "@openperf/deft";

import { Preview } from "../../src/Preview";
import { TRACE } from "./trace";

export default function WaterfallFlatDensity() {
  return (
    <Preview block={true}>
      <Waterfall.Root aria-label="Spans" density="flat" duration={TRACE.duration}>
        {TRACE.spans.map((span) => (
          <Waterfall.Row
            depth={span.depth}
            duration={span.duration}
            key={span.name}
            name={span.name}
            start={span.start}
            tone={span.tone}
          />
        ))}
      </Waterfall.Root>
    </Preview>
  );
}

density="flat" is for compact waterfalls embedded in list pages. Rows are shorter and leaves get no chevron space; leave expanded undefined on every row to make a static summary rather than an expandable tree.

Details

import { Waterfall } from "@openperf/deft";
import { Fragment, useState } from "react";

import { Preview } from "../../src/Preview";
import { TRACE } from "./trace";

export default function WaterfallDetails() {
  const [selected, setSelected] = useState<null | string>("Span 3");

  return (
    <Preview block={true}>
      <Waterfall.Root aria-label="Spans" duration={TRACE.duration}>
        {TRACE.spans.map((span) => (
          <Fragment key={span.name}>
            <Waterfall.Row
              depth={span.depth}
              duration={span.duration}
              name={span.name}
              onSelect={() => {
                setSelected(selected === span.name ? null : span.name);
              }}
              selected={selected === span.name}
              start={span.start}
              tone={span.tone}
            />
            {selected === span.name && (
              <Waterfall.Details depth={span.depth}>Details</Waterfall.Details>
            )}
          </Fragment>
        ))}
      </Waterfall.Root>
    </Preview>
  );
}

Waterfall.Details shows what the selected span carries right under its row: stats, an error and its attributes. Render it after the selected row and pass the row’s depth so it lines up with the name. Selecting the row again should close it. Details are not rows: arrow keys skip them, and clicks inside never select a span.

Markers

import { Waterfall } from "@openperf/deft";

import { Preview } from "../../src/Preview";
import { TRACE } from "./trace";

export default function WaterfallMarkers() {
  return (
    <Preview block={true}>
      <Waterfall.Root aria-label="Spans" duration={TRACE.duration}>
        {TRACE.spans.map((span) => (
          <Waterfall.Row
            depth={span.depth}
            duration={span.duration}
            key={span.name}
            markers={
              span.name === "Span 3"
                ? [
                    { at: 60, id: "log", kind: "log" },
                    { at: 90, id: "error", kind: "error" },
                  ]
                : undefined
            }
            name={span.name}
            start={span.start}
            tone={span.tone}
          />
        ))}
      </Waterfall.Root>
    </Preview>
  );
}

Markers show point-in-time events along a span: log circles for log entries and error diamonds for failures. Position them with at so readers see when an event happened without changing the span’s duration.

API

PropType
durationnumber

On Root and Row, in ms.

Typenumber
nameWidth?number

On Root. Width of the name column in pixels, capped at a share of the root.

Typenumber
Default300
density?"tree" | "flat"

On Root.

Type"tree" | "flat"
Default"tree"
nameReactNode

On Row. The span name on the left.

TypeReactNode
leading?ReactNode

On Row. Before the name, such as a service icon.

TypeReactNode
label?ReactNode

On Row. Next to the bar, such as the duration.

TypeReactNode
startnumber

On Row, ms from the trace start.

Typenumber
depth?number

On Row and Details.

Typenumber
Default0
tone?"root" | "child" | "path" | "failing"

On Row.

Type"root" | "child" | "path" | "failing"
Default"child"
dimmed?boolean

On Row. De-emphasizes spans off the critical path.

Typeboolean
Defaultfalse
expanded?boolean

On Row. Undefined for leaves.

Typeboolean
onToggle?() => void

On Row. Expands or collapses its children.

Type() => void
selected?boolean

On Row.

Typeboolean
Defaultfalse
onSelect?() => void

On Row.

Type() => void
markers?{ id: string; at: number; kind: "log" | "error" }[]

On Row. `at` is in ms from the trace start.

Type{ id: string; at: number; kind: "log" | "error" }[]
formatDuration?(milliseconds: number) => ReactNode

On Ruler. Defaults to milliseconds in the active locale.

Type(milliseconds: number) => ReactNode
segments{ id: string; duration: number; className?: string }[]

On Summary. Each segment is as wide as its share of the total.

Type{ id: string; duration: number; className?: string }[]
errorAt?number

On Summary. Error marker, ms from the trace start.

Typenumber

Other props of Root, Details and Summary go to the native div. Parts: Root, Details, Row, Ruler, Summary.

Was this page helpful?