From 56a49d6e924eeccaba3232b2b9381cbbaa7159cd Mon Sep 17 00:00:00 2001 From: Markus Stange Date: Wed, 24 Jun 2026 16:24:30 -0400 Subject: [PATCH 1/2] Move the flame graph tooltip percentage computation into FlameGraphTiming. The Canvas computed the tooltip percentage itself, by subtracting the box's start from its end. That only works because the non-inverted flame graph happens to lay boxes out relative to rootTotalSummary. Move the computation behind a getRatioOfRootTotalSummary() method on FlameGraphTiming so that each implementation can decide how its box widths relate to the percentage it wants to display. --- src/components/flame-graph/Canvas.tsx | 8 +++---- src/profile-logic/flame-graph.ts | 33 +++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 4 deletions(-) diff --git a/src/components/flame-graph/Canvas.tsx b/src/components/flame-graph/Canvas.tsx index c7b042fa0f..caf03dd00a 100644 --- a/src/components/flame-graph/Canvas.tsx +++ b/src/components/flame-graph/Canvas.tsx @@ -419,10 +419,10 @@ class FlameGraphCanvasImpl extends React.PureComponent { return null; } - const ratio = - stackTiming.end[flameGraphTimingIndex] - - stackTiming.start[flameGraphTimingIndex]; - + const ratio = flameGraphTiming.getRatioOfRootTotalSummary( + depth, + flameGraphTimingIndex + ); let percentage = formatPercent(ratio); if (tracedTiming) { const { total } = getSelfAndTotalForCallNode( diff --git a/src/profile-logic/flame-graph.ts b/src/profile-logic/flame-graph.ts index 24f8347c5a..46eb4d6aa6 100644 --- a/src/profile-logic/flame-graph.ts +++ b/src/profile-logic/flame-graph.ts @@ -52,6 +52,25 @@ export interface FlameGraphTiming { readonly rowCount: number; getRow(depth: number): FlameGraphTimingRow; getAllRowsForTesting(): FlameGraphTimingRow[]; + + // For a given node / "box", get the sample percentage (as a fraction of 1) + // that should be displayed in the tooltip for this node. + getRatioOfRootTotalSummary(depth: number, indexInRow: number): number; +} + +// Returns the width of a box as a fraction of the full flame graph width. +function _getBoxWidth( + flameGraphTiming: FlameGraphTiming, + depth: number, + indexInRow: number +): number { + const row = flameGraphTiming.getRow(depth); + if (indexInRow < 0 || indexInRow >= row.length) { + throw new Error( + `Out-of-bounds call to getRatioOfRootTotalSummary: For depth ${depth}, ${indexInRow} is outside 0..${row.length}` + ); + } + return row.end[indexInRow] - row.start[indexInRow]; } class FlameGraphTimingNonInverted implements FlameGraphTiming { @@ -104,6 +123,12 @@ class FlameGraphTimingNonInverted implements FlameGraphTiming { return rows; } + // For a given node / "box", get the sample percentage (as a fraction of 1) that + // should be displayed in the tooltip for this node. + getRatioOfRootTotalSummary(depth: number, indexInRow: number): number { + return _getBoxWidth(this, depth, indexInRow); + } + _buildNextTimingRow(): void { const { total, self, rootTotalSummary } = this._callTreeTimings; const { prefix } = this._callNodeTable; @@ -435,6 +460,14 @@ class FlameGraphTimingInverted implements FlameGraphTiming { return rows; } + // For a given node / "box", get the sample percentage (as a fraction of 1) that + // should be displayed in the tooltip for this node. In the inverted flame + // graph, box widths are computed relative to rootTotalSummary, so the box + // width is already the ratio we want. + getRatioOfRootTotalSummary(depth: number, indexInRow: number): number { + return _getBoxWidth(this, depth, indexInRow); + } + _compareCallNodesByFuncName = ( a: IndexIntoCallNodeTable, b: IndexIntoCallNodeTable From 76c9da5b3fc12f40c85ca29ec099873d2edf89a2 Mon Sep 17 00:00:00 2001 From: Markus Stange Date: Sun, 21 Jun 2026 10:56:11 -0400 Subject: [PATCH 2/2] Let the flame graph use a width reference other than rootTotalSummary. The flame graph currently derives box widths from rootTotalSummary, which also serves as the 100% reference for the displayed percentages. The upcoming function list wings need those to differ: a wing's root should fill the full width, while percentages should stay relative to all filtered samples. Add a separate flameGraphWidthTotal to CallTreeTimings for the width reference. It equals rootTotalSummary everywhere so far, so nothing changes yet. --- src/profile-logic/call-tree.ts | 17 ++++++++++++++--- src/profile-logic/flame-graph.ts | 10 ++++++---- .../__snapshots__/profile-view.test.ts.snap | 1 + src/test/unit/profile-tree.test.ts | 1 + src/types/profile-derived.ts | 6 ++++++ 5 files changed, 28 insertions(+), 7 deletions(-) diff --git a/src/profile-logic/call-tree.ts b/src/profile-logic/call-tree.ts index f870e0f703..0bd8f5ece6 100644 --- a/src/profile-logic/call-tree.ts +++ b/src/profile-logic/call-tree.ts @@ -51,6 +51,7 @@ export type CallTreeTimingsNonInverted = { self: Float64Array; total: Float64Array; rootTotalSummary: number; // sum of absolute values, this is used for computing percentages + flameGraphWidthTotal: number; // used as 100% reference for flame graph box widths }; type TotalAndHasChildren = { total: number; hasChildren: boolean }; @@ -723,7 +724,11 @@ export function computeCallNodeSelfAndSummary( rootTotalSummary += abs(callNodeSelf[callNodeIndex]); } - return { callNodeSelf, rootTotalSummary }; + return { + callNodeSelf, + rootTotalSummary, + flameGraphWidthTotal: rootTotalSummary, + }; } export function getSelfAndTotalForCallNode( @@ -894,7 +899,8 @@ export function computeCallTreeTimingsNonInverted( callNodeSelfAndSummary: CallNodeSelfAndSummary ): CallTreeTimingsNonInverted { const callNodeTable = callNodeInfo.getCallNodeTable(); - const { callNodeSelf, rootTotalSummary } = callNodeSelfAndSummary; + const { callNodeSelf, rootTotalSummary, flameGraphWidthTotal } = + callNodeSelfAndSummary; // Compute the following variables: const callNodeTotal = new Float64Array(callNodeTable.length); @@ -932,6 +938,7 @@ export function computeCallTreeTimingsNonInverted( total: callNodeTotal, callNodeHasChildren, rootTotalSummary, + flameGraphWidthTotal, }; } @@ -1334,5 +1341,9 @@ export function computeCallNodeTracedSelfAndSummary( } } - return { callNodeSelf, rootTotalSummary }; + return { + callNodeSelf, + rootTotalSummary, + flameGraphWidthTotal: rootTotalSummary, + }; } diff --git a/src/profile-logic/flame-graph.ts b/src/profile-logic/flame-graph.ts index 46eb4d6aa6..a25b674804 100644 --- a/src/profile-logic/flame-graph.ts +++ b/src/profile-logic/flame-graph.ts @@ -126,11 +126,13 @@ class FlameGraphTimingNonInverted implements FlameGraphTiming { // For a given node / "box", get the sample percentage (as a fraction of 1) that // should be displayed in the tooltip for this node. getRatioOfRootTotalSummary(depth: number, indexInRow: number): number { - return _getBoxWidth(this, depth, indexInRow); + const ratioOfFullWidth = _getBoxWidth(this, depth, indexInRow); + const total = ratioOfFullWidth * this._callTreeTimings.flameGraphWidthTotal; + return total / this._callTreeTimings.rootTotalSummary; } _buildNextTimingRow(): void { - const { total, self, rootTotalSummary } = this._callTreeTimings; + const { total, self, flameGraphWidthTotal } = this._callTreeTimings; const { prefix } = this._callNodeTable; const depth = this._timingRows.length; @@ -167,8 +169,8 @@ class FlameGraphTimingNonInverted implements FlameGraphTiming { } startPerCallNode[nodeIndex] = currentStart; - const totalRelative = abs(totalVal / rootTotalSummary); - const selfRelativeVal = abs(self[nodeIndex] / rootTotalSummary); + const totalRelative = abs(totalVal / flameGraphWidthTotal); + const selfRelativeVal = abs(self[nodeIndex] / flameGraphWidthTotal); const currentEnd = currentStart + totalRelative; start.push(currentStart); diff --git a/src/test/store/__snapshots__/profile-view.test.ts.snap b/src/test/store/__snapshots__/profile-view.test.ts.snap index 702b836990..8ae46ca333 100644 --- a/src/test/store/__snapshots__/profile-view.test.ts.snap +++ b/src/test/store/__snapshots__/profile-view.test.ts.snap @@ -2362,6 +2362,7 @@ CallTree { 0, 0, ], + "flameGraphWidthTotal": 2, "rootTotalSummary": 2, "self": Float64Array [ 0, diff --git a/src/test/unit/profile-tree.test.ts b/src/test/unit/profile-tree.test.ts index 48c82e95e1..d535aadd59 100644 --- a/src/test/unit/profile-tree.test.ts +++ b/src/test/unit/profile-tree.test.ts @@ -86,6 +86,7 @@ describe('unfiltered call tree', function () { type: 'NON_INVERTED', timings: { rootTotalSummary: 3, + flameGraphWidthTotal: 3, callNodeHasChildren: new Uint8Array([1, 1, 1, 1, 0, 1, 0, 1, 0]), self: new Float64Array([0, 0, 0, 0, 1, 0, 1, 0, 1]), total: new Float64Array([3, 3, 2, 1, 1, 1, 1, 1, 1]), diff --git a/src/types/profile-derived.ts b/src/types/profile-derived.ts index 10fbae4239..6d7b2fc41f 100644 --- a/src/types/profile-derived.ts +++ b/src/types/profile-derived.ts @@ -877,6 +877,12 @@ export type CallNodeSelfAndSummary = { // The sum of absolute values in callNodeSelf. // This is used for computing the percentages displayed in the call tree. rootTotalSummary: number; + // The total used as the 100% reference for flame graph box widths. + // Usually equals rootTotalSummary, but for the upper wing it is set to the + // total of samples that contain the selected function, so that the root node + // fills the full flame graph width while percentages remain relative to all + // filtered samples. + flameGraphWidthTotal: number; }; /**