Skip to content

feat(log-viewer): make the inspector call tree readable - #877

Merged
lcottercertinia merged 12 commits into
certinia:mainfrom
lukecotter:feat-inspector-calltree
Jul 30, 2026
Merged

feat(log-viewer): make the inspector call tree readable#877
lcottercertinia merged 12 commits into
certinia:mainfrom
lukecotter:feat-inspector-calltree

Conversation

@lukecotter

@lukecotter lukecotter commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator

Stacked on #876 — its three commits are the first ones here, so review from b0b1ffb3 onward. Rebases away once #876 merges.

Call tree quality

Zero-duration rows are excluded when the tree is built. Parsing ten real logs (839,740 tree nodes) puts these bookkeeping events — HEAP_ALLOCATE 385k, STATEMENT_EXECUTE 207k, VARIABLE_ASSIGNMENT 76k, VARIABLE_SCOPE_BEGIN 15k — at 82.7% of all nodes. The Call Tree tab hides them behind Details; the inspector showed every one, and on a FINEST log they consumed the 20,000-node budget before the tree reached anything worth reading.

The inspector is a summary, so it prunes them rather than offering a toggle — read the detail rows on the Call Tree tab, which is where the full log lives. realSubtree() drops a frame post-order unless it has duration, has a kept descendant, or its type reports limits rather than time (EXCLUDED_DETAIL_TYPES). The selection itself and the path leading to it always show, even with no duration of its own. The budget is still charged per materialised node, so truncation semantics are unchanged.

Bottom-Up caller rows read Calls 0. Only the seed frame incremented callCount. Every bucket in the chain now credits the call it contributed, which is what the Call Tree tab's bottom-up already does (accumulate() runs for the seed and each ancestor).

Grouped modes lead with their ranking metric — Aggregated by Total, Bottom-Up by Self. Time Order stays chronological, since that is the mode's entire point. Headers stay sortable in all three.

Truncation is visible. ScopedCallTree.truncated was computed and never read, so a subtree cut at the budget looked complete. It now reports it, beside a note that times are relative to the selection rather than to the log.

Names say what the frame is, not what event logged it

The same ten logs say the TYPE: prefix was redundant on 99.7% of rows — METHOD_ENTRY: fflib_SObjectDomain.triggerHandler(System.Type) already reads as a method — and load-bearing on 0.2%, where the text is a bare true, a line number, or empty.

So a name is now the frame's text plus its suffix, and the type returns only when the text cannot stand alone (SYSTEM_MODE_ENTER: false). One rule in eventText.ts (eventLabel) replaces three copies of the old type !== text && !excludedTypes.has(type) test, and the per-table excludedTypes sets go away.

Three frames whose text genuinely cannot identify itself gained a suffix instead:

  • CODE_UNIT_STARTED was (entrypoint), wrong for most of them — 394 of 415 occurrences are __sfdc_trigger/... units fired partway through a transaction, alongside Validation:..., VF: /apex/... and Workflow:01I.... Now (code unit), which also distinguishes a batch code unit (ffrr.ffrr.ContractPopulateBatch) from a method entry.
  • ENTERING_MANAGED_PKG — text is a bare namespace token (c2g, dlrs) → (managed package).
  • FLOW_START_INTERVIEW_BEGIN — text is just the flow's label → (flow).

The raw type is still available: Type is now a column in Time Order and Aggregated too (Bottom-Up/Analysis already had one), hidden by default in every view and switchable from the Columns menu, where it sorts and groups like any other column.

Also here

  • fix: the Details section showed Namespace and Caller namespace even when identical, which is the common case. The second row now appears only when the two differ — packaged code entered from another namespace's trigger, the case actually worth seeing.
  • fix: a frame whose text is its type rendered EXECUTION_STARTED: EXECUTION_STARTED. The fallback now short-circuits when text and type match.
  • fix: SYSTEM_CONSTRUCTOR_ENTRY's suffix lacked the leading space every other suffix has, so rows read <init>()(system constructor).

Verified

  • tsc -b, eslint log-viewer/src lana/src apex-log-parser, prettier --check, docusaurus build — clean
  • jest --runInBand — 81 suites / 1204 tests (baseline 81 / 1192)
  • New tests cover the build-time pruning (a bare heap allocation is dropped, a zero-duration statement wrapping a timed method is kept, the selection survives with no duration), caller call-counts, the scope note, both namespace cases, the three parser suffixes, and eventLabel's keep/fallback branches
  • Eyeballed live in a browser harness on a 19.7MB FINEST log: rows read EXECUTION_STARTED, execute_anonymous_apex (code unit), <init>() (system constructor), SYSTEM_MODE_ENTER: false; no label doubling; Type column hidden by default; inspector Details / Call stack / Call tree render correctly in a 500px panel. The two fixes above came out of that pass.

Four fixes to the scoped call tree, all visible on any FINEST log:

- Zero-duration bookkeeping rows (heap allocations, statements, variable
  assignments) are hidden by default behind a Details toggle, as on the Call
  Tree tab. Measured over ten real logs they are 82.7% of parsed nodes, so the
  tree was mostly noise and the node budget was spent on it. The Show-Details
  rule moved from Aggregation to DetailsFilter so all four trees share one
  definition; the selection and the path to it always show, even with no
  duration of their own.
- Bottom-Up caller rows counted no calls, so every one read "Calls 0". Each
  bucket in the chain now credits the call it contributed, matching the tab's
  bottom-up.
- Aggregated leads with Total and Bottom-Up with Self; Time Order stays
  chronological. Headers remain sortable.
- `truncated` was computed but never read, so a cut tree looked complete. It
  now says so, next to a note that times are relative to the selection.
"Namespace: c2g / Caller namespace: c2g" told the reader nothing and cost a row
in a panel where rows are scarce. The row now appears only when the two differ —
packaged code entered from another namespace's trigger — which is the case worth
seeing.
Three suffixes, checked against 124 real logs:

- CODE_UNIT_STARTED was "(entrypoint)", which is wrong for most of them —
  394 of 415 occurrences are `__sfdc_trigger/...` units fired partway through a
  transaction, alongside `Validation:...`, `VF: /apex/...` and `Workflow:01I...`.
  Now "(code unit)", which is both accurate and what distinguishes a batch code
  unit (`ffrr.ffrr.ContractPopulateBatch`) from a method entry.
- ENTERING_MANAGED_PKG's text is a bare namespace token (`c2g`, `dlrs`,
  `java__util`) → "(managed package)".
- FLOW_START_INTERVIEW_BEGIN's text is just the flow's label ("Account Before
  Save LC") → "(flow)". The plural FLOW_START_INTERVIEWS_BEGIN already resolves
  its own more specific suffix once it ends.
Across 10 logs (839,740 nodes) the `TYPE: ` prefix was redundant on 99.7% of
rows — `METHOD_ENTRY: fflib_SObjectDomain.triggerHandler(System.Type)` already
says what it is — and load-bearing on 0.2%, where the text is a bare `true`,
a line number, or empty.

So the name is now the frame's text plus its suffix, and the type comes back
only when the text can't stand alone (`SYSTEM_MODE_ENTER: false`). One rule in
`eventText.ts` (`eventLabel`) replaces the three copies of the old
`type !== text && !excludedTypes.has(type)` test in the Time Order, Bottom-Up
and Inspector formatters, and the per-table `excludedTypes` sets go away —
frames that read as their own statement text no longer need listing.

The raw type is still there: **Type** is now a column in Time Order and
Aggregated too (Bottom-Up/Analysis already had it), hidden by default in every
view and available from the Columns menu, where it sorts and groups like any
other column.
The segmented view-mode switch hardcoded its height, padding and radius, so
it sat taller than the Expand/Collapse/Columns controls beside it. Adopt the
shared --filter-control-* tokens instead.
"Times are relative to the selection" needs no explaining to anyone who has
used a performance panel, so the footer note goes.

The 20,000-node build-time cap silently truncated large scoped subtrees.
Remove it and render the subtree the way the Call Tree tab does — dataTree
starts collapsed and VirtualVerticalRenderer keeps deep expansion lazy — so
the user expands into it instead of hitting a wall.
…ppressing them

Declare `anchoringPolicy` and `rowKeyboardNavigation` on Tabulator's `Options`
next to the modules that register them, and share the virtual-scroll option pair
from TableShared so no table has to remember both halves.
# Conflicts:
#	CHANGELOG.md
#	log-viewer/src/components/CallTreeDetail.ts
@lcottercertinia
lcottercertinia merged commit 7df4d29 into certinia:main Jul 30, 2026
7 checks passed
@lukecotter
lukecotter deleted the feat-inspector-calltree branch July 30, 2026 15:50
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