Where did the latency go? — critical path and self time in a waterfall

A waterfall answers three questions at a glance — which span holds time its children do not account for, which children overlap and which wait on each other, and where there is no span at all — and only the chain that does not overlap can be shortened.

Previously

The right trace is open on screen. Reading it is a skill, not a glance — the biggest bar is usually a decoy.

Scene 14

Where did the latency go?

  1. Watch
  2. Try it
  3. Predict
  4. Capture
reading a waterfall: self time, critical path, repeatsONE REQUEST, EVERY SPAN IT MADE0750ms1.5s2.3s3.0s0 → 3.0 s (example)POST /checkout3.0scheckout2.9sGET /cartstock-checkcharge2.6sPOST /score2.4sread-balanceappend-entryappend-entryappend-entry+197 more append-entry1.2sorders-worker: no span1.5shatched = time spent in childrenring = on the critical path×N×N = identical spans collapsedREADOUTrequest, end to end3.00 scritical path (dependent chain)2.60 swidest bar off that path1.20 s — append ×200optimisation appliednone yetTIME TO CULPRIT4 minexampletarget ≤ 5 minNine spans of one 3.0 s checkout (example). Four bars are over a second. Which one is worth your week?
four bars are over a second — three of them only because they contain the fourth
What to watch for

You are holding the right trace at last — so which line of it do you actually go and fix? Watch the same 3 s checkout get read in four passes. First the bars alone. Then each bar split into the part it spent waiting inside a child and the part it held itself — scene 02 described that second part in plain words and deliberately did not name it; here it gets its name, self time. Then the one chain of spans that each had to wait for the next gets ringed. Then 200 identical calls to the legacy ledger service fold into a single row.

Continue unlocks when the animation finishes.
Implementation

Highlighted lines are the ones running in the diagram right now.

Span.self_time
duration minus the union of the children's intervals
def self_time(span):
covered = 0
cursor = span.start_ms
for child in sort_by_start(span.children):
if child.end_ms <= cursor:
continue
covered += child.end_ms - max(child.start_ms, cursor)
cursor = child.end_ms
return span.duration_ms - covered
Trace.critical_path
walk back from the end, last-finishing child each step
def critical_path(span, ends_at):
chain = [span]
cursor = ends_at
for child in sort_by_finish(span.children, reverse=True):
if child.end_ms > cursor:
continue
chain += critical_path(child, child.end_ms)
cursor = child.start_ms
return chain
Viewer.collapse_repeats
same operation, same peer, k times — one row
def collapse_repeats(siblings, fold_repeats):
key_of = lambda s: (s.name, s.peer)
rows = []
for key, group in group_by(siblings, key_of):
if not fold_repeats or len(group) < FOLD_MIN:
rows += group
continue
first = min(s.start_ms for s in group)
last = max(s.end_ms for s in group)
rows.append(fold(key, first, last - first,
badge=f'×{len(group)}'))
return rows

Where this sits in Build a distributed tracing system (Jaeger / Zipkin style)

Scene 14 of 17, in the Store & read act — Key by trace id, find it, read it, distrust it.. The waterfall answers three questions at a glance: which span holds time its children don't account for, which children overlap, and where there is no span at all. Only the chain that doesn't overlap can be shortened.

Up next. You named a culprit from the shape of the bars. Can you trust the bars?

All 17 scenes in Build a distributed tracing system (Jaeger / Zipkin style) · Every curriculum

Built with Arqly
Every scene in Build a distributed tracing system (Jaeger / Zipkin style) builds on the one before it.All 17 Build a distributed tracing system (Jaeger / Zipkin style) scenes