diff --git a/src/actions/profile-view.ts b/src/actions/profile-view.ts index af47396293..80c350cfd9 100644 --- a/src/actions/profile-view.ts +++ b/src/actions/profile-view.ts @@ -1696,6 +1696,7 @@ export function changeInvertCallstack( dispatch({ type: 'CHANGE_INVERT_CALLSTACK', invertCallstack, + selectedTab: getSelectedTab(getState()), selectedThreadIndexes: getSelectedThreadIndexes(getState()), newSelectedCallNodePath, }); diff --git a/src/app-logic/url-handling.ts b/src/app-logic/url-handling.ts index 70c6f026f2..85a0b35e88 100644 --- a/src/app-logic/url-handling.ts +++ b/src/app-logic/url-handling.ts @@ -184,6 +184,7 @@ type BaseQuery = { type CallTreeQuery = BaseQuery & { search: string; // "js::RunScript" invertCallstack: null | undefined; + invertFlameGraph: null | undefined; hideIdleSamples: null | undefined; ctSummary: string; }; @@ -201,6 +202,7 @@ type NetworkQuery = BaseQuery & { type StackChartQuery = BaseQuery & { search: string; // "js::RunScript" invertCallstack: null | undefined; + invertFlameGraph: null | undefined; hideIdleSamples: null | undefined; showUserTimings: null | undefined; sameWidths: null | undefined; @@ -216,6 +218,7 @@ type Query = BaseQuery & { // CallTree/StackChart specific search?: string; invertCallstack?: null | undefined; + invertFlameGraph?: null | undefined; hideIdleSamples?: null | undefined; ctSummary?: string; transforms?: string; @@ -342,7 +345,10 @@ export function getQueryStringFromUrlState(urlState: UrlState): string { query = baseQuery as CallTreeQueryShape; query.search = urlState.profileSpecific.callTreeSearchString || undefined; - query.invertCallstack = urlState.profileSpecific.invertCallstack + query.invertCallstack = urlState.profileSpecific.invertCallTree + ? null + : undefined; + query.invertFlameGraph = urlState.profileSpecific.invertFlameGraph ? null : undefined; // The URL param is inverted (`hideIdleSamples`) so the default-on state @@ -605,7 +611,8 @@ export function stateFromLocation( lastSelectedCallTreeSummaryStrategy: toValidCallTreeSummaryStrategy( query.ctSummary || undefined ), - invertCallstack: query.invertCallstack === undefined ? false : true, + invertCallTree: query.invertCallstack === undefined ? false : true, + invertFlameGraph: query.invertFlameGraph === undefined ? false : true, includeIdleSamples: query.hideIdleSamples === undefined, showUserTimings: query.showUserTimings === undefined ? false : true, stackChartSameWidths: query.sameWidths === undefined ? false : true, diff --git a/src/components/flame-graph/Canvas.tsx b/src/components/flame-graph/Canvas.tsx index 4fd54e7042..655f51ede9 100644 --- a/src/components/flame-graph/Canvas.tsx +++ b/src/components/flame-graph/Canvas.tsx @@ -18,6 +18,7 @@ import { } from 'firefox-profiler/utils/format-numbers'; import { TooltipCallNode } from 'firefox-profiler/components/tooltip/CallNode'; import { getTimingsForCallNodeIndex } from 'firefox-profiler/profile-logic/profile-data'; +import { getSelfAndTotalForCallNode } from 'firefox-profiler/profile-logic/call-tree'; import MixedTupleMap from 'mixedtuplemap'; import type { @@ -49,7 +50,7 @@ import type { import type { CallTree, - CallTreeTimingsNonInverted, + CallTreeTimings, } from 'firefox-profiler/profile-logic/call-tree'; export type OwnProps = { @@ -74,7 +75,7 @@ export type OwnProps = { readonly callTreeSummaryStrategy: CallTreeSummaryStrategy; readonly ctssSamples: SamplesLikeTable; readonly ctssSampleCategoriesAndSubcategories: SampleCategoriesAndSubcategories; - readonly tracedTiming: CallTreeTimingsNonInverted | null; + readonly tracedTiming: CallTreeTimings | null; readonly displayStackType: boolean; }; @@ -397,11 +398,12 @@ class FlameGraphCanvasImpl extends React.PureComponent { let percentage = formatPercent(ratio); if (tracedTiming) { - const time = formatCallNodeNumberWithUnit( - 'tracing-ms', - false, - tracedTiming.total[callNodeIndex] + const { total } = getSelfAndTotalForCallNode( + callNodeIndex, + callNodeInfo, + tracedTiming ); + const time = formatCallNodeNumberWithUnit('tracing-ms', false, total); percentage = `${time} (${percentage})`; } 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..6e3e87aac7 100644 --- a/src/components/flame-graph/FlameGraph.tsx +++ b/src/components/flame-graph/FlameGraph.tsx @@ -3,10 +3,11 @@ * 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,19 +289,6 @@ 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' - ) - : null; - const maxViewportHeight = maxStackDepthPlusOne * STACK_FRAME_HEIGHT; return ( @@ -349,7 +337,7 @@ export class FlameGraph startsAtBottom, ctssSamples, ctssSampleCategoriesAndSubcategories, - tracedTiming: tracedTimingNonInverted, + tracedTiming, displayStackType, }} /> @@ -359,10 +347,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/reducers/url-state.ts b/src/reducers/url-state.ts index 97b41cbeae..0c8a92be9a 100644 --- a/src/reducers/url-state.ts +++ b/src/reducers/url-state.ts @@ -316,10 +316,21 @@ const lastSelectedCallTreeSummaryStrategy: Reducer = ( } }; -const invertCallstack: Reducer = (state = false, action) => { +const invertCallTree: Reducer = (state = false, action) => { switch (action.type) { case 'CHANGE_INVERT_CALLSTACK': - return action.invertCallstack; + return action.selectedTab === 'calltree' ? action.invertCallstack : state; + default: + return state; + } +}; + +const invertFlameGraph: Reducer = (state = false, action) => { + switch (action.type) { + case 'CHANGE_INVERT_CALLSTACK': + return action.selectedTab === 'flame-graph' + ? action.invertCallstack + : state; default: return state; } @@ -784,7 +795,8 @@ const profileSpecific = combineReducers({ selectedThreads, implementation, lastSelectedCallTreeSummaryStrategy, - invertCallstack, + invertCallTree, + invertFlameGraph, includeIdleSamples, showUserTimings, stackChartSameWidths, 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..48f75a438c 100644 --- a/src/selectors/url-state.ts +++ b/src/selectors/url-state.ts @@ -125,9 +125,17 @@ export const getNetworkSearchString: Selector = (state) => getProfileSpecificState(state).networkSearchString; export const getSelectedTab: Selector = (state) => getUrlState(state).selectedTab; -export const getInvertCallstack: Selector = (state) => - getSelectedTab(state) === 'calltree' && - getProfileSpecificState(state).invertCallstack; +export const getInvertCallstack: Selector = (state) => { + const tab = getSelectedTab(state); + const profileSpecific = getProfileSpecificState(state); + if (tab === 'calltree') { + return profileSpecific.invertCallTree; + } + if (tab === 'flame-graph') { + return profileSpecific.invertFlameGraph; + } + return false; +}; 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..5d39138f5e 100644 --- a/src/test/components/FlameGraph.test.tsx +++ b/src/test/components/FlameGraph.test.tsx @@ -80,19 +80,49 @@ 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)); }); expect(getInvertCallstack(getState())).toBe(false); }); + it('shows traced timing in icicle graph tooltips', () => { + const { + dispatch, + flushRafCalls, + getTooltip, + moveMouse, + findFillTextPosition, + } = setupFlameGraph(); + flushDrawLog(); + + act(() => { + dispatch(changeInvertCallstack(true)); + }); + flushRafCalls(); + + moveMouse(findFillTextPosition('E')); + expect( + within(ensureExists(getTooltip())).getByText('1.0ms (33%)') + ).toBeInTheDocument(); + }); + it('shows a tooltip when hovering', () => { const { getTooltip, moveMouse, findFillTextPosition } = setupFlameGraph(); expect(getTooltip()).toBe(null); @@ -443,5 +473,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`] = `
  • +