web/src/features/traces/components/TraceTimelineV2/TimelineV2.mdx
import { Canvas, Meta } from "@storybook/addon-docs/blocks"; import * as TimelineV2Stories from "./TimelineV2.stories"; import * as SizeStories from "./TimelineV2Sizes.stories"; import * as CompressionStories from "./TimelineV2Compression.stories"; import * as MobileStories from "./TimelineV2Mobile.stories";
<Meta of={TimelineV2Stories} name="Findings" />A throwaway renderer over a pure layout(), built to answer one question before
any production code moves: does fit-to-box + zoom + time compression read
better than today's fixed 900px canvas, at every size and shape?
Verdict: yes, and by a wide margin. The model holds at every cell of the matrix. Two things it does not solve on its own are called out below: the name gutter below ~480px, and what a collapsed gap should look like.
At 320px the trace is whole, the axis is honest and the durations are readable.
That is the claim, and it holds without a single conditional on viewport width:
layout() gets the measured box and everything else follows. The 1920px cell
gets 3.37 px/ms for the same trace, from the same code path.
Where it does not win on its own:
RunnableSequence renders as
Runna…. Fit-to-box fixed time, not identity. See the mobile section.endTime traces
render a column of dashed minimum-width markers. That is honest — there is
genuinely no duration to draw — but a trace where nothing has an end would
read better as a list than as a chart. Worth a Phase 2 decision, not a
renderer change.Both are the exact failure class the architecture exists to remove, and both are worth remembering for Phase 2:
box prop while
the actual content area was 2px narrower, so gutter + lane exceeded the
viewport and produced 2px of horizontal scroll. Fixed by measuring the scroll
viewport's own client box with a ResizeObserver rather than deriving it.| Take | Lane | Verdict |
|---|---|---|
split — gutter beside chart | 191px | Correlation kept, names useless (Chat…) |
icons — gutter is icon + caret | 272px | Chart readable, identity gone entirely |
overlay — names over the bars | 318px | Worst: name, duration and bar fight for the same pixels, depth lost |
stacked — name above, bar below | 318px | Best. Full names, full-width chart, depth by indent, durations kept |
modes — tree ⇄ timeline toggle | 318px | Honest, but correlating a name to a bar costs a mode flip |
Recommendation: stacked for pointer: coarse / narrow boxes, split
above ~480px. It is the only take that keeps all three of identity, duration
and position, and it costs only row height — which touch density was already
spending (44px). Note this moved density's second axis somewhere the plan did
not anticipate: resolveDensity takes lines: 1 | 2, because a row that stacks
a name over a bar needs two text lines' worth of height.
Density is not a function of box height. The first revision grew rows into the slack of a tall box (a 3-row trace got 34px rows in a 420px box); review rejected it on sight — density is the whole point of the parent ticket, so the box decides how many rows are visible, never how tall they are. Row height resolves from modality and lines-per-row only: 26px fine, 44px coarse, 38px stacked.
Does this need a designer? For the choice, no — stacked is clearly
ahead. For the finish, yes: row separation, what a selected row looks like,
and whether the type icon stays on the name line are visual calls, and the
stacked row is a new pattern in this product rather than a tweak to an existing
one.
Fitted honestly, a 36.02s trace whose real work is three 40ms spans gives 0.005 px/ms: three 4px slivers pinned to the minimum bar width. This is exactly the "36.02s root drawn as a sliver" complaint, reproduced.
<Canvas of={CompressionStories.LongTailCompressed} />Compressed: 0.372 px/ms, a 74× change, and the spans are real bars with readable labels. Two collapsed bands cost 28px each — 29% of a 191px lane spent saying "nothing happened here".
Recommendation: default OFF, one click away, and auto-suggested. Reasons:
layout() reports gaps that would collapse,
offer it in place — "36.02s, 99.6% idle · compress?" — rather than making
every user carry a preference.Two things compression needs before it ships, both learned here:
0ms. With them the
axis reads 0ms · 17.95s · 35.91s and the visible jump across a 28px band is
what explains the band.⋯. The current treatment is a dashed grey column
with a hover title. It is legible-but-mute, and 28px cannot hold 17.84s.
This is a small design ask.The plan bets that vertical virtualization keeps the mounted row count low enough that recomputing every visible bar per frame in React is affordable, so we can adopt Sentry's 146-line space model and skip its 2447-line imperative view manager. The bet is correct, with three orders of magnitude to spare.
Measured on the 10k-span story, 768×420 box, 486px lane:
| Measurement | Result |
|---|---|
| Mounted bars for 10,000 rows | 20 |
layout(), 40-row window, per call | 0.0070 ms (0.0046 ms compressed) |
| Frame time, 235 frames of continuous ctrl-wheel zoom | p50 8.3 ms, p95 9.3 ms, max 9.4 ms |
| Frames over 16.7 ms | 0 |
| Same measurement with no zoom driven (baseline) | p50 8.3 ms, max 9.4 ms |
| Zoom range actually swept during the run | 0.02 → 486 px/ms |
prepareTimeline(10k) — memoized on tree + collapse | 1.92 ms |
timeCompressionFor(10k) — memoized on box width | 2.05 ms |
Frame time under continuous zoom is indistinguishable from idle, and the pure math is ~0.007ms against a 16.7ms budget. Measured in Storybook's dev build, so React is unminified and in development mode — production is faster, making these numbers a pessimistic bound.
Conclusion: adopt the space model, skip the view manager. If a future shape
ever does drop frames, layout() is the seam an imperative write path plugs
into without redesigning anything above it — the two calls that are not cheap
(prepareTimeline, timeCompressionFor) are already memoized out of the frame
loop.
DURATION_LABEL_BUFFER_PX = 48 to milliseconds using the
uncompressed px/ms. In a 320px lane that reserves ~5.4 seconds around each
span of a 36s trace, and compression becomes a no-op precisely where it is
needed most. One fixed-point step — compress with no buffer, then re-compress
with the buffer priced at the resulting scale — takes the long-tail case from
~15px of legible bar to ~41px.trace_view.x and shrinks the window; we hold the window and pull x back.
Panning past the end should stop, not silently zoom out.mat3. Both spaces are
one-dimensional in time, so the matrix composition buys nothing.gl-matrix, no TraceScheduler, no DOM-ref write path.Pure and tested, no React: layout(), the four-space view model, compression,
the text measurer, density resolution. TreeNode already satisfies LayoutNode
structurally, so there is no adapter to write.
Suggested sub-issue split:
layout() → absolute placement, SCALE_WIDTH deleted, gutter/chart split,
virtualization, playhead and selection kept working. Closes the community
issue; its acceptance test is Reporter Shape at peek width.stacked composition below 480px,
pointer: coarse resolution, and a tap equivalent for every hover-only
affordance in TimelineBar.