Skip to content

model: index the children of wide nodes in StacktraceTree - #5759

Open
evkuzin wants to merge 3 commits into
grafana:mainfrom
evkuzin:upstream-model-stacktrace-tree-wide-index
Open

evkuzin wants to merge 3 commits into
grafana:mainfrom
evkuzin:upstream-model-stacktrace-tree-wide-index

Conversation

@evkuzin

@evkuzin evkuzin commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

StacktraceTree.Insert finds a child by walking its parent's sibling list, so inserting under a node with N children costs O(N). Profiles of whole hosts have nodes with thousands of children: stacks that are cut short (failed unwinds, truncated stacks) all hang off the root, each with a distinct leaf. Every insert then scans those siblings, and building the tree of such a profile is dominated by that scan.

This is the same fix as #5732 for symdb.stacktraceTree, applied to model.StacktraceTree, which the query path uses to build the intermediate tree of each stack trace range.

Change

  • An insert scans a node's siblings as before, up to 64 of them. Only when a node has more children than that are they indexed by location, in an open-addressing table (the same childIndex design as symdb: index children of wide stack trace tree nodes #5732, which allocates a few times per tree rather than once per entry).
  • New children are still appended after the last sibling, so the resulting tree is identical to the one built by scanning. Reset clears the index.
  • Trees with no wide nodes pay only a counter per level.

Tests

  • Test_StacktraceTree_Insert_matches_sibling_scan: the nodes built by Insert are identical to those of a plain sibling-scan reference, on wide and narrow synthetic stacks and on pkg/phlaredb/symdb/testdata/big-profile.pb.gz.
  • Test_StacktraceTree_Reset_clears_wide_index.

Benchmarks

cpu: AMD EPYC Processor (with IBPB)
                                                │   main.txt    │             branch.txt              │
                                                │    sec/op     │    sec/op     vs base               │
_StacktraceTree_Insert/wide-32                   3697.7m ±  5%   466.9m ± 18%  -87.37% (p=0.000 n=10)
_StacktraceTree_Insert/narrow-32                  259.2m ± 17%   262.9m ±  9%        ~ (p=0.739 n=10)
_StacktraceTree_Insert/profile-32                 353.4m ±  4%   384.7m ±  8%   +8.84% (p=0.009 n=10)
_StacktraceTree_Insert_fanout/children=8-32       11.91m ±  3%   12.04m ±  3%        ~ (p=0.739 n=10)
_StacktraceTree_Insert_fanout/children=16-32      12.97m ±  2%   12.98m ±  4%        ~ (p=0.853 n=10)
_StacktraceTree_Insert_fanout/children=32-32      15.58m ±  4%   15.27m ±  1%   -2.01% (p=0.035 n=10)
_StacktraceTree_Insert_fanout/children=64-32      17.88m ±  5%   17.35m ±  2%   -2.99% (p=0.001 n=10)
_StacktraceTree_Insert_fanout/children=128-32     23.46m ±  2%   20.53m ±  2%  -12.49% (p=0.000 n=10)
_StacktraceTree_Insert_fanout/children=256-32     31.42m ±  2%   22.73m ±  3%  -27.67% (p=0.000 n=10)
_StacktraceTree_Insert_fanout/children=1024-32    85.01m ±  1%   24.99m ±  2%  -70.61% (p=0.000 n=10)
_StacktraceTree_Insert_fanout/children=4096-32   233.90m ±  2%   28.66m ±  2%  -87.75% (p=0.000 n=10)
  • fanout sweeps the number of children of the hottest node (visits skewed towards the first children, as in real profiles). The index is neutral below 32 children and never slower at any width; the gain grows with width.
  • wide: synthetic stacks with 5000 distinct root frames and 500 second frames.
  • narrow: deep stacks over 4 frames per level; no node becomes wide.
  • profile: big-profile.pb.gz is 9% slower. It has no node wide enough to benefit, and pays for tracking the last sibling on each scan. This is the same trade-off as in symdb: index children of wide stack trace tree nodes #5732
  • Bytes allocated are flat; the extra allocations (+2 to +8 per tree) are the index table growing, only once a node exceeds 64 children.

In production (whole-host CPU profiles), StacktraceTree.Insert went from ~21% to ~3% of query-backend CPU under load.

@simonswine simonswine left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for bringing this ingestion optimisation to the query path.

In our workload we barely see any regression, but also no improvement. I think for us it is very uncommon to have a node with more than 64 children. full report

Before we can merge this we need to fix the clash with the truncation marker and add a regression test case.

Btw: I also run the benchmarks from a draft PR of mine against it: #5661

See the results here: https://simonswine.github.io/benchstat-prof-reports/reports/pyroscope-5759-main-common-100x/report.html

Comment thread pkg/model/stacktraces.go Outdated
if i, ok := t.wide.get(childKey(parent, location)); ok {
return i
}
tail := childKey(parent, sentinel)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using sentinel here will mix up wide nodes and truncated children nodes. They get marked here btw:

dst = append(dst, sentinel)

I think we should likely use a separate constant at least for marking wide nodes (potentially for both).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thx for spotting this one!

@github-actions

github-actions Bot commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

Signed commits report

All 3 commits between main and upstream-model-stacktrace-tree-wide-index have verified signatures. ✅

@evkuzin
evkuzin force-pushed the upstream-model-stacktrace-tree-wide-index branch from 1a30f45 to b72e8d6 Compare October 15, 2026 14:28
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