feat(log-viewer): scope the inspector's timeline summary to the window on screen - #999
Draft
lukecotter wants to merge 8 commits into
Draft
lukecotter wants to merge 8 commits into
lukecotter wants to merge 8 commits into
Conversation
The inspector's Timeline summary could only ever read the whole log, so zooming in left the figures answering a question the user had moved on from. The chart now records the stretch of log it shows, and everything else reads it. One publish per frame, coalesced, so the figures follow a gesture instead of landing after it. - `core/log/rangeScope.ts` holds the window, with the chart as its only writer: it owns the viewport, so it also decides when one is wide enough to be the whole log. - State rather than an event, so a section built after the last viewport change still opens on the window the user is looking at. - `windowFor` reads a viewport as a window or as the whole log. A full zoom-out sets zoom to width over span, and reading the width back out of that division lands an ULP either side, so the whole-log test carries a nanosecond of slack. A viewport of no width is no window: before layout the bounds collapse, and a window of nothing would read as a stretch of log where nothing ran.
Clicking a governor usage chart, or revealing a frame from the call tree, moved the chart but told nothing: both reached past the chart to its viewport manager and then asked for a repaint, which reports nowhere. So the inspector's summary kept reading the window the chart had left. `FlameChart.focusOn` focuses and then reports, and both callers use it. The repaint they asked for goes with them, since reporting a change already renders. Every other way the viewport moves already reported itself: the interaction handler, `ViewportAnimator`, the minimap lens, `resetZoom`, area zoom, and focus on a frame or a marker.
`getViewportManager` handed out the live viewport, so any caller could move it without reporting where it landed. That is what the seek and the reveal did, and nothing stopped the next caller doing it again. Its one remaining caller only read the bounds, so it is now `getViewportBounds`. The viewport is private with no public way out, and `focusOn` is the only way in.
The window moves with the gesture, so a walk per window would put a walk of the log in every frame of a drag. On a 100MB log, 431k calls, that measured 27ms for a narrow window and 175ms for a wide one, per section, per frame. The log is read once instead, into an index any stretch of it can be read from. Measured on the same log: 117ms to build, once; 0ms for a window whatever its width; 6ms for sixty windows, a whole drag. Cross-checked against a walk of every event on five windows, self time and counts exact. - Self time by category and by namespace is bucketed by time and kept as a running total, so a window's whole buckets are one subtraction each. Only the part bucket at each edge is read event by event, about a hundred events on the largest logs. Self time sits in the gaps between an event's children, and those gaps never overlap another event's anywhere in the tree, so the buckets add up. - Statement counts need no buckets: running totals by start and by end give the statements a window reaches any part of, exactly. Only the events carrying a counter are held. - One build serves every reader and nothing abandons it. A build tied to one window would restart every frame and never finish.
…e's window Over a whole log these two bars answer a question the log level already answers. Over a window they invert: on one 100MB log, System goes from 11% of the log to 61% of a window, Automation from 7.7% to 35%, and one package from 2.5% to 15.4%. That is the reading a user zooms in to get. Both read the window from the index. A selection still wins over it: a picked frame is answered as itself, wherever the timeline is looking. While the window answers, the whole-log walk waits, since its result would be thrown away. The shared event fixture now lays its tree out in time, so a windowed test reads real figures rather than passing because a window scores everything zero.
The Overview renders first, so whole-log governor figures above windowed ones is the confusion to avoid. The statements a window ran now come from the events themselves. Not from the cumulative snapshots, which are far too sparse to window: one 100MB log carries two readings across 27 seconds, so a delta reads zero for windows where statements provably ran. - Every whole-log row stays, in whole-log order, showing 0 where the window ran none. Rows that appear and vanish under a drag are worse than a zero. - CPU time and heap keep their whole-log figure and say so: the log reports them only in total, and a user who narrowed the view still needs to know the transaction breached. - A counter the log never reported statement by statement also keeps its whole-log figure. Its total came from the cumulative block, which no window can cut, and reading 0 would say no statements ran. - The window is a Timeline idea, so the Overview on every other tab reads the whole log. The tab panels all stay mounted, so it has to be told.
… log Windowed figures with nothing saying so read as wrong figures. The scope toggle's log side now names the window's own bounds, and with no selection those bounds stand on their own above the sections. - The bounds carry enough decimals to keep their two ends apart: a seek window is 2% of the log, where two figures to the same place would say nothing. - The governor trend charts keep the whole series and shade the window on it. Readings are sparse, so a chart clipped to a short window would draw nothing. - The whole-log call tree says it is whole-log rather than contradicting the sections above it in silence. Pruning it to the window is its own problem: a wide window already costs the cheap aggregation 117ms of index build, and a tree needs more. Closes certinia#875
Two conflicts: - CHANGELOG: main trimmed the Unreleased entries, so the range scope is one short entry in that style. - measure: main split measure.ts into an area per feature, so the range-scope timings move into scripts/measure/window.ts as their own area, run by `pnpm measure window`.
lcottercertinia
marked this pull request as draft
September 18, 2026 12:30
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
📝 PR Overview
The inspector's Timeline summary could only read the whole log, so zooming into the two seconds you care about left every figure answering the whole transaction. A window changes the answer: on one 100MB log, System goes from 11% of the log to 61% of a window, Automation from 7.7% to 35%, and one package from 2.5% to 15.4%.
The Timeline now publishes the stretch of log it shows, and the summary follows it live. Reading a window costs 0ms because the log is read once into an index: 117ms to build, then any window at any width is free (before: 27ms narrow, 175ms wide, per section, per frame of a drag).
🛠️ Changes made
core/log/rangeScope.tsholds the window with the chart as its only writer. State rather than an event, so a section built after the last viewport change still opens on the window the user is looking at. One publish per frame, coalesced.FlameChart.focusOnfocuses and reports;getViewportManageris now the read-onlygetViewportBounds, so nothing outside the chart can move the viewport without reporting it.🧩 Type of change (check all applicable)
📷 Screenshots / gifs / video [optional]
N/A
🔗 Related Issues
closes #875
✅ Tests added?
pnpm test— 1667 tests, 141 suites. New coverage for the index (bucket edges, unclosed frames, overlap counting, exactness against event times),windowFor(whole-log float slack, degenerate and non-finite viewports),FlameChart.focusOnreporting, the windowed gauges, and the namespace bar mounting into a window.The shared event fixture now lays its tree out in time, so a windowed test reads real figures rather than passing because a window scores everything zero.
📚 Docs updated?
Anything else we need to know? [optional]
Reviewer guidance — 7 commits, each standing on its own; the perf commit is the one to read closely.
core/log/windowStats.tsis the new module. The shape follows Chrome DevTools'statsForTimeRange, which keeps per-category cumulative self time and answers a range with two binary searches; it is bucketed here rather than kept per category transition, because Apex changes category on nearly every event and per-transition arrays would cost about 28MB on a 100MB log against about 430KB bucketed.Not in scope — pruning the whole-log call tree to the window. It is the least valuable quarter of #875 and needs its own approach.
Seen while working here, each for its own issue — the trend charts map a point as
t / duration.totalwhiletis an absolute timestamp (0.03% skew, invisible);GovernorTrendsshares one cursor across its four charts;TemporalSegmentTree.queryBucketStatshas no callers and its advertised O(log n) is wrong;searchConfig.debounceMsis a dead knob;TimelineViewport.getBoundsduplicatescalculateViewportBounds.