Skip to content

fix: keep inspector sections through a move - #1160

Merged
lcottercertinia merged 18 commits into
certinia:mainfrom
lukecotter:bug-database-time-reattach
Oct 8, 2026
Merged

lcottercertinia merged 18 commits into
certinia:mainfrom
lukecotter:bug-database-time-reattach

Conversation

@lukecotter

@lukecotter lukecotter commented Oct 8, 2026 •

Copy link
Copy Markdown
Collaborator

📝 PR Overview

Moving an Inspector section (drag, or Alt+Arrow) detaches it and attaches it again. The pane's keyed repeat does this so that a move keeps each section's state. But four sections dropped their state on detach:

  • Call tree, Call stack and the Database Call tree destroyed their table. They rebuilt it only for a new selection or log, so after a move they stayed empty.
  • Variables stopped a comparison in progress and did not start it again.

Now a move keeps the live state: the rows, the scroll position, the open rows and the selection. No table is rebuilt.

🛠️ Changes made

  • TeardownController (new, core/events/) tears a host's state down only when the host has left the page:
    • On detach, it waits a microtask. A move attaches the host again in the same task, so the controller then does nothing.
    • A host that stays detached is torn down, as before.
    • A host that comes back after a teardown is built again.
  • DatabaseTime, CallTreeDetail, CallStackDetail and VariablesDetail use it in place of their disconnectedCallback.
  • The three table builds skip a host that is detached, so a table is never built into a removed section and left to leak. The controller builds it if the section comes back.
  • CallStackDetail builds again after its render, so Tabulator measures a host that has its layout.

🧩 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

🔗 Related Issues

Follows #1144 and #1148.

✅ Tests added?

  • 👍 yes
  • 🙅 no, not needed
  • 🙋 no, I need help

📚 Docs updated?

  • 🔖 README.md
  • 🔖 CHANGELOG.md
  • 📖 help site
  • 🧪 Marked any pre-release-only features (README 🧪 badge — see RELEASING.md)
  • 🙅 not needed

Anything else we need to know? [optional]

Each of the four sections has a test for a move and a test for a removal followed by a re-attach. All of them fail without this fix. The controller has its own tests for a move, a teardown, a rebuild and a second teardown.

CalltreeView and TimelineFlameChart keep their own re-attach code. It works, so this PR does not move it to the controller.

…ndex

Every render of the visible call tree flattened the whole log into an
array to list its types. The view now lists them once per log, the
first time it is shown, by scanning the LogIndex rows. A log whose call
tree is never opened pays nothing.

The list drops the blank entry a typeless event added: picking it
filtered nothing.

Type list on a 100 MB log:
- before: 65-85 ms on every render
- after: 28 ms once per log
The Timeline legend and the Inspector category bar each walked the whole
event tree. They now sum the index's categoryId and self columns.

The legend's walk ran synchronously when a log opened:
- 100 MB log: 68.4 ms → 0.88 ms
- 20 MB sample: 19.4 ms → 0.32 ms
… and @lit/task

LogStore.derive(fn) runs fn over the log's index once and shares the result.
The Timeline legend, Inspector category bar and Call Tree type filter load
through @lit/task Tasks, which drop stale runs and track loading and error.
The legend and the bar now share one category sum.
A per-row namespace column is derived once per log; each scope then sums
the index's self column over its subtrees.

Each new selection walked its whole subtree; now it sums the index:
- 100 MB log: whole log 72.1 ms → 0.83 ms after a one-off 32 ms column
- 100 MB log, a method's 42,433 calls as one scope: 44.0 ms → 0.92 ms
One sliced pass over the index gives each signature its self time, count
and worst call. The self-time spread, Hot Spots and the diagnostics hot
spot share it, in place of three walks of eventsById. Each view now loads
through a task, so the diagnostics pane only shows the current log's
findings.

Before: each view walked the whole log on its own, synchronously.
After: one shared pass, in slices, then each view reads it.
- 100 MB log: spread 311 → 34 ms, hot spots 357 → 133 ms (the remainder
  is the hot path), diagnostics 244 → 118 ms; shared pass 83–111 ms
- 20 MB log: spread 69 → 2 ms, hot spots 58 → 2 ms, diagnostics 59 → 27 ms;
  shared pass 22 ms
…rapper

SignatureTimes gives the views a ranked list of signatures, the log's
total self time, and the self times of chosen signatures. The ranking and
its tie rule live in one place, so Hot Spots, the spread and the
diagnostics hot spot agree: on a tie, the signature timed first comes
first. The per-row ids stay private.

DerivedValue wraps the task each Inspector and Analysis section used to
load a derived value. It answers only for the log the host shows, so a
section never shows the last log's data.

Log diagnostics is now a LogStore derivation and takes its store as an
argument, in place of a module cache and the current-store global. The
hot-spot label is now the full method text; before, a text with a '|'
lost its start.
The spread collects each lane's self times into an array sized from its
call count and sorts it natively. The hot path groups calls through
nested maps, so it builds no key string per call, and it hands the
thread back between slices.

Median of 5 runs, before → after:
- spread: 40 MB log 24 → 15 ms, 100 MB log 36 → 19 ms
- hot path and hot spots: 40 MB log 67 → 58 ms, 100 MB log 130 → 95 ms,
  now in slices of about 8 ms in place of one block
Statements come from a statements derivation that reads only SOQL and DML
category rows. databaseOverview and rowBudgets are LogStore derivations, so
the per-log WeakMap caches and the LogStore statement lists go. The overview
finds each statement's enclosing statement with a stack over subtreeEnd, and
keeps the old walk order so ties stay the same.

The Database call tree shows its grid skeleton while its rows derive, and
drops the last log's rows on a log switch.
The variable index reads only the rows the parser gives no category, where
variable lines live, rather than walking the whole tree twice. The first pass
keeps the writes whose address can resolve, so the second reads only those.
variableIndexFor becomes the variableIndex LogStore derivation, so its own
per-log caches go.
The series reads only the rows whose category can hold a limit event, in log
order, then puts readings tied in time in the order the tree walk met them, so
the output is unchanged. apexLimitTimeSeries and its per-log cache become the
apexLimitSeries LogStore derivation; the timeline, overview, trends,
diagnostics and database views derive it.
The series builder hands the thread back every CHECK_EVERY readings,
so the sort, coalesce and fold steps on large logs no longer block.
A unit test now pins the order of readings that tie in time.
The Inspector line now names the Analysis and Database views, which
this stack also made faster. The Call Tree line drops its ms figure,
since no total render time was measured.
The Call Tree and Inspector perf lines become one line that names the
tab views and the Inspector sections.
Moving an inspector section detached it. The Call tree, Call stack and
Database call tree sections destroyed their table and rebuilt it only
for a new selection or log, so they stayed empty, and the Variables
section dropped a comparison in progress. A teardown controller now
waits a microtask and keeps the live state when the section is back,
so a move keeps its scroll, open rows and selection. A real removal
still frees the state, and a re-attach after it rebuilds.
lcottercertinia
lcottercertinia previously approved these changes Oct 8, 2026
…eattach

# Conflicts:
#	log-viewer/src/components/CallTreeDetail.ts
#	log-viewer/src/components/CategoryTimeBar.ts
#	log-viewer/src/components/LogOverview.ts
#	log-viewer/src/components/NamespaceTimeBar.ts
#	log-viewer/src/components/__tests__/NamespaceTimeBar.test.ts
#	log-viewer/src/components/__tests__/fixtures/logEvents.ts
#	log-viewer/src/components/namespaceTime.ts
#	log-viewer/src/core/log/frameVariables.ts
#	log-viewer/src/features/database/components/DatabaseTimeTree.ts
@lcottercertinia
lcottercertinia merged commit bde059c into certinia:main Oct 8, 2026
9 checks passed
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.

2 participants