2.158 seconds of child spans. A 1.044-second request.
This interactive lab shows why adding span durations does not measure request latency. Parallel calls overlap, child detail spans are nested inside their parents, and a naive sum counts the same wall-clock time repeatedly. The trace's causal critical path identifies the operation that actually controls the response time.
The result is generated by a real Standard ML model running on Poly/ML. A dependency-free Python service invokes the typed functional model and exposes correlated OTLP-shaped traces, metrics, and logs.
The default trace fans a search request out to five shards:
| Observation | Value |
|---|---|
| Root request duration | 1,044 ms |
| Sum of every non-root span | 2,158 ms |
| Naive overcount | 2.067× |
| Critical-path span work | 780 ms |
| Parallel work removed | 1,040 ms |
| Nested detail counted twice | 338 ms |
| Peak shard concurrency | 5 |
The naive sum mixes three different things:
- independent shard calls that run at the same time;
decode response, which is already inside the slow shard span;- sequential work that really is on the response's causal path.
The critical path is:
authenticate → GET /shard/5 → merge results → render response
It isolates search-shard-5 as the bottleneck without pretending that
parallel time happened serially.
The lab keeps the trace DAG intact and correlates:
- parent and child span IDs;
- nanosecond start and end timestamps;
SERVER,CLIENT, andINTERNALspan kinds;- the root
http.server.request.duration; - custom comparison metrics for naive sum, critical-path work, overcount, and peak concurrency;
- fan-out and bottleneck logs tied to the same trace ID;
- critical-path membership and service identity on every operation.
A duration table encourages arithmetic. A trace waterfall preserves causality, overlap, nesting, and the idle gaps that arithmetic erases.
Requirements:
- Poly/ML 5.7 or newer
- Python 3.11 or newer
- GNU Make
make check
make runOpen http://127.0.0.1:3000.
Useful endpoints:
GET /api/simulate
GET /api/telemetry
GET /healthz
Every control is also a query parameter:
/api/simulate?branches=5&normalBranchMs=260&slowBranchMs=520&overlapPct=90&nestedCoveragePct=65&rootOverheadMs=90
docker compose up --buildThe image installs Debian's Poly/ML toolchain, executes the Standard ML model self-test during the build, and starts the lab on port 3000.
browser controls
│
▼
Python standard-library HTTP service
│
├──▶ Standard ML trace-DAG model
│ ├── parent/child spans
│ ├── parallel fan-out
│ ├── nested detail
│ └── critical-path accounting
│
└──▶ correlated OTLP-shaped evidence
├── trace waterfall
├── duration comparison metrics
├── fan-out logs
└── critical-path diagnosis
The browser does not calculate durations, paths, concurrency, or overlap. It renders the Standard ML model's output.
- Add fan-out branches. The naive sum grows rapidly while request latency changes much less.
- Reduce overlap. The same shard work becomes increasingly serial, so root duration rises and the naive sum becomes less misleading.
- Increase nested coverage. The detail span adds no wall time but inflates the naive sum.
- Increase the slow shard duration. Both request latency and critical-path work rise because that operation really gates the join.
- Increase root overhead. The root span exposes time that no child span explains.
- The OpenTelemetry overview defines a trace as a directed acyclic graph of spans connected by parent/child relationships.
- The OpenTelemetry tracing API defines span timestamps, duration, parenthood, and span lifetime.
- The OpenTelemetry observability primer describes waterfall diagrams as views of parent/child relationships.
This is a deterministic educational model. It does not generate upstream traffic, introduce latency, export telemetry, or contact external services.