Skip to content

feat(log-viewer): scope the inspector's timeline summary to the window on screen - #999

Draft
lukecotter wants to merge 8 commits into
certinia:mainfrom
lukecotter:feat-inspector-range-scope
Draft

lukecotter wants to merge 8 commits into
certinia:mainfrom
lukecotter:feat-inspector-range-scope

Conversation

@lukecotter

Copy link
Copy Markdown
Collaborator

📝 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

  • The chart publishes what it showscore/log/rangeScope.ts holds 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.
  • The log is read once — 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. Statement counts need no buckets: running totals by start and by end give the statements a window reaches any part of, exactly. Cross-checked against a walk of every event on five windows: self time and counts exact.
  • Time by category and Time by namespace read the window. A selection still wins over it, so a picked frame is answered as itself wherever the timeline is looking.
  • Overview counts the window's statements from the events, not from the cumulative snapshots, which are far too sparse to window — one 100MB log carries two readings across 27 seconds. Every whole-log row stays, in whole-log order, showing 0 where the window ran none, so no row appears or vanishes under a drag.
  • CPU time and heap stay whole-log and say so — the log reports them only in total, and a user who narrowed the view still needs to know the transaction breached. So does a counter the log never reported statement by statement.
  • The window is named — the scope toggle's log side gives its bounds, with enough decimals to keep the two ends apart at a deep zoom. The governor trend charts keep the whole series and shade the window on it, since readings are too sparse to clip. The whole-log call tree says it is whole-log.
  • A seek or a reveal now reports where it moved the chart. Both reached past the chart to its viewport manager, so the summary kept reading the window the chart had left. FlameChart.focusOn focuses and reports; getViewportManager is now the read-only getViewportBounds, so nothing outside the chart can move the viewport without reporting it.

🧩 Type of change (check all applicable)

  • 🐛 Bug fix - something not working as expected
  • ✨ New feature – adds new functionality
  • ♻️ Refactor - internal changes with no user impact
  • ⚡ Performance Improvement
  • 📝 Documentation - README or documentation site changes
  • 🔧 Chore - dev tooling, CI, config
  • 💥 Breaking change

📷 Screenshots / gifs / video [optional]

N/A

🔗 Related Issues

closes #875

✅ Tests added?

  • 👍 yes

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.focusOn reporting, 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?

  • 🔖 CHANGELOG.md
  • 🔖 README.md
  • 📖 help site
  • 🧪 Marked any pre-release-only features

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.ts is 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.total while t is an absolute timestamp (0.03% skew, invisible); GovernorTrends shares one cursor across its four charts; TemporalSegmentTree.queryBucketStats has no callers and its advertised O(log n) is wrong; searchConfig.debounceMs is a dead knob; TimelineViewport.getBounds duplicates calculateViewportBounds.

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
lcottercertinia marked this pull request as draft September 18, 2026 12:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

✨ feat(Inspector): scope stats and call tree to the selected timeline range

1 participant