From 422bae0bedcdc37b6d823d560c423a0c3dde1475 Mon Sep 17 00:00:00 2001 From: "loongtao.zhang" Date: Fri, 5 Jun 2026 10:31:03 +0800 Subject: [PATCH 1/4] feat: invert call stack in flame graph to render as icicle graph --- .../flame-graph/ConnectedFlameGraph.tsx | 18 +- src/components/flame-graph/FlameGraph.tsx | 26 +- src/components/flame-graph/index.tsx | 2 +- src/profile-logic/flame-graph.ts | 267 +++++++++++++++++- src/profile-logic/profile-data.ts | 3 +- src/selectors/per-thread/stack-sample.ts | 112 +++++++- src/selectors/url-state.ts | 3 +- src/test/components/FlameGraph.test.tsx | 16 +- .../__snapshots__/FlameGraph.test.tsx.snap | 13 + src/test/store/actions.test.ts | 26 +- src/test/store/profile-view.test.ts | 18 +- 11 files changed, 453 insertions(+), 51 deletions(-) diff --git a/src/components/flame-graph/ConnectedFlameGraph.tsx b/src/components/flame-graph/ConnectedFlameGraph.tsx index d2dcd36f63..21d060d600 100644 --- a/src/components/flame-graph/ConnectedFlameGraph.tsx +++ b/src/components/flame-graph/ConnectedFlameGraph.tsx @@ -16,7 +16,10 @@ import { getProfileUsesMultipleStackTypes, } from 'firefox-profiler/selectors/profile'; import { selectedThreadSelectors } from 'firefox-profiler/selectors/per-thread'; -import { getSelectedThreadsKey } from '../../selectors/url-state'; +import { + getInvertCallstack, + getSelectedThreadsKey, +} from '../../selectors/url-state'; import { changeSelectedCallNode, changeRightClickedCallNode, @@ -56,7 +59,6 @@ type StateProps = { readonly thread: Thread; readonly weightType: WeightType; readonly innerWindowIDToPageMap: Map | null; - readonly maxStackDepthPlusOne: number; readonly timeRange: StartEndRange; readonly previewSelection: PreviewSelection | null; readonly flameGraphTiming: FlameGraphTiming; @@ -68,6 +70,7 @@ type StateProps = { readonly scrollToSelectionGeneration: number; readonly categories: CategoryList; readonly interval: Milliseconds; + readonly startsAtBottom: boolean; readonly callTreeSummaryStrategy: CallTreeSummaryStrategy; readonly ctssSamples: SamplesLikeTable; readonly ctssSampleCategoriesAndSubcategories: SampleCategoriesAndSubcategories; @@ -139,7 +142,6 @@ class ConnectedFlameGraphImpl const { thread, threadsKey, - maxStackDepthPlusOne, flameGraphTiming, callTree, callNodeInfo, @@ -151,6 +153,7 @@ class ConnectedFlameGraphImpl callTreeSummaryStrategy, categories, interval, + startsAtBottom, innerWindowIDToPageMap, weightType, ctssSamples, @@ -165,7 +168,7 @@ class ConnectedFlameGraphImpl thread={thread} weightType={weightType} innerWindowIDToPageMap={innerWindowIDToPageMap} - maxStackDepthPlusOne={maxStackDepthPlusOne} + maxStackDepthPlusOne={flameGraphTiming.rowCount} timeRange={timeRange} previewSelection={previewSelection} flameGraphTiming={flameGraphTiming} @@ -177,7 +180,7 @@ class ConnectedFlameGraphImpl scrollToSelectionGeneration={scrollToSelectionGeneration} categories={categories} interval={interval} - startsAtBottom={true} + startsAtBottom={startsAtBottom} callTreeSummaryStrategy={callTreeSummaryStrategy} ctssSamples={ctssSamples} ctssSampleCategoriesAndSubcategories={ @@ -203,10 +206,6 @@ export const ConnectedFlameGraph = explicitConnectWithForwardRef< mapStateToProps: (state) => ({ thread: selectedThreadSelectors.getFilteredThread(state), weightType: selectedThreadSelectors.getWeightTypeForCallTree(state), - // Use the filtered call node max depth, rather than the preview filtered one, so - // that the viewport height is stable across preview selections. - maxStackDepthPlusOne: - selectedThreadSelectors.getFilteredCallNodeMaxDepthPlusOne(state), flameGraphTiming: selectedThreadSelectors.getFlameGraphTiming(state), callTree: selectedThreadSelectors.getCallTree(state), timeRange: getCommittedRange(state), @@ -220,6 +219,7 @@ export const ConnectedFlameGraph = explicitConnectWithForwardRef< selectedThreadSelectors.getRightClickedCallNodeIndex(state), scrollToSelectionGeneration: getScrollToSelectionGeneration(state), interval: getProfileInterval(state), + startsAtBottom: !getInvertCallstack(state), callTreeSummaryStrategy: selectedThreadSelectors.getCallTreeSummaryStrategy(state), innerWindowIDToPageMap: getInnerWindowIDToPageMap(state), diff --git a/src/components/flame-graph/FlameGraph.tsx b/src/components/flame-graph/FlameGraph.tsx index dd6d888341..df420be19b 100644 --- a/src/components/flame-graph/FlameGraph.tsx +++ b/src/components/flame-graph/FlameGraph.tsx @@ -3,10 +3,12 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import * as React from 'react'; -import { FlameGraphCanvas } from './Canvas'; +import { + FlameGraphCanvas, + type OwnProps as FlameGraphCanvasProps, +} from './Canvas'; import { ContextMenuTrigger } from 'firefox-profiler/components/shared/ContextMenuTrigger'; import { extractNonInvertedCallTreeTimings } from 'firefox-profiler/profile-logic/call-tree'; -import { ensureExists } from 'firefox-profiler/utils/types'; import type { Thread, @@ -288,17 +290,9 @@ export class FlameGraph onCallNodeEnterOrDoubleClick, } = this.props; - // Get the CallTreeTimingsNonInverted out of tracedTiming. We pass this - // along rather than the more generic CallTreeTimings type so that the - // FlameGraphCanvas component can operate on the more specialized type. - // (CallTreeTimingsNonInverted and CallTreeTimingsInverted are very - // different, and the flame graph is only used with non-inverted timings.) const tracedTimingNonInverted = tracedTiming !== null - ? ensureExists( - extractNonInvertedCallTreeTimings(tracedTiming), - 'The flame graph should only ever see non-inverted timings, see UrlState.getInvertCallstack' - ) + ? extractNonInvertedCallTreeTimings(tracedTiming) : null; const maxViewportHeight = maxStackDepthPlusOne * STACK_FRAME_HEIGHT; @@ -359,10 +353,14 @@ export class FlameGraph } } -function viewportNeedsUpdate() { - // By always returning false we prevent the viewport from being +function viewportNeedsUpdate( + prevProps: FlameGraphCanvasProps, + nextProps: FlameGraphCanvasProps +) { + // By returning false we prevent the viewport from being // reset and scrolled all the way to the bottom when doing // operations like changing the time selection or applying a // transform. - return false; + // We only reset the viewport if the layout direction changes. + return prevProps.startsAtBottom !== nextProps.startsAtBottom; } diff --git a/src/components/flame-graph/index.tsx b/src/components/flame-graph/index.tsx index 70af6f60c0..9dd908e996 100644 --- a/src/components/flame-graph/index.tsx +++ b/src/components/flame-graph/index.tsx @@ -40,7 +40,7 @@ class FlameGraphViewImpl extends React.PureComponent { role="tabpanel" aria-labelledby="flame-graph-tab-button" > - + {isPreviewSelectionEmpty ? ( diff --git a/src/profile-logic/flame-graph.ts b/src/profile-logic/flame-graph.ts index 14f118e843..24f8347c5a 100644 --- a/src/profile-logic/flame-graph.ts +++ b/src/profile-logic/flame-graph.ts @@ -8,7 +8,11 @@ import type { IndexIntoCallNodeTable, } from 'firefox-profiler/types'; import type { StringTable } from 'firefox-profiler/utils/string-table'; -import type { CallTreeTimingsNonInverted } from './call-tree'; +import type { + CallTreeTimingsInverted, + CallTreeTimingsNonInverted, +} from './call-tree'; +import type { CallNodeInfoInverted } from './call-node-info'; import { bisectionRightByStrKey } from 'firefox-profiler/utils/bisect'; @@ -44,7 +48,13 @@ export type FlameGraphTimingRow = { * the implementation to generate rows lazily as the user scrolls towards * deeper calls. */ -export class FlameGraphTiming { +export interface FlameGraphTiming { + readonly rowCount: number; + getRow(depth: number): FlameGraphTimingRow; + getAllRowsForTesting(): FlameGraphTimingRow[]; +} + +class FlameGraphTimingNonInverted implements FlameGraphTiming { _flameGraphRows: FlameGraphRows; _callNodeTable: CallNodeTable; _callTreeTimings: CallTreeTimingsNonInverted; @@ -348,5 +358,256 @@ export function getFlameGraphTiming( callNodeTable: CallNodeTable, callTreeTimings: CallTreeTimingsNonInverted ): FlameGraphTiming { - return new FlameGraphTiming(flameGraphRows, callNodeTable, callTreeTimings); + return new FlameGraphTimingNonInverted( + flameGraphRows, + callNodeTable, + callTreeTimings + ); +} + +type InvertedNodeTiming = { + total: number; + hasChildren: boolean; +}; + +type InvertedFlameGraphRowItem = { + nodeIndex: IndexIntoCallNodeTable; + start: UnitIntervalOfProfileRange; + end: UnitIntervalOfProfileRange; +}; + +// The flame graph does not support horizontal zooming, so boxes that are smaller +// than a fraction of a 16k-wide display cannot be hovered or read. We still take +// their width into account when positioning later boxes, but we do not expand +// their descendants. +const MIN_INVERTED_FLAME_GRAPH_BOX_WIDTH = 1 / 16384; + +class FlameGraphTimingInverted implements FlameGraphTiming { + _callNodeInfo: CallNodeInfoInverted; + _callTreeTimings: CallTreeTimingsInverted; + _funcTable: FuncTable; + _stringTable: StringTable; + _nonRootTimingCache: Map = + new Map(); + _timingRows: FlameGraphTimingRow[] = []; + _nextRowItems: InvertedFlameGraphRowItem[]; + _rowCount: number; + + constructor( + callNodeInfo: CallNodeInfoInverted, + callTreeTimings: CallTreeTimingsInverted, + funcTable: FuncTable, + stringTable: StringTable + ) { + this._callNodeInfo = callNodeInfo; + this._callTreeTimings = callTreeTimings; + this._funcTable = funcTable; + this._stringTable = stringTable; + + const { rootTotalSummary } = callTreeTimings; + this._rowCount = + rootTotalSummary === 0 ? 0 : callNodeInfo.getCallNodeTable().maxDepth + 1; + this._nextRowItems = this._getRootRowItems(); + } + + get rowCount(): number { + return this._rowCount; + } + + getRow(depth: number): FlameGraphTimingRow { + if (depth < 0 || depth >= this.rowCount) { + throw new Error( + `Out-of-bounds call to getRow: ${depth} is outside 0..${this.rowCount}` + ); + } + while (this._timingRows.length <= depth) { + this._buildNextTimingRow(); + } + return this._timingRows[depth]; + } + + // Convenience method for tests, don't call in production + getAllRowsForTesting(): FlameGraphTimingRow[] { + const rows = []; + for (let depth = 0; depth < this.rowCount; depth++) { + rows.push(this.getRow(depth)); + } + return rows; + } + + _compareCallNodesByFuncName = ( + a: IndexIntoCallNodeTable, + b: IndexIntoCallNodeTable + ): number => { + const callNodeInfo = this._callNodeInfo; + const { name } = this._funcTable; + const stringTable = this._stringTable; + const funcA = callNodeInfo.funcForNode(a); + const funcB = callNodeInfo.funcForNode(b); + const funcNameA = stringTable.getString(name[funcA]); + const funcNameB = stringTable.getString(name[funcB]); + if (funcNameA < funcNameB) { + return -1; + } + if (funcNameA > funcNameB) { + return 1; + } + return funcA - funcB; + }; + + _getNodeTiming(nodeIndex: IndexIntoCallNodeTable): InvertedNodeTiming { + const callNodeInfo = this._callNodeInfo; + const { callNodeSelf, totalPerRootFunc, hasChildrenPerRootFunc } = + this._callTreeTimings; + + if (callNodeInfo.isRoot(nodeIndex)) { + return { + total: totalPerRootFunc[nodeIndex], + hasChildren: hasChildrenPerRootFunc[nodeIndex] !== 0, + }; + } + + const cached = this._nonRootTimingCache.get(nodeIndex); + if (cached !== undefined) { + return cached; + } + + const nodeDepth = callNodeInfo.depthForNode(nodeIndex); + const [rangeStart, rangeEnd] = + callNodeInfo.getSuffixOrderIndexRangeForCallNode(nodeIndex); + const suffixOrderedCallNodes = callNodeInfo.getSuffixOrderedCallNodes(); + const callNodeTableDepthCol = callNodeInfo.getCallNodeTable().depth; + + let total = 0; + let hasChildren = false; + for (let i = rangeStart; i < rangeEnd; i++) { + const selfNode = suffixOrderedCallNodes[i]; + const self = callNodeSelf[selfNode]; + total += self; + hasChildren = + hasChildren || + (self !== 0 && callNodeTableDepthCol[selfNode] > nodeDepth); + } + + const timing = { total, hasChildren }; + this._nonRootTimingCache.set(nodeIndex, timing); + return timing; + } + + _getChildrenWithTiming( + nodeIndex: IndexIntoCallNodeTable + ): IndexIntoCallNodeTable[] { + const callNodeInfo = this._callNodeInfo; + const children = callNodeInfo.getChildren(nodeIndex); + const displayedChildren = []; + for (let i = 0; i < children.length; i++) { + const child = children[i]; + const { total, hasChildren } = this._getNodeTiming(child); + if (total !== 0 || hasChildren) { + displayedChildren.push(child); + } + } + displayedChildren.sort(this._compareCallNodesByFuncName); + return displayedChildren; + } + + _getRootRowItems(): InvertedFlameGraphRowItem[] { + const { rootTotalSummary, sortedRoots, totalPerRootFunc } = + this._callTreeTimings; + const abs = Math.abs; + + if (rootTotalSummary === 0) { + return []; + } + + const roots = []; + for (let i = 0; i < sortedRoots.length; i++) { + roots.push(sortedRoots[i]); + } + roots.sort(this._compareCallNodesByFuncName); + + const rootRowItems: InvertedFlameGraphRowItem[] = []; + let currentRootStart = 0; + for (let i = 0; i < roots.length; i++) { + const root = roots[i]; + const totalRelative = abs(totalPerRootFunc[root] / rootTotalSummary); + const start = currentRootStart; + const end = start + totalRelative; + if (totalRelative >= MIN_INVERTED_FLAME_GRAPH_BOX_WIDTH) { + rootRowItems.push({ nodeIndex: root, start, end }); + } + currentRootStart = end; + } + + return rootRowItems; + } + + _buildNextTimingRow(): void { + const { rootTotalSummary } = this._callTreeTimings; + const callNodeInfo = this._callNodeInfo; + const abs = Math.abs; + + const rowItems = this._nextRowItems; + const start: UnitIntervalOfProfileRange[] = []; + const end: UnitIntervalOfProfileRange[] = []; + const selfRelative: number[] = []; + const timingCallNodes: IndexIntoCallNodeTable[] = []; + const nextRowItems: InvertedFlameGraphRowItem[] = []; + + for (let i = 0; i < rowItems.length; i++) { + const { nodeIndex, start: itemStart, end: itemEnd } = rowItems[i]; + const { total } = this._getNodeTiming(nodeIndex); + + start.push(itemStart); + end.push(itemEnd); + selfRelative.push( + callNodeInfo.isRoot(nodeIndex) ? abs(total / rootTotalSummary) : 0 + ); + timingCallNodes.push(nodeIndex); + + if (itemEnd - itemStart < MIN_INVERTED_FLAME_GRAPH_BOX_WIDTH) { + continue; + } + + const children = this._getChildrenWithTiming(nodeIndex); + let currentChildStart = itemStart; + for (let childIndex = 0; childIndex < children.length; childIndex++) { + const child = children[childIndex]; + const childTotal = this._getNodeTiming(child).total; + const childTotalRelative = abs(childTotal / rootTotalSummary); + const childEnd = currentChildStart + childTotalRelative; + if (childTotalRelative >= MIN_INVERTED_FLAME_GRAPH_BOX_WIDTH) { + nextRowItems.push({ + nodeIndex: child, + start: currentChildStart, + end: childEnd, + }); + } + currentChildStart = childEnd; + } + } + + this._nextRowItems = nextRowItems; + this._timingRows.push({ + start, + end, + selfRelative, + callNode: timingCallNodes, + length: timingCallNodes.length, + }); + } +} + +export function getInvertedFlameGraphTiming( + callNodeInfo: CallNodeInfoInverted, + callTreeTimings: CallTreeTimingsInverted, + funcTable: FuncTable, + stringTable: StringTable +): FlameGraphTiming { + return new FlameGraphTimingInverted( + callNodeInfo, + callTreeTimings, + funcTable, + stringTable + ); } diff --git a/src/profile-logic/profile-data.ts b/src/profile-logic/profile-data.ts index 865efbd41f..d55997a537 100644 --- a/src/profile-logic/profile-data.ts +++ b/src/profile-logic/profile-data.ts @@ -2690,7 +2690,6 @@ export function computeCallNodeMaxDepthPlusOne( // computed for the filtered thread, but a samples-like table can use the preview // filtered thread, which involves a subset of the total call nodes. let maxDepth = -1; - const callNodeTable = callNodeInfo.getCallNodeTable(); // TODO: Use sampleCallNodes instead const stackIndexToCallNodeIndex = callNodeInfo.getStackIndexToNonInvertedCallNodeIndex(); @@ -2700,7 +2699,7 @@ export function computeCallNodeMaxDepthPlusOne( continue; } const callNodeIndex = stackIndexToCallNodeIndex[stackIndex]; - const depth = callNodeTable.depth[callNodeIndex]; + const depth = callNodeInfo.depthForNode(callNodeIndex); if (depth > maxDepth) { maxDepth = depth; } diff --git a/src/selectors/per-thread/stack-sample.ts b/src/selectors/per-thread/stack-sample.ts index 21bbecb964..97c5e38283 100644 --- a/src/selectors/per-thread/stack-sample.ts +++ b/src/selectors/per-thread/stack-sample.ts @@ -141,6 +141,9 @@ export function getStackAndSampleSelectorsPerThread( return _getNonInvertedCallNodeInfo(state); }; + const getNonInvertedCallNodeInfo: Selector = + _getNonInvertedCallNodeInfo; + const _getCallNodeTable: Selector = (state) => _getNonInvertedCallNodeInfo(state).getCallNodeTable(); @@ -309,6 +312,13 @@ export function getStackAndSampleSelectorsPerThread( ProfileData.computeCallNodeMaxDepthPlusOne ); + const getNonInvertedFilteredCallNodeMaxDepthPlusOne: Selector = + createSelector( + threadSelectors.getFilteredCtssSamples, + _getNonInvertedCallNodeInfo, + ProfileData.computeCallNodeMaxDepthPlusOne + ); + /** * When computing the call tree, a "samples" table is used, which * can represent a variety of formats with different weight types. @@ -321,6 +331,32 @@ export function getStackAndSampleSelectorsPerThread( (samples) => samples.weightType || 'samples' ); + const _getSampleIndexToNonInvertedCallNodeIndexForPreviewFilteredCtssThreadNonInverted: Selector< + Array + > = createSelector( + (state: State) => + threadSelectors.getPreviewFilteredCtssSamples(state).stack, + (state: State) => + _getNonInvertedCallNodeInfo( + state + ).getStackIndexToNonInvertedCallNodeIndex(), + ProfileData.getSampleIndexToCallNodeIndex + ); + + const getCallNodeSelfAndSummaryNonInverted: Selector = + createSelector( + threadSelectors.getPreviewFilteredCtssSamples, + _getSampleIndexToNonInvertedCallNodeIndexForPreviewFilteredCtssThreadNonInverted, + _getNonInvertedCallNodeInfo, + (samples, sampleIndexToCallNodeIndex, callNodeInfo) => { + return CallTree.computeCallNodeSelfAndSummary( + samples, + sampleIndexToCallNodeIndex, + callNodeInfo.getCallNodeTable().length + ); + } + ); + const getCallNodeSelfAndSummary: Selector = createSelector( threadSelectors.getPreviewFilteredCtssSamples, @@ -343,8 +379,8 @@ export function getStackAndSampleSelectorsPerThread( const getCallTreeTimingsNonInverted: Selector = createSelector( - getCallNodeInfo, - getCallNodeSelfAndSummary, + _getNonInvertedCallNodeInfo, + getCallNodeSelfAndSummaryNonInverted, CallTree.computeCallTreeTimingsNonInverted ); @@ -369,6 +405,23 @@ export function getStackAndSampleSelectorsPerThread( CallTree.getCallTree ); + const getNonInvertedCallTreeTimingsWrapper: Selector = + createSelector(getCallTreeTimingsNonInverted, (timings) => ({ + type: 'NON_INVERTED' as const, + timings, + })); + + const getNonInvertedCallTree: Selector = createSelector( + threadSelectors.getFilteredThread, + _getNonInvertedCallNodeInfo, + ProfileSelectors.getCategories, + threadSelectors.getPreviewFilteredCtssSamples, + getNonInvertedCallTreeTimingsWrapper, + getWeightTypeForCallTree, + ProfileSelectors.getSourceTable, + CallTree.getCallTree + ); + const getFunctionListTree: Selector = createSelector( threadSelectors.getFilteredThread, _getInvertedCallNodeInfo, @@ -431,6 +484,30 @@ export function getStackAndSampleSelectorsPerThread( } ); + const getTracedTimingNonInverted: Selector = + createSelector( + threadSelectors.getPreviewFilteredCtssSamples, + _getSampleIndexToNonInvertedCallNodeIndexForPreviewFilteredCtssThreadNonInverted, + _getNonInvertedCallNodeInfo, + ProfileSelectors.getProfileInterval, + (samples, sampleIndexToCallNodeIndex, callNodeInfo, interval) => { + const CallNodeSelfAndSummary = + CallTree.computeCallNodeTracedSelfAndSummary( + samples, + sampleIndexToCallNodeIndex, + callNodeInfo.getCallNodeTable().length, + interval + ); + if (CallNodeSelfAndSummary === null) { + return null; + } + return CallTree.computeCallTreeTimings( + callNodeInfo, + CallNodeSelfAndSummary + ); + } + ); + const getTracedSelfAndTotalForSelectedCallNode: Selector = createSelector( getSelectedCallNodeIndex, @@ -466,20 +543,41 @@ export function getStackAndSampleSelectorsPerThread( _getStackTimingByDepthWithMap(state).sameWidthsIndexToTimestampMap; const getFlameGraphRows: Selector = createSelector( - (state: State) => getCallNodeInfo(state).getCallNodeTable(), + (state: State) => _getNonInvertedCallNodeInfo(state).getCallNodeTable(), (state: State) => threadSelectors.getFilteredThread(state).funcTable, (state: State) => threadSelectors.getFilteredThread(state).stringTable, FlameGraph.computeFlameGraphRows ); - const getFlameGraphTiming: Selector = + const getFlameGraphTimingNonInverted: Selector = createSelector( getFlameGraphRows, - (state: State) => getCallNodeInfo(state).getCallNodeTable(), + (state: State) => _getNonInvertedCallNodeInfo(state).getCallNodeTable(), getCallTreeTimingsNonInverted, FlameGraph.getFlameGraphTiming ); + const getCallTreeTimingsInverted: Selector = + createSelector( + _getInvertedCallNodeInfo, + getCallNodeSelfAndSummaryNonInverted, + CallTree.computeCallTreeTimingsInverted + ); + + const getFlameGraphTimingInverted: Selector = + createSelector( + _getInvertedCallNodeInfo, + getCallTreeTimingsInverted, + (state: State) => threadSelectors.getFilteredThread(state).funcTable, + (state: State) => threadSelectors.getFilteredThread(state).stringTable, + FlameGraph.getInvertedFlameGraphTiming + ); + + const getFlameGraphTiming: Selector = (state) => + UrlState.getInvertCallstack(state) + ? getFlameGraphTimingInverted(state) + : getFlameGraphTimingNonInverted(state); + const getRightClickedCallNodeIndex: Selector = createSelector( getRightClickedCallNodeInfo, @@ -502,6 +600,7 @@ export function getStackAndSampleSelectorsPerThread( unfilteredSamplesRange, getWeightTypeForCallTree, getCallNodeInfo, + getNonInvertedCallNodeInfo, getSourceViewStackLineInfo, getAssemblyViewNativeSymbolIndex, getAssemblyViewStackAddressInfo, @@ -513,15 +612,18 @@ export function getStackAndSampleSelectorsPerThread( getSampleSelectedStatesInFilteredThread, getTreeOrderComparatorInFilteredThread, getCallTree, + getNonInvertedCallTree, getFunctionListTree, getFunctionListTimings, getSourceViewLineTimings, getAssemblyViewAddressTimings, getTracedTiming, + getTracedTimingNonInverted, getTracedSelfAndTotalForSelectedCallNode, getStackTimingByDepth, getSameWidthsIndexToTimestampMap, getFilteredCallNodeMaxDepthPlusOne, + getNonInvertedFilteredCallNodeMaxDepthPlusOne, getFlameGraphTiming, getRightClickedCallNodeIndex, }; diff --git a/src/selectors/url-state.ts b/src/selectors/url-state.ts index 9e06d122bd..a65e73c9c8 100644 --- a/src/selectors/url-state.ts +++ b/src/selectors/url-state.ts @@ -126,7 +126,8 @@ export const getNetworkSearchString: Selector = (state) => export const getSelectedTab: Selector = (state) => getUrlState(state).selectedTab; export const getInvertCallstack: Selector = (state) => - getSelectedTab(state) === 'calltree' && + (getSelectedTab(state) === 'calltree' || + getSelectedTab(state) === 'flame-graph') && getProfileSpecificState(state).invertCallstack; export const getIncludeIdleSamples: Selector = (state) => getProfileSpecificState(state).includeIdleSamples; diff --git a/src/test/components/FlameGraph.test.tsx b/src/test/components/FlameGraph.test.tsx index 18baab4dd2..08288fcfa0 100644 --- a/src/test/components/FlameGraph.test.tsx +++ b/src/test/components/FlameGraph.test.tsx @@ -80,13 +80,22 @@ describe('FlameGraph', function () { expect(drawCalls.length).toBeGreaterThan(0); }); - it('ignores invertCallstack and always displays non-inverted', () => { - const { getState, dispatch } = setupFlameGraph(); + it('respects invertCallstack and toggles icicle graph mode', () => { + const { getState, dispatch, flushRafCalls } = setupFlameGraph(); expect(getInvertCallstack(getState())).toBe(false); + + const initialDrawCalls = flushDrawLog(); + act(() => { dispatch(changeInvertCallstack(true)); }); - expect(getInvertCallstack(getState())).toBe(false); + expect(getInvertCallstack(getState())).toBe(true); + flushRafCalls(); + + const icicleDrawCalls = flushDrawLog(); + expect(icicleDrawCalls.length).toBeGreaterThan(0); + expect(icicleDrawCalls).not.toEqual(initialDrawCalls); + act(() => { dispatch(changeInvertCallstack(false)); }); @@ -443,5 +452,6 @@ function setupFlameGraph() { getContextMenu, clickMenuItem, findFillTextPosition, + flushRafCalls, }; } diff --git a/src/test/components/__snapshots__/FlameGraph.test.tsx.snap b/src/test/components/__snapshots__/FlameGraph.test.tsx.snap index c77453c811..c09bad1bb9 100644 --- a/src/test/components/__snapshots__/FlameGraph.test.tsx.snap +++ b/src/test/components/__snapshots__/FlameGraph.test.tsx.snap @@ -471,6 +471,19 @@ exports[`FlameGraph matches the snapshot 1`] = `
  • +