Skip to content

[draft] ui: flamegraph collection - #7146

Draft
gignat-dev wants to merge 6 commits into
mainfrom
dev/gignat/flamegraph_collection
Draft

[draft] ui: flamegraph collection#7146
gignat-dev wants to merge 6 commits into
mainfrom
dev/gignat/flamegraph_collection

Conversation

@gignat-dev

Copy link
Copy Markdown
Contributor

No description provided.

@github-actions

github-actions Bot commented Aug 20, 2026

Copy link
Copy Markdown

@gignat-dev
gignat-dev force-pushed the dev/gignat/flamegraph_collection branch from 053b42c to 20edc2e Compare August 21, 2026 08:56
Each metric materializes a persistent flamegraph operator table, cached
by metric object identity and dropped only when the QueryFlamegraph is
disposed. Callers that cycle through many metric arrays (stepping
through profiles, re-filtering a merged set) therefore accumulate one
materialized source table per array with no bound.

Evict tables whose metric is no longer reachable from the current
metrics array at fetch time. The eviction runs inside the query
limiter, so it is serialized against every task that creates or reads
these tables; switching between metrics of one array still hits the
cache.
Introduce FlamegraphCollection (ui/src/components): a view for traces
holding many related flamegraphs -- a DataGrid with one row per entry
above a flamegraph. The grid's filters select the working set; with
Merge on the selection is summed into a single flamegraph, with Merge
off the entries are stepped through one at a time (chevrons, arrow
keys, click-to-jump) with a shared measure, view and flamegraph filters
so entries are compared like for like. Consumers supply rows, columns,
a stable entry key and a metricsForKeys(keys) factory; merged mode is
metricsForKeys(allWorkingKeys), step mode metricsForKeys([key]) per
entry. Grid state persists through a real zod schema, and the arrow-key
handler ignores hidden collections (area-selection tabs stay mounted
under Gate).

Grid id cells derive the entry they highlight and jump to from the
row's entry key, never from the displayed text: labels (a thread or
process name) need not be unique. DataGrid hands cell renderers only
the visible columns' values, so the component fingerprints those
against the full data rows to recover the key; a row that cannot be
resolved uniquely (the aggregate-totals row, or rows indistinguishable
across every visible column) renders inert rather than guessing an
entry. In step mode the flamegraph is titled by the entry's display
label, falling back to its key. DataGrid's pivot controls are disabled:
pivoting would regroup only the grid while the flamegraph keeps reading
the flat rows.
An archive with dozens of pprofs currently imports as an undifferentiated
blob: every profile lands on the hard-coded "pprof_file" scope. Scope each
profile by its source file instead: TraceFileTracker gains a CurrentFile()
accessor and the pprof reader walks the file-nesting chain past the gzip
layer to the nearest named ancestor. The file id is captured on the first
Parse() call -- ParseProfile runs from OnPushDataToSorter, which may fire
after the end-of-file notifications have popped the trace-file parsing
stack, so reading CurrentFile() there would make scope attribution depend
on pipeline ordering.

Locations without a pprof mapping each allocated a fresh dummy mapping, so
identical frames never interned across profiles and callsites multiplied
by the profile count: a 32 MB / 1000-profile archive peaked at 1.8 GB and
OOMed the Wasm heap. Intern the shared fallback mapping and skip
re-symbolizing interned frames; the same archive now peaks at 647 MB.

Diff tests lock the behaviour in: zip/tar members are scoped by member
file name (walking past the unnamed gzip layer), directly opened pprofs
keep the "pprof_file" scope, duplicate archive members do not duplicate
frames/mappings/symbols, and mapping-less locations share a single
interned "[unknown]" fallback mapping (asserted with a minimal
hand-encoded pprof, since no checked-in pprof lacks mappings).
AggregateProfiles adopts FlamegraphCollection to organize archives: one
grid row per profile, one grid column and flamegraph measure per sample
type found across the archive. Merging is a single query -- callsites
are interned globally, so same-stack samples combine across profiles --
and only the selected subset touches the callstack tables, so the page
scales to thousands of profiles. Profile-only traces land on the page
directly via initialPage.

The Playwright test assembles a tar of the two checked-in pprofs in the
test itself (new PerfettoTestHelper.openTraceBuffer), so no new
GCS-hosted test data or screenshot baselines are needed.
Each stack-sample area tab (one per profiler source: linux.perf,
instruments, chrome, gecko, simpleperf, perf_text, legacy_v8) rendered
a single flamegraph merging every selected track. Render a
FlamegraphCollection instead: one grid row per selected execution
context (thread/process, optionally per profiler session) with its
sample count and per-counter totals over the selection window. Merge
stays the default, so the previous output is unchanged; the grid adds
filtering down to a subset and stepping through contexts one at a time.

The context tag-walk, key encoding and SQL fragments move to pure
helpers emitting byte-compatible SQL, and metric ids are now stable
(counter:<name> / sample_count) so the selected metric survives
working-set changes. Persisted per-source FlamegraphStates migrate into
the new collection states on load.
The heap-profile area tabs refused any selection spanning more than one
process. Render a FlamegraphCollection instead, with one row per
process (dump count, allocation totals). flamegraphMetricsForHeapProfile
generalizes to a list of upids: each process window is right-extended
to its own next dump (per-upid alloc_bound) and the
unreleased-allocation classification is keyed (upid, callsite_id), so
merging processes never mixes their release accounting and a single
upid produces the same window and classification as before. The heap
name is now escaped through sqliteString. Persisted per-profile-type
FlamegraphStates migrate into the new collection states on load.
@gignat-dev
gignat-dev force-pushed the dev/gignat/flamegraph_collection branch from 20edc2e to d14b7f1 Compare August 24, 2026 08:56
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.

1 participant