Skip to content

Commit 2e38000

Browse files
committed
feat: invert call stack in flame graph to render as icicle graph
1 parent fdb0dce commit 2e38000

10 files changed

Lines changed: 240 additions & 92 deletions

File tree

src/components/flame-graph/Canvas.tsx

Lines changed: 33 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,10 @@ class FlameGraphCanvasImpl extends React.PureComponent<Props> {
125125
// selection or applying a transform), move the viewport
126126
// vertically so that its offset from the base of the flame graph
127127
// is maintained.
128-
if (prevProps.maxStackDepthPlusOne !== this.props.maxStackDepthPlusOne) {
128+
if (
129+
!this.props.isInverted &&
130+
prevProps.maxStackDepthPlusOne !== this.props.maxStackDepthPlusOne
131+
) {
129132
this.props.viewport.moveViewport(
130133
0,
131134
(prevProps.maxStackDepthPlusOne - this.props.maxStackDepthPlusOne) *
@@ -151,16 +154,21 @@ class FlameGraphCanvasImpl extends React.PureComponent<Props> {
151154
}
152155

153156
_scrollSelectionIntoView = () => {
154-
const { selectedCallNodeIndex, maxStackDepthPlusOne, callNodeInfo } =
155-
this.props;
157+
const {
158+
selectedCallNodeIndex,
159+
maxStackDepthPlusOne,
160+
callNodeInfo,
161+
isInverted,
162+
} = this.props;
156163

157164
if (selectedCallNodeIndex === null) {
158165
return;
159166
}
160167

161-
const callNodeTable = callNodeInfo.getCallNodeTable();
162-
const depth = callNodeTable.depth[selectedCallNodeIndex];
163-
const y = (maxStackDepthPlusOne - depth - 1) * ROW_HEIGHT;
168+
const depth = callNodeInfo.depthForNode(selectedCallNodeIndex);
169+
const y = isInverted
170+
? depth * ROW_HEIGHT
171+
: (maxStackDepthPlusOne - depth - 1) * ROW_HEIGHT;
164172

165173
if (y < this.props.viewport.viewportTop) {
166174
this.props.viewport.moveViewport(0, this.props.viewport.viewportTop - y);
@@ -192,6 +200,7 @@ class FlameGraphCanvasImpl extends React.PureComponent<Props> {
192200
viewportTop,
193201
viewportBottom,
194202
},
203+
isInverted,
195204
} = this.props;
196205

197206
const { hoveredItem } = hoverInfo;
@@ -232,14 +241,12 @@ class FlameGraphCanvasImpl extends React.PureComponent<Props> {
232241
fastFillStyle.set(getBackgroundColor());
233242
ctx.fillRect(0, 0, deviceContainerWidth, deviceContainerHeight);
234243

235-
const callNodeTable = callNodeInfo.getCallNodeTable();
236-
237-
const startDepth = Math.floor(
238-
maxStackDepthPlusOne - viewportBottom / stackFrameHeight
239-
);
240-
const endDepth = Math.ceil(
241-
maxStackDepthPlusOne - viewportTop / stackFrameHeight
242-
);
244+
const startDepth = isInverted
245+
? Math.floor(viewportTop / stackFrameHeight)
246+
: Math.floor(maxStackDepthPlusOne - viewportBottom / stackFrameHeight);
247+
const endDepth = isInverted
248+
? Math.ceil(viewportBottom / stackFrameHeight)
249+
: Math.ceil(maxStackDepthPlusOne - viewportTop / stackFrameHeight);
243250

244251
// Only draw the stack frames that are vertically within view.
245252
// The graph is drawn from bottom to top, in order of increasing depth.
@@ -251,10 +258,12 @@ class FlameGraphCanvasImpl extends React.PureComponent<Props> {
251258
continue;
252259
}
253260

254-
const cssRowTop: CssPixels =
255-
(maxStackDepthPlusOne - depth - 1) * ROW_HEIGHT - viewportTop;
256-
const cssRowBottom: CssPixels =
257-
(maxStackDepthPlusOne - depth) * ROW_HEIGHT - viewportTop;
261+
const cssRowTop: CssPixels = isInverted
262+
? depth * ROW_HEIGHT - viewportTop
263+
: (maxStackDepthPlusOne - depth - 1) * ROW_HEIGHT - viewportTop;
264+
const cssRowBottom: CssPixels = isInverted
265+
? (depth + 1) * ROW_HEIGHT - viewportTop
266+
: (maxStackDepthPlusOne - depth) * ROW_HEIGHT - viewportTop;
258267
const deviceRowTop: DevicePixels = snap(cssRowTop * cssToDeviceScale);
259268
const deviceRowBottom: DevicePixels =
260269
snap(cssRowBottom * cssToDeviceScale) - 1;
@@ -300,7 +309,7 @@ class FlameGraphCanvasImpl extends React.PureComponent<Props> {
300309
i === hoveredItem.flameGraphTimingIndex;
301310
const isHighlighted = isSelected || isRightClicked || isHovered;
302311

303-
const categoryIndex = callNodeTable.category[callNodeIndex];
312+
const categoryIndex = callNodeInfo.categoryForNode(callNodeIndex);
304313
const category = categories[categoryIndex];
305314
const colorStyles = mapCategoryColorNameToStackChartStyles(
306315
category.color
@@ -322,7 +331,7 @@ class FlameGraphCanvasImpl extends React.PureComponent<Props> {
322331
deviceBoxLeft + deviceHorizontalPadding;
323332
const deviceTextWidth: DevicePixels = deviceBoxRight - deviceTextLeft;
324333
if (deviceTextWidth > textMeasurement.minWidth) {
325-
const funcIndex = callNodeTable.func[callNodeIndex];
334+
const funcIndex = callNodeInfo.funcForNode(callNodeIndex);
326335
const funcName = thread.stringTable.getString(
327336
thread.funcTable.name[funcIndex]
328337
);
@@ -472,11 +481,12 @@ class FlameGraphCanvasImpl extends React.PureComponent<Props> {
472481
flameGraphTiming,
473482
maxStackDepthPlusOne,
474483
viewport: { viewportTop, containerWidth },
484+
isInverted,
475485
} = this.props;
476486
const pos = x / containerWidth;
477-
const depth = Math.floor(
478-
maxStackDepthPlusOne - (y + viewportTop) / ROW_HEIGHT
479-
);
487+
const depth = isInverted
488+
? Math.floor((y + viewportTop) / ROW_HEIGHT)
489+
: Math.floor(maxStackDepthPlusOne - (y + viewportTop) / ROW_HEIGHT);
480490
const stackTiming = flameGraphTiming[depth];
481491

482492
if (!stackTiming) {

src/components/flame-graph/FlameGraph.tsx

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@
44
import * as React from 'react';
55

66
import { explicitConnectWithForwardRef } from '../../utils/connect';
7-
import { FlameGraphCanvas } from './Canvas';
7+
import {
8+
FlameGraphCanvas,
9+
type OwnProps as FlameGraphCanvasProps,
10+
} from './Canvas';
811

912
import {
1013
getCategories,
@@ -28,7 +31,6 @@ import {
2831
updateBottomBoxContentsAndMaybeOpen,
2932
} from 'firefox-profiler/actions/profile-view';
3033
import { extractNonInvertedCallTreeTimings } from 'firefox-profiler/profile-logic/call-tree';
31-
import { ensureExists } from 'firefox-profiler/utils/types';
3234

3335
import type {
3436
Thread,
@@ -352,10 +354,7 @@ class FlameGraphImpl
352354
// different, and the flame graph is only used with non-inverted timings.)
353355
const tracedTimingNonInverted =
354356
tracedTiming !== null
355-
? ensureExists(
356-
extractNonInvertedCallTreeTimings(tracedTiming),
357-
'The flame graph should only ever see non-inverted timings, see UrlState.getInvertCallstack'
358-
)
357+
? extractNonInvertedCallTreeTimings(tracedTiming)
359358
: null;
360359

361360
const maxViewportHeight = maxStackDepthPlusOne * STACK_FRAME_HEIGHT;
@@ -376,7 +375,7 @@ class FlameGraphImpl
376375
maxViewportHeight,
377376
maximumZoom: 1,
378377
previewSelection,
379-
startsAtBottom: true,
378+
startsAtBottom: !isInverted,
380379
disableHorizontalMovement: true,
381380
viewportNeedsUpdate,
382381
marginLeft: 0,
@@ -416,12 +415,16 @@ class FlameGraphImpl
416415
}
417416
}
418417

419-
function viewportNeedsUpdate() {
420-
// By always returning false we prevent the viewport from being
418+
function viewportNeedsUpdate(
419+
prevProps: FlameGraphCanvasProps,
420+
nextProps: FlameGraphCanvasProps
421+
) {
422+
// By returning false we prevent the viewport from being
421423
// reset and scrolled all the way to the bottom when doing
422424
// operations like changing the time selection or applying a
423425
// transform.
424-
return false;
426+
// We only reset the viewport if the invertCallstack setting changes.
427+
return prevProps.isInverted !== nextProps.isInverted;
425428
}
426429

427430
export const FlameGraph = explicitConnectWithForwardRef<

src/components/flame-graph/MaybeFlameGraph.tsx

Lines changed: 3 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -4,36 +4,23 @@
44
import * as React from 'react';
55

66
import { explicitConnectWithForwardRef } from 'firefox-profiler/utils/connect';
7-
import { getInvertCallstack } from '../../selectors/url-state';
87
import { selectedThreadSelectors } from '../../selectors/per-thread';
9-
import { changeInvertCallstack } from '../../actions/profile-view';
108
import { FlameGraphEmptyReasons } from './FlameGraphEmptyReasons';
119
import { FlameGraph, type FlameGraphHandle } from './FlameGraph';
1210

1311
import type { ConnectedProps } from 'firefox-profiler/utils/connect';
1412

1513
import './MaybeFlameGraph.css';
1614

17-
// TODO: This component isn't needed any more. Whenever the selected tab
18-
// is "flame-graph", `invertCallstack` will be `false`. <MaybeFlameGraph /> is
19-
// only used in the "flame-graph" tab.
20-
2115
type StateProps = {
2216
readonly isPreviewSelectionEmpty: boolean;
23-
readonly invertCallstack: boolean;
24-
};
25-
type DispatchProps = {
26-
readonly changeInvertCallstack: typeof changeInvertCallstack;
2717
};
18+
type DispatchProps = {};
2819
type Props = ConnectedProps<{}, StateProps, DispatchProps>;
2920

3021
class MaybeFlameGraphImpl extends React.PureComponent<Props> {
3122
_flameGraph: React.RefObject<FlameGraphHandle | null> = React.createRef();
3223

33-
_onSwitchToNormalCallstackClick = () => {
34-
this.props.changeInvertCallstack(false);
35-
};
36-
3724
override componentDidMount() {
3825
const flameGraph = this._flameGraph.current;
3926
if (flameGraph) {
@@ -42,28 +29,12 @@ class MaybeFlameGraphImpl extends React.PureComponent<Props> {
4229
}
4330

4431
override render() {
45-
const { isPreviewSelectionEmpty, invertCallstack } = this.props;
32+
const { isPreviewSelectionEmpty } = this.props;
4633

4734
if (isPreviewSelectionEmpty) {
4835
return <FlameGraphEmptyReasons />;
4936
}
5037

51-
if (invertCallstack) {
52-
return (
53-
<div className="flameGraphDisabledMessage">
54-
<h3>The Flame Graph is not available for inverted call stacks</h3>
55-
<p>
56-
<button
57-
type="button"
58-
onClick={this._onSwitchToNormalCallstackClick}
59-
>
60-
Switch to the normal call stack
61-
</button>{' '}
62-
to show the Flame Graph.
63-
</p>
64-
</div>
65-
);
66-
}
6738
return <FlameGraph ref={this._flameGraph} />;
6839
}
6940
}
@@ -76,13 +47,10 @@ export const MaybeFlameGraph = explicitConnectWithForwardRef<
7647
>({
7748
mapStateToProps: (state) => {
7849
return {
79-
invertCallstack: getInvertCallstack(state),
8050
isPreviewSelectionEmpty:
8151
!selectedThreadSelectors.getHasPreviewFilteredCtssSamples(state),
8252
};
8353
},
84-
mapDispatchToProps: {
85-
changeInvertCallstack,
86-
},
54+
mapDispatchToProps: {},
8755
component: MaybeFlameGraphImpl,
8856
});

src/components/flame-graph/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ const FlameGraphView = () => (
1313
role="tabpanel"
1414
aria-labelledby="flame-graph-tab-button"
1515
>
16-
<StackSettings hideInvertCallstack={true} />
16+
<StackSettings />
1717
<TransformNavigator />
1818
<MaybeFlameGraph />
1919
</div>

src/profile-logic/flame-graph.ts

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import type {
88
IndexIntoCallNodeTable,
99
} from 'firefox-profiler/types';
1010
import type { StringTable } from 'firefox-profiler/utils/string-table';
11-
import type { CallTreeTimingsNonInverted } from './call-tree';
11+
import type { CallTreeTimingsNonInverted, CallTree } from './call-tree';
1212

1313
import { bisectionRightByStrKey } from 'firefox-profiler/utils/bisect';
1414

@@ -307,3 +307,75 @@ export function getFlameGraphTiming(
307307

308308
return timing;
309309
}
310+
311+
export function computeFlameGraphTimingFromCallTree(
312+
callTree: CallTree
313+
): FlameGraphTiming {
314+
const rootTotalSummary = callTree.getTotal();
315+
if (rootTotalSummary === 0) {
316+
return [];
317+
}
318+
319+
const timing: FlameGraphTiming = [];
320+
321+
function traverse(
322+
nodeIndex: IndexIntoCallNodeTable,
323+
depth: number,
324+
startX: number
325+
): number {
326+
const { self, total } = callTree.getNodeData(nodeIndex);
327+
if (total === 0) {
328+
return startX;
329+
}
330+
331+
const totalRelative = Math.abs(total / rootTotalSummary);
332+
const endX = startX + totalRelative;
333+
334+
if (!timing[depth]) {
335+
timing[depth] = {
336+
start: [],
337+
end: [],
338+
selfRelative: [],
339+
callNode: [],
340+
length: 0,
341+
};
342+
}
343+
344+
timing[depth].start.push(startX);
345+
timing[depth].end.push(endX);
346+
timing[depth].selfRelative.push(Math.abs(self / rootTotalSummary));
347+
timing[depth].callNode.push(nodeIndex);
348+
timing[depth].length++;
349+
350+
const children = [...callTree.getChildren(nodeIndex)];
351+
if (children.length > 0) {
352+
// Sort children alphabetically by function name.
353+
children.sort((a, b) => {
354+
const nameA = callTree.getNodeData(a).funcName;
355+
const nameB = callTree.getNodeData(b).funcName;
356+
return nameA.localeCompare(nameB);
357+
});
358+
359+
let currentChildStart = startX;
360+
for (const child of children) {
361+
currentChildStart = traverse(child, depth + 1, currentChildStart);
362+
}
363+
}
364+
365+
return endX;
366+
}
367+
368+
const roots = [...callTree.getRoots()];
369+
roots.sort((a, b) => {
370+
const nameA = callTree.getNodeData(a).funcName;
371+
const nameB = callTree.getNodeData(b).funcName;
372+
return nameA.localeCompare(nameB);
373+
});
374+
375+
let currentStart = 0;
376+
for (const root of roots) {
377+
currentStart = traverse(root, 0, currentStart);
378+
}
379+
380+
return timing;
381+
}

src/profile-logic/profile-data.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2686,7 +2686,6 @@ export function computeCallNodeMaxDepthPlusOne(
26862686
// computed for the filtered thread, but a samples-like table can use the preview
26872687
// filtered thread, which involves a subset of the total call nodes.
26882688
let maxDepth = -1;
2689-
const callNodeTable = callNodeInfo.getCallNodeTable();
26902689
// TODO: Use sampleCallNodes instead
26912690
const stackIndexToCallNodeIndex =
26922691
callNodeInfo.getStackIndexToNonInvertedCallNodeIndex();
@@ -2696,7 +2695,7 @@ export function computeCallNodeMaxDepthPlusOne(
26962695
continue;
26972696
}
26982697
const callNodeIndex = stackIndexToCallNodeIndex[stackIndex];
2699-
const depth = callNodeTable.depth[callNodeIndex];
2698+
const depth = callNodeInfo.depthForNode(callNodeIndex);
27002699
if (depth > maxDepth) {
27012700
maxDepth = depth;
27022701
}

0 commit comments

Comments
 (0)