diff --git a/src/actions/profile-view.js b/src/actions/profile-view.js index 6fe354ece7..5b0063a5de 100644 --- a/src/actions/profile-view.js +++ b/src/actions/profile-view.js @@ -65,7 +65,6 @@ import type { Pid, IndexIntoSamplesTable, CallNodePath, - CallNodeInfo, IndexIntoCallNodeTable, IndexIntoResourceTable, TrackIndex, @@ -86,6 +85,7 @@ import { } from '../profile-logic/transforms'; import { changeStoredProfileNameInDb } from 'firefox-profiler/app-logic/uploaded-profiles-db'; import type { TabSlug } from '../app-logic/tabs-handling'; +import type { CallNodeInfo } from '../profile-logic/call-node-info'; import { intersectSets } from 'firefox-profiler/utils/set'; /** @@ -2035,12 +2035,13 @@ export function handleCallNodeTransformShortcut( const threadSelectors = getThreadSelectorsFromThreadsKey(threadsKey); const unfilteredThread = threadSelectors.getThread(getState()); const callNodeInfo = threadSelectors.getCallNodeInfo(getState()); - const callNodeTable = callNodeInfo.getCallNodeTable(); const implementation = getImplementationFilter(getState()); const inverted = getInvertCallstack(getState()); const callNodePath = callNodeInfo.getCallNodePathFromIndex(callNodeIndex); - const funcIndex = callNodeTable.func[callNodeIndex]; - const category = callNodeTable.category[callNodeIndex]; + const funcIndex = callNodeInfo.funcForNode(callNodeIndex); + const category = callNodeInfo.categoryForNode(callNodeIndex); + + const nonInvertedCallNodeTable = callNodeInfo.getNonInvertedCallNodeTable(); switch (event.key) { case 'F': @@ -2099,7 +2100,7 @@ export function handleCallNodeTransformShortcut( break; } case 'r': { - if (funcHasRecursiveCall(callNodeTable, funcIndex)) { + if (funcHasRecursiveCall(nonInvertedCallNodeTable, funcIndex)) { dispatch( addTransformToStack(threadsKey, { type: 'collapse-recursion', @@ -2110,7 +2111,7 @@ export function handleCallNodeTransformShortcut( break; } case 'R': { - if (funcHasDirectRecursiveCall(callNodeTable, funcIndex)) { + if (funcHasDirectRecursiveCall(nonInvertedCallNodeTable, funcIndex)) { dispatch( addTransformToStack(threadsKey, { type: 'collapse-direct-recursion', diff --git a/src/components/calltree/CallTree.js b/src/components/calltree/CallTree.js index 4ad5c7110d..65bbf352b4 100644 --- a/src/components/calltree/CallTree.js +++ b/src/components/calltree/CallTree.js @@ -38,7 +38,6 @@ import type { State, ImplementationFilter, ThreadsKey, - CallNodeInfo, CategoryList, IndexIntoCallNodeTable, CallNodeDisplayData, @@ -47,6 +46,7 @@ import type { SelectionContext, } from 'firefox-profiler/types'; import type { CallTree as CallTreeType } from 'firefox-profiler/profile-logic/call-tree'; +import type { CallNodeInfo } from 'firefox-profiler/profile-logic/call-node-info'; import type { Column, @@ -320,7 +320,6 @@ class CallTreeImpl extends PureComponent { // This tree is empty. return; } - const callNodeTable = callNodeInfo.getCallNodeTable(); newExpandedCallNodeIndexes.push(currentCallNodeIndex); for (let i = 0; i < maxInterestingDepth; i++) { const children = tree.getChildren(currentCallNodeIndex); @@ -330,7 +329,8 @@ class CallTreeImpl extends PureComponent { // Let's find if there's a non idle children. const firstNonIdleNode = children.find( - (nodeIndex) => callNodeTable.category[nodeIndex] !== idleCategoryIndex + (nodeIndex) => + callNodeInfo.categoryForNode(nodeIndex) !== idleCategoryIndex ); // If there's a non idle children, use it; otherwise use the first @@ -341,7 +341,7 @@ class CallTreeImpl extends PureComponent { } this._onExpandedCallNodesChange(newExpandedCallNodeIndexes); - const categoryIndex = callNodeTable.category[currentCallNodeIndex]; + const categoryIndex = callNodeInfo.categoryForNode(currentCallNodeIndex); if (categoryIndex !== idleCategoryIndex) { // If we selected the call node with a "idle" category, we'd have a // completely dimmed activity graph because idle stacks are not drawn in diff --git a/src/components/flame-graph/Canvas.js b/src/components/flame-graph/Canvas.js index f6cc1e23c7..36367ae94e 100644 --- a/src/components/flame-graph/Canvas.js +++ b/src/components/flame-graph/Canvas.js @@ -28,7 +28,6 @@ import type { CssPixels, DevicePixels, Milliseconds, - CallNodeInfo, IndexIntoCallNodeTable, CallTreeSummaryStrategy, WeightType, @@ -42,6 +41,7 @@ import type { FlameGraphDepth, IndexIntoFlameGraphTiming, } from 'firefox-profiler/profile-logic/flame-graph'; +import type { CallNodeInfo } from 'firefox-profiler/profile-logic/call-node-info'; import type { ChartCanvasScale, @@ -50,7 +50,7 @@ import type { import type { CallTree, - CallTreeTimings, + CallTreeTimingsNonInverted, } from 'firefox-profiler/profile-logic/call-tree'; export type OwnProps = {| @@ -77,7 +77,7 @@ export type OwnProps = {| +callTreeSummaryStrategy: CallTreeSummaryStrategy, +ctssSamples: SamplesLikeTable, +unfilteredCtssSamples: SamplesLikeTable, - +tracedTiming: CallTreeTimings | null, + +tracedTiming: CallTreeTimingsNonInverted | null, +displayImplementation: boolean, +displayStackType: boolean, |}; diff --git a/src/components/flame-graph/FlameGraph.js b/src/components/flame-graph/FlameGraph.js index 9e025c8c09..4887262022 100644 --- a/src/components/flame-graph/FlameGraph.js +++ b/src/components/flame-graph/FlameGraph.js @@ -30,6 +30,8 @@ import { handleCallNodeTransformShortcut, updateBottomBoxContentsAndMaybeOpen, } from 'firefox-profiler/actions/profile-view'; +import { extractNonInvertedCallTreeTimings } from 'firefox-profiler/profile-logic/call-tree'; +import { ensureExists } from 'firefox-profiler/utils/flow'; import type { Thread, @@ -40,7 +42,6 @@ import type { SamplesLikeTable, PreviewSelection, CallTreeSummaryStrategy, - CallNodeInfo, IndexIntoCallNodeTable, ThreadsKey, InnerWindowID, @@ -48,6 +49,7 @@ import type { } from 'firefox-profiler/types'; import type { FlameGraphTiming } from 'firefox-profiler/profile-logic/flame-graph'; +import type { CallNodeInfo } from 'firefox-profiler/profile-logic/call-node-info'; import type { CallTree, @@ -344,6 +346,19 @@ class FlameGraphImpl extends React.PureComponent { displayStackType, } = 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 ( @@ -394,7 +409,7 @@ class FlameGraphImpl extends React.PureComponent { isInverted, ctssSamples, unfilteredCtssSamples, - tracedTiming, + tracedTiming: tracedTimingNonInverted, displayImplementation, displayStackType, }} diff --git a/src/components/shared/CallNodeContextMenu.js b/src/components/shared/CallNodeContextMenu.js index e7eb5abcd1..01f7ed46f1 100644 --- a/src/components/shared/CallNodeContextMenu.js +++ b/src/components/shared/CallNodeContextMenu.js @@ -53,7 +53,6 @@ import type { TransformType, ImplementationFilter, IndexIntoCallNodeTable, - CallNodeInfo, CallNodePath, Thread, ThreadsKey, @@ -64,6 +63,7 @@ import type { import type { TabSlug } from 'firefox-profiler/app-logic/tabs-handling'; import type { ConnectedProps } from 'firefox-profiler/utils/connect'; +import type { CallNodeInfo } from 'firefox-profiler/profile-logic/call-node-info'; import type { BrowserConnectionStatus } from 'firefox-profiler/app-logic/browser-connection'; type StateProps = {| @@ -147,8 +147,7 @@ class CallNodeContextMenuImpl extends React.PureComponent { callNodeInfo, } = rightClickedCallNodeInfo; - const callNodeTable = callNodeInfo.getCallNodeTable(); - const funcIndex = callNodeTable.func[callNodeIndex]; + const funcIndex = callNodeInfo.funcForNode(callNodeIndex); const isJS = funcTable.isJS[funcIndex]; const stringIndex = funcTable.name[funcIndex]; const functionCall = stringTable.getString(stringIndex); @@ -185,8 +184,7 @@ class CallNodeContextMenuImpl extends React.PureComponent { callNodeInfo, } = rightClickedCallNodeInfo; - const callNodeTable = callNodeInfo.getCallNodeTable(); - const funcIndex = callNodeTable.func[callNodeIndex]; + const funcIndex = callNodeInfo.funcForNode(callNodeIndex); const stringIndex = funcTable.fileName[funcIndex]; if (stringIndex === null) { return null; @@ -209,8 +207,7 @@ class CallNodeContextMenuImpl extends React.PureComponent { callNodeInfo, } = rightClickedCallNodeInfo; - const callNodeTable = callNodeInfo.getCallNodeTable(); - const funcIndex = callNodeTable.func[callNodeIndex]; + const funcIndex = callNodeInfo.funcForNode(callNodeIndex); const line = funcTable.lineNumber[funcIndex]; const column = funcTable.columnNumber[funcIndex]; return { line, column }; @@ -337,8 +334,7 @@ class CallNodeContextMenuImpl extends React.PureComponent { const { threadsKey, callNodePath, thread, callNodeIndex, callNodeInfo } = rightClickedCallNodeInfo; const selectedFunc = callNodePath[callNodePath.length - 1]; - const callNodeTable = callNodeInfo.getCallNodeTable(); - const category = callNodeTable.category[callNodeIndex]; + const category = callNodeInfo.categoryForNode(callNodeIndex); switch (type) { case 'focus-subtree': addTransformToStack(threadsKey, { @@ -451,8 +447,7 @@ class CallNodeContextMenuImpl extends React.PureComponent { const { callNodeInfo, callNodeIndex } = rightClickedCallNodeInfo; const { innerWindowIDToPageMap } = this.props; - const callNodeTable = callNodeInfo.getCallNodeTable(); - const innerWindowID = callNodeTable.innerWindowID[callNodeIndex]; + const innerWindowID = callNodeInfo.innerWindowIDForNode(callNodeIndex); if (innerWindowID && innerWindowIDToPageMap) { const page = innerWindowIDToPageMap.get(innerWindowID); @@ -583,9 +578,8 @@ class CallNodeContextMenuImpl extends React.PureComponent { callNodeInfo, } = rightClickedCallNodeInfo; - const callNodeTable = callNodeInfo.getCallNodeTable(); - const categoryIndex = callNodeTable.category[callNodeIndex]; - const funcIndex = callNodeTable.func[callNodeIndex]; + const categoryIndex = callNodeInfo.categoryForNode(callNodeIndex); + const funcIndex = callNodeInfo.funcForNode(callNodeIndex); const isJS = funcTable.isJS[funcIndex]; const hasCategory = categoryIndex !== -1; // This could be the C++ library, or the JS filename. @@ -599,6 +593,9 @@ class CallNodeContextMenuImpl extends React.PureComponent { const fileName = filePath && parseFileNameFromSymbolication(filePath).path.match(/[^\\/]+$/)?.[0]; + + const callNodeTable = callNodeInfo.getNonInvertedCallNodeTable(); + const showOpenDebuggerItem = isJS && filePath && @@ -607,6 +604,7 @@ class CallNodeContextMenuImpl extends React.PureComponent { filePath !== 'self-hosted' && browserConnectionStatus.status === 'ESTABLISHED' && this._getTabID(); + return ( <> {fileName ? ( diff --git a/src/components/shared/thread/CPUGraph.js b/src/components/shared/thread/CPUGraph.js index e1eda91ea9..8fa969dd37 100644 --- a/src/components/shared/thread/CPUGraph.js +++ b/src/components/shared/thread/CPUGraph.js @@ -13,9 +13,9 @@ import type { CategoryList, IndexIntoSamplesTable, Milliseconds, - CallNodeInfo, SelectedState, } from 'firefox-profiler/types'; +import type { CallNodeInfo } from 'firefox-profiler/profile-logic/call-node-info'; type Props = {| +className: string, diff --git a/src/components/shared/thread/StackGraph.js b/src/components/shared/thread/StackGraph.js index f56efd68f7..64e8c8638c 100644 --- a/src/components/shared/thread/StackGraph.js +++ b/src/components/shared/thread/StackGraph.js @@ -12,10 +12,10 @@ import type { CategoryList, IndexIntoSamplesTable, Milliseconds, - CallNodeInfo, IndexIntoCallNodeTable, SelectedState, } from 'firefox-profiler/types'; +import type { CallNodeInfo } from 'firefox-profiler/profile-logic/call-node-info'; type Props = {| +className: string, diff --git a/src/components/stack-chart/Canvas.js b/src/components/stack-chart/Canvas.js index ce865cc563..cc382b1163 100644 --- a/src/components/stack-chart/Canvas.js +++ b/src/components/stack-chart/Canvas.js @@ -29,7 +29,6 @@ import type { ThreadsKey, UserTimingMarkerPayload, WeightType, - CallNodeInfo, IndexIntoCallNodeTable, CombinedTimingRows, Milliseconds, @@ -41,6 +40,7 @@ import type { InnerWindowID, Page, } from 'firefox-profiler/types'; +import type { CallNodeInfo } from 'firefox-profiler/profile-logic/call-node-info'; import type { ChartCanvasScale, @@ -128,8 +128,7 @@ class StackChartCanvasImpl extends React.PureComponent { return; } - const callNodeTable = callNodeInfo.getCallNodeTable(); - const depth = callNodeTable.depth[selectedCallNodeIndex]; + const depth = callNodeInfo.depthForNode(selectedCallNodeIndex); const y = depth * ROW_CSS_PIXELS_HEIGHT; if (y < this.props.viewport.viewportTop) { @@ -262,7 +261,7 @@ class StackChartCanvasImpl extends React.PureComponent { categoryForUserTiming = 0; } - const callNodeTable = callNodeInfo.getCallNodeTable(); + const callNodeTable = callNodeInfo.getNonInvertedCallNodeTable(); // Only draw the stack frames that are vertically within view. for (let depth = startDepth; depth < endDepth; depth++) { diff --git a/src/components/stack-chart/index.js b/src/components/stack-chart/index.js index 9b9b65cf11..1947fd736b 100644 --- a/src/components/stack-chart/index.js +++ b/src/components/stack-chart/index.js @@ -43,7 +43,6 @@ import { getBottomBoxInfoForCallNode } from '../../profile-logic/profile-data'; import type { Thread, CategoryList, - CallNodeInfo, IndexIntoCallNodeTable, CombinedTimingRows, MarkerIndex, @@ -58,6 +57,7 @@ import type { InnerWindowID, Page, } from 'firefox-profiler/types'; +import type { CallNodeInfo } from 'firefox-profiler/profile-logic/call-node-info'; import type { ConnectedProps } from '../../utils/connect'; @@ -181,8 +181,7 @@ class StackChartImpl extends React.PureComponent { event.preventDefault(); const { callNodeInfo, selectedCallNodeIndex, thread } = this.props; if (selectedCallNodeIndex !== null) { - const callNodeTable = callNodeInfo.getCallNodeTable(); - const funcIndex = callNodeTable.func[selectedCallNodeIndex]; + const funcIndex = callNodeInfo.funcForNode(selectedCallNodeIndex); const funcName = thread.stringTable.getString( thread.funcTable.name[funcIndex] ); diff --git a/src/components/timeline/TrackThread.js b/src/components/timeline/TrackThread.js index b1ad0c23c2..b0fa771fa3 100644 --- a/src/components/timeline/TrackThread.js +++ b/src/components/timeline/TrackThread.js @@ -52,13 +52,13 @@ import type { IndexIntoSamplesTable, Milliseconds, StartEndRange, - CallNodeInfo, ImplementationFilter, IndexIntoCallNodeTable, SelectedState, State, ThreadsKey, } from 'firefox-profiler/types'; +import type { CallNodeInfo } from 'firefox-profiler/profile-logic/call-node-info'; import type { ConnectedProps } from 'firefox-profiler/utils/connect'; diff --git a/src/components/tooltip/CallNode.js b/src/components/tooltip/CallNode.js index 9bc4568b68..6e334ae7d2 100644 --- a/src/components/tooltip/CallNode.js +++ b/src/components/tooltip/CallNode.js @@ -16,7 +16,6 @@ import type { CategoryList, IndexIntoCallNodeTable, CallNodeDisplayData, - CallNodeInfo, WeightType, Milliseconds, CallTreeSummaryStrategy, @@ -31,6 +30,7 @@ import type { ItemTimings, OneCategoryBreakdown, } from 'firefox-profiler/profile-logic/profile-data'; +import type { CallNodeInfo } from 'firefox-profiler/profile-logic/call-node-info'; import './CallNode.css'; import classNames from 'classnames'; @@ -365,12 +365,11 @@ export class TooltipCallNode extends React.PureComponent { callNodeInfo, displayStackType, } = this.props; - const callNodeTable = callNodeInfo.getCallNodeTable(); - const categoryIndex = callNodeTable.category[callNodeIndex]; + const categoryIndex = callNodeInfo.categoryForNode(callNodeIndex); const categoryColor = categories[categoryIndex].color; - const subcategoryIndex = callNodeTable.subcategory[callNodeIndex]; - const funcIndex = callNodeTable.func[callNodeIndex]; - const innerWindowID = callNodeTable.innerWindowID[callNodeIndex]; + const subcategoryIndex = callNodeInfo.subcategoryForNode(callNodeIndex); + const funcIndex = callNodeInfo.funcForNode(callNodeIndex); + const innerWindowID = callNodeInfo.innerWindowIDForNode(callNodeIndex); const funcStringIndex = thread.funcTable.name[funcIndex]; const funcName = thread.stringTable.getString(funcStringIndex); diff --git a/src/profile-logic/address-timings.js b/src/profile-logic/address-timings.js index 00080a3f3d..657895bddc 100644 --- a/src/profile-logic/address-timings.js +++ b/src/profile-logic/address-timings.js @@ -74,7 +74,6 @@ import type { FuncTable, StackTable, SamplesLikeTable, - CallNodeInfo, IndexIntoCallNodeTable, IndexIntoNativeSymbolTable, StackAddressInfo, @@ -83,6 +82,7 @@ import type { } from 'firefox-profiler/types'; import { getMatchingAncestorStackForInvertedCallNode } from './profile-data'; +import type { CallNodeInfo, CallNodeInfoInverted } from './call-node-info'; /** * For each stack in `stackTable`, and one specific native symbol, compute the @@ -202,12 +202,13 @@ export function getStackAddressInfoForCallNode( callNodeInfo: CallNodeInfo, nativeSymbol: IndexIntoNativeSymbolTable ): StackAddressInfo { - return callNodeInfo.isInverted() + const callNodeInfoInverted = callNodeInfo.asInverted(); + return callNodeInfoInverted !== null ? getStackAddressInfoForCallNodeInverted( stackTable, frameTable, callNodeIndex, - callNodeInfo, + callNodeInfoInverted, nativeSymbol ) : getStackAddressInfoForCallNodeNonInverted( @@ -426,16 +427,17 @@ export function getStackAddressInfoForCallNodeInverted( stackTable: StackTable, frameTable: FrameTable, callNodeIndex: IndexIntoCallNodeTable, - callNodeInfo: CallNodeInfo, + callNodeInfo: CallNodeInfoInverted, nativeSymbol: IndexIntoNativeSymbolTable ): StackAddressInfo { - const invertedCallNodeTable = callNodeInfo.getCallNodeTable(); - const depth = invertedCallNodeTable.depth[callNodeIndex]; - const endIndex = invertedCallNodeTable.subtreeRangeEnd[callNodeIndex]; - const callNodeIsRootOfInvertedTree = - invertedCallNodeTable.prefix[callNodeIndex] === -1; - const stackIndexToCallNodeIndex = callNodeInfo.getStackIndexToCallNodeIndex(); + const depth = callNodeInfo.depthForNode(callNodeIndex); + const [rangeStart, rangeEnd] = + callNodeInfo.getSuffixOrderIndexRangeForCallNode(callNodeIndex); + const callNodeIsRootOfInvertedTree = callNodeInfo.isRoot(callNodeIndex); + const stackIndexToCallNodeIndex = + callNodeInfo.getStackIndexToNonInvertedCallNodeIndex(); const stackTablePrefixCol = stackTable.prefix; + const suffixOrderIndexes = callNodeInfo.getSuffixOrderIndexes(); // "self address" == "the address which a stack's self time is contributed to" const callNodeSelfAddressForAllStacks = []; @@ -449,8 +451,9 @@ export function getStackAddressInfoForCallNodeInverted( const stackForCallNode = getMatchingAncestorStackForInvertedCallNode( stackIndex, - callNodeIndex, - endIndex, + rangeStart, + rangeEnd, + suffixOrderIndexes, depth, stackIndexToCallNodeIndex, stackTablePrefixCol diff --git a/src/profile-logic/call-node-info.js b/src/profile-logic/call-node-info.js index cf28cd6747..f5203318fc 100644 --- a/src/profile-logic/call-node-info.js +++ b/src/profile-logic/call-node-info.js @@ -4,38 +4,100 @@ // @flow -import { hashPath } from 'firefox-profiler/utils/path'; +import { + hashPath, + concatHash, + hashPathSingleFunc, +} from 'firefox-profiler/utils/path'; +import { ensureExists } from '../utils/flow'; +import { bisectionRightByKey } from '../utils/bisect'; import type { IndexIntoFuncTable, - CallNodeInfo, CallNodeTable, CallNodePath, IndexIntoCallNodeTable, + IndexIntoCategoryList, + IndexIntoNativeSymbolTable, + IndexIntoSubcategoryListForCategory, + InnerWindowID, } from 'firefox-profiler/types'; /** - * The implementation of the CallNodeInfo interface. + * An interface that's implemented in both the non-inverted and in the inverted + * case. The two CallNodeInfo implementations wrap the call node table and + * provide associated functionality. */ -export class CallNodeInfoImpl implements CallNodeInfo { +export interface CallNodeInfo { // If true, call node indexes describe nodes in the inverted call tree. - _isInverted: boolean; + isInverted(): boolean; - // The call node table. This is either the inverted or the non-inverted call - // node table, depending on _isInverted. - _callNodeTable: CallNodeTable; + // Returns this object as CallNodeInfoInverted if isInverted(), otherwise null. + asInverted(): CallNodeInfoInverted | null; + + // Returns the non-inverted call node table. + // This is always the non-inverted call node table, regardless of isInverted(). + getNonInvertedCallNodeTable(): CallNodeTable; + + // Returns a mapping from the stack table to the non-inverted call node table. + // The Int32Array should be used as if it were a + // Map. + // + // All entries are >= 0. + // This always maps to the non-inverted call node table, regardless of isInverted(). + getStackIndexToNonInvertedCallNodeIndex(): Int32Array; - // The non-inverted call node table, regardless of _isInverted. - _nonInvertedCallNodeTable: CallNodeTable; + // Converts a call node index into a call node path. + getCallNodePathFromIndex( + callNodeIndex: IndexIntoCallNodeTable | null + ): CallNodePath; - // The mapping of stack index to corresponding call node index. This maps to - // either the inverted or the non-inverted call node table, depending on - // _isInverted. - _stackIndexToCallNodeIndex: Int32Array; + // Converts a call node path into a call node index. + getCallNodeIndexFromPath( + callNodePath: CallNodePath + ): IndexIntoCallNodeTable | null; + + // Returns the call node index that matches the function `func` and whose + // parent's index is `parent`. If `parent` is -1, this returns the index of + // the root node with function `func`. + // Returns null if the described call node doesn't exist. + getCallNodeIndexFromParentAndFunc( + parent: IndexIntoCallNodeTable | -1, + func: IndexIntoFuncTable + ): IndexIntoCallNodeTable | null; + + // These functions return various properties about each node. You could also + // get these properties from the call node table, but that only works if the + // call node is a non-inverted call node (because we only have a non-inverted + // call node table). If your code is generic over inverted / non-inverted mode, + // and you just have a IndexIntoCallNodeTable and a CallNodeInfo instance, + // call the functions below. + + prefixForNode( + callNodeIndex: IndexIntoCallNodeTable + ): IndexIntoCallNodeTable | -1; + funcForNode(callNodeIndex: IndexIntoCallNodeTable): IndexIntoFuncTable; + categoryForNode(callNodeIndex: IndexIntoCallNodeTable): IndexIntoCategoryList; + subcategoryForNode( + callNodeIndex: IndexIntoCallNodeTable + ): IndexIntoCategoryList; + innerWindowIDForNode( + callNodeIndex: IndexIntoCallNodeTable + ): IndexIntoCategoryList; + depthForNode(callNodeIndex: IndexIntoCallNodeTable): number; + sourceFramesInlinedIntoSymbolForNode( + callNodeIndex: IndexIntoCallNodeTable + ): IndexIntoNativeSymbolTable | -1 | -2; +} + +/** + * The implementation of the CallNodeInfo interface for the non-inverted tree. + */ +export class CallNodeInfoNonInverted implements CallNodeInfo { + // The call node table. (always non-inverted) + _callNodeTable: CallNodeTable; // The mapping of stack index to corresponding non-inverted call node index. - // This always maps to the non-inverted call node table, regardless of - // _isInverted. _stackIndexToNonInvertedCallNodeIndex: Int32Array; // This is a Map. This map speeds up @@ -45,33 +107,23 @@ export class CallNodeInfoImpl implements CallNodeInfo { constructor( callNodeTable: CallNodeTable, - nonInvertedCallNodeTable: CallNodeTable, - stackIndexToCallNodeIndex: Int32Array, - stackIndexToNonInvertedCallNodeIndex: Int32Array, - isInverted: boolean + stackIndexToNonInvertedCallNodeIndex: Int32Array ) { this._callNodeTable = callNodeTable; - this._nonInvertedCallNodeTable = nonInvertedCallNodeTable; - this._stackIndexToCallNodeIndex = stackIndexToCallNodeIndex; this._stackIndexToNonInvertedCallNodeIndex = stackIndexToNonInvertedCallNodeIndex; - this._isInverted = isInverted; } isInverted(): boolean { - return this._isInverted; - } - - getCallNodeTable(): CallNodeTable { - return this._callNodeTable; + return false; } - getStackIndexToCallNodeIndex(): Int32Array { - return this._stackIndexToCallNodeIndex; + asInverted(): CallNodeInfoInverted | null { + return null; } getNonInvertedCallNodeTable(): CallNodeTable { - return this._nonInvertedCallNodeTable; + return this._callNodeTable; } getStackIndexToNonInvertedCallNodeIndex(): Int32Array { @@ -201,4 +253,1468 @@ export class CallNodeInfoImpl implements CallNodeInfo { return null; } + + prefixForNode( + callNodeIndex: IndexIntoCallNodeTable + ): IndexIntoCallNodeTable | -1 { + return this._callNodeTable.prefix[callNodeIndex]; + } + + funcForNode(callNodeIndex: IndexIntoCallNodeTable): IndexIntoFuncTable { + return this._callNodeTable.func[callNodeIndex]; + } + + categoryForNode( + callNodeIndex: IndexIntoCallNodeTable + ): IndexIntoCategoryList { + return this._callNodeTable.category[callNodeIndex]; + } + + subcategoryForNode( + callNodeIndex: IndexIntoCallNodeTable + ): IndexIntoCategoryList { + return this._callNodeTable.subcategory[callNodeIndex]; + } + + innerWindowIDForNode( + callNodeIndex: IndexIntoCallNodeTable + ): IndexIntoCategoryList { + return this._callNodeTable.innerWindowID[callNodeIndex]; + } + + depthForNode(callNodeIndex: IndexIntoCallNodeTable): number { + return this._callNodeTable.depth[callNodeIndex]; + } + + sourceFramesInlinedIntoSymbolForNode( + callNodeIndex: IndexIntoCallNodeTable + ): IndexIntoNativeSymbolTable | -1 | -2 { + return this._callNodeTable.sourceFramesInlinedIntoSymbol[callNodeIndex]; + } +} + +// A "subtype" of IndexIntoCallNodeTable, used in places where it is known that +// we are referring to an inverted call node. We just use it as a convention, +// Flow doesn't actually treat this any different from any other index and won't +// catch incorrect uses. +type InvertedCallNodeHandle = number; + +// An index into InvertedNonRootCallNodeTable. This is usually created by +// taking an InvertedCallNodeHandle and subtracting rootCount. +type IndexIntoInvertedNonRootCallNodeTable = number; + +// Information about the roots of the inverted call tree. We compute this +// information upfront for all roots. The root count is fixed, so most of the +// arrays in this struct are fixed-size typed arrays. +// The number of roots is the same as the number of functions in the funcTable. +type InvertedRootCallNodeTable = {| + category: Int32Array, // IndexIntoFuncTable -> IndexIntoCategoryList + subcategory: Int32Array, // IndexIntoFuncTable -> IndexIntoSubcategoryListForCategory + innerWindowID: Float64Array, // IndexIntoFuncTable -> InnerWindowID + // IndexIntoNativeSymbolTable: all frames that collapsed into this call node inlined into the same native symbol + // -1: divergent: some, but not all, frames that collapsed into this call node were inlined, or they are from different symbols + // -2: no inlining + sourceFramesInlinedIntoSymbol: Int32Array, // IndexIntoFuncTable -> IndexIntoNativeSymbolTable | -1 | -2 + // The (exclusive) end of the suffix order index range for each root node. + // The beginning of the range is given by suffixOrderIndexRangeEnd[i - 1], or by + // zero. This is possible because both the inverted root order and the suffix order + // are determined by the func order. + suffixOrderIndexRangeEnd: Uint32Array, // IndexIntoFuncTable -> SuffixOrderIndex, + length: number, +|}; + +// Information about the non-root nodes of the inverted call tree. This table +// grows on-demand, as new inverted call nodes are materialized. +type InvertedNonRootCallNodeTable = {| + prefix: InvertedCallNodeHandle[], + func: IndexIntoFuncTable[], // IndexIntoInvertedNonRootCallNodeTable -> IndexIntoFuncTable + pathHash: string[], // IndexIntoInvertedNonRootCallNodeTable -> string + category: IndexIntoCategoryList[], // IndexIntoInvertedNonRootCallNodeTable -> IndexIntoCategoryList + subcategory: IndexIntoSubcategoryListForCategory[], // IndexIntoInvertedNonRootCallNodeTable -> IndexIntoSubcategoryListForCategory + innerWindowID: InnerWindowID[], // IndexIntoInvertedNonRootCallNodeTable -> InnerWindowID + // IndexIntoNativeSymbolTable: all frames that collapsed into this call node inlined into the same native symbol + // -1: divergent: some, but not all, frames that collapsed into this call node were inlined, or they are from different symbols + // -2: no inlining + sourceFramesInlinedIntoSymbol: Array, + suffixOrderIndexRangeStart: SuffixOrderIndex[], // IndexIntoInvertedNonRootCallNodeTable -> SuffixOrderIndex + suffixOrderIndexRangeEnd: SuffixOrderIndex[], // IndexIntoInvertedNonRootCallNodeTable -> SuffixOrderIndex + + // Non-null for non-root nodes whose children haven't been created yet. + // The array at index x caches ancestors of the non-inverted nodes belonging + // to the inverted node x, specifically the ancestor "k steps up" from each + // non-inverted node, with k being the depth of the inverted node. + // This is useful to quickly compute the children for this inverted node. + // Afterwards it's set to null for that index, and the next level is passed on + // to the newly-created children. + // Please refer to 'Why do we keep a "deepNodes" property in the inverted table?' + // in the comment on CallNodeInvertedImpl below for a more detailed explanation. + // + // For every inverted non-root call node x with deepNodes[x] !== null: + // For every suffix order index i in suffixOrderIndexRangeStart[x]..suffixOrderIndexRangeEnd[x], + // the k'th parent node of suffixOrderedCallNodes[i] is stored at + // deepNodes[x][i - suffixOrderIndexRangeStart[x]], with k = depth[x]. + deepNodes: Array, // IndexIntoInvertedNonRootCallNodeTable -> (Uint32Array | null) + + depth: number[], // IndexIntoInvertedNonRootCallNodeTable -> number + length: number, +|}; + +// Compute the InvertedRootCallNodeTable. +// We compute this information upfront for all roots. The root count is fixed - +// the number of roots is the same as the number of functions in the funcTable. +function _createInvertedRootCallNodeTable( + callNodeTable: CallNodeTable, + rootSuffixOrderIndexRangeEndCol: Uint32Array, + suffixOrderedCallNodes: Uint32Array, + defaultCategory: IndexIntoCategoryList +): InvertedRootCallNodeTable { + const funcCount = rootSuffixOrderIndexRangeEndCol.length; + const category = new Int32Array(funcCount); + const subcategory = new Int32Array(funcCount); + const innerWindowID = new Float64Array(funcCount); + const sourceFramesInlinedIntoSymbol = new Int32Array(funcCount); + let previousRootSuffixOrderIndexRangeEnd = 0; + for (let funcIndex = 0; funcIndex < funcCount; funcIndex++) { + const callNodeSuffixOrderIndexRangeStart = + previousRootSuffixOrderIndexRangeEnd; + const callNodeSuffixOrderIndexRangeEnd = + rootSuffixOrderIndexRangeEndCol[funcIndex]; + previousRootSuffixOrderIndexRangeEnd = callNodeSuffixOrderIndexRangeEnd; + if ( + callNodeSuffixOrderIndexRangeStart === callNodeSuffixOrderIndexRangeEnd + ) { + // This root is never actually displayed in the inverted tree. It + // corresponds to a func which has no self time - no non-inverted node has + // this func as its self func. This root only exists for simplicity, so + // that there is one root per func. + + // Set a dummy value for this unused root. + sourceFramesInlinedIntoSymbol[funcIndex] = -2; // "no symbol" + // (the other columns are already initialized to zero because they're + // typed arrays) + continue; + } + + // Fill the remaining fields with the conflict-resolved versions of the values + // in the non-inverted call node table. + const firstNonInvertedCallNodeIndex = + suffixOrderedCallNodes[callNodeSuffixOrderIndexRangeStart]; + let resolvedCategory = + callNodeTable.category[firstNonInvertedCallNodeIndex]; + let resolvedSubcategory = + callNodeTable.subcategory[firstNonInvertedCallNodeIndex]; + const resolvedInnerWindowID = + callNodeTable.innerWindowID[firstNonInvertedCallNodeIndex]; + let resolvedSourceFramesInlinedIntoSymbol = + callNodeTable.sourceFramesInlinedIntoSymbol[ + firstNonInvertedCallNodeIndex + ]; + + // Resolve conflicts in the same way as for the non-inverted call node table. + for ( + let orderingIndex = callNodeSuffixOrderIndexRangeStart + 1; + orderingIndex < callNodeSuffixOrderIndexRangeEnd; + orderingIndex++ + ) { + const currentNonInvertedCallNodeIndex = + suffixOrderedCallNodes[orderingIndex]; + // Resolve category conflicts, by resetting a conflicting subcategory or + // category to the default category. + if ( + resolvedCategory !== + callNodeTable.category[currentNonInvertedCallNodeIndex] + ) { + // Conflicting origin stack categories -> default category + subcategory. + resolvedCategory = defaultCategory; + resolvedSubcategory = 0; + } else if ( + resolvedSubcategory !== + callNodeTable.subcategory[currentNonInvertedCallNodeIndex] + ) { + // Conflicting origin stack subcategories -> "Other" subcategory. + resolvedSubcategory = 0; + } + + // Resolve "inlined into" conflicts. This can happen if you have two + // function calls A -> B where only one of the B calls is inlined, or + // if you use call tree transforms in such a way that a function B which + // was inlined into two different callers (A -> B, C -> B) gets collapsed + // into one call node. + if ( + resolvedSourceFramesInlinedIntoSymbol !== + callNodeTable.sourceFramesInlinedIntoSymbol[ + currentNonInvertedCallNodeIndex + ] + ) { + // Conflicting inlining: -1. + resolvedSourceFramesInlinedIntoSymbol = -1; + } + + // FIXME: Resolve conflicts of InnerWindowID + } + + category[funcIndex] = resolvedCategory; + subcategory[funcIndex] = resolvedSubcategory; + innerWindowID[funcIndex] = resolvedInnerWindowID; + sourceFramesInlinedIntoSymbol[funcIndex] = + resolvedSourceFramesInlinedIntoSymbol; + } + + return { + category, + subcategory, + innerWindowID, + sourceFramesInlinedIntoSymbol, + suffixOrderIndexRangeEnd: rootSuffixOrderIndexRangeEndCol, + length: funcCount, + }; +} + +function _createEmptyInvertedNonRootCallNodeTable(): InvertedNonRootCallNodeTable { + return { + prefix: [], + func: [], + pathHash: [], + category: [], + subcategory: [], + innerWindowID: [], + sourceFramesInlinedIntoSymbol: [], + suffixOrderIndexRangeStart: [], + suffixOrderIndexRangeEnd: [], + deepNodes: [], + depth: [], + length: 0, + }; +} + +// The return type of _computeSuffixOrderForInvertedRoots. +// +// This is not the fully-refined suffix order; you could say that it's +// refined up to depth zero. It is refined enough so that every root has a +// contiguous range in the suffix order, where each range contains the root's +// corresponding non-inverted nodes. +type SuffixOrderForInvertedRoots = {| + suffixOrderedCallNodes: Uint32Array, + suffixOrderIndexes: Uint32Array, + rootSuffixOrderIndexRangeEndCol: Uint32Array, +|}; + +/** + * Computes an ordering for the non-inverted call node table where all + * non-inverted call nodes are ordered by their self func. + * + * This function is very performance sensitive. The number of non-inverted call + * nodes can be very high, e.g. ~3 million for https://share.firefox.dev/3N56qMu + */ +function _computeSuffixOrderForInvertedRoots( + nonInvertedCallNodeTable: CallNodeTable, + funcCount: number +): SuffixOrderForInvertedRoots { + // Rather than using Array.prototype.sort, this function uses the technique + // used by "radix sort": + // + // 1. Count the occurrences per key, i.e. the number of call nodes per func. + // 2. Reserve slices in the sorted space, by accumulating the counts into a + // start index per partition. + // 3. Put the unsorted values into their sorted spots, incrementing the + // per-partition next index as we go. + // + // This is much faster, and it also makes it easier to compute the inverse + // mapping (suffixOrderIndexes) and the rootSuffixOrderIndexRangeEndCol. + + // Pass 1: Compute, per func, how many non-inverted call nodes end in this func. + const nodeCountPerFunc = new Uint32Array(funcCount); + const callNodeCount = nonInvertedCallNodeTable.length; + const callNodeTableFuncCol = nonInvertedCallNodeTable.func; + for (let i = 0; i < callNodeCount; i++) { + const func = callNodeTableFuncCol[i]; + nodeCountPerFunc[func]++; + } + + // Pass 2: Compute cumulative start index based on the counts. + const startIndexPerFunc = nodeCountPerFunc; // Warning: we are reusing the same array + let nextFuncStartIndex = 0; + for (let func = 0; func < startIndexPerFunc.length; func++) { + const count = nodeCountPerFunc[func]; + startIndexPerFunc[func] = nextFuncStartIndex; + nextFuncStartIndex += count; + } + + // Pass 3: Compute the new ordering based on the reserved slices in startIndexPerFunc. + const nextIndexPerFunc = startIndexPerFunc; + const suffixOrderedCallNodes = new Uint32Array(callNodeCount); + const suffixOrderIndexes = new Uint32Array(callNodeCount); + for (let callNode = 0; callNode < callNodeCount; callNode++) { + const func = callNodeTableFuncCol[callNode]; + const orderIndex = nextIndexPerFunc[func]++; + suffixOrderedCallNodes[orderIndex] = callNode; + suffixOrderIndexes[callNode] = orderIndex; + } + + // The indexes in nextIndexPerFunc have now been advanced such that they point + // at the end of each partition. + const rootSuffixOrderIndexRangeEndCol = startIndexPerFunc; + + return { + suffixOrderedCallNodes, + suffixOrderIndexes, + rootSuffixOrderIndexRangeEndCol, + }; +} + +// Information used to create the children of a node in the inverted tree. +type ChildrenInfo = {| + // The func for each child. Duplicate-free and sorted by func. + funcPerChild: Uint32Array, // IndexIntoFuncTable[] + // The number of deep nodes for each child. Every entry is non-zero. + deepNodeCountPerChild: Uint32Array, + // The subset of the parent's self nodes which are not part of childrenSelfNodes. + selfNodesWhichEndAtParent: IndexIntoCallNodeTable[], + // The self nodes and their corresponding deep nodes for all children, each + // flattened into a single array. + // The length of these arrays is the sum of the values in deepNodeCountPerChild. + childrenSelfNodes: Uint32Array, + childrenDeepNodes: Uint32Array, + // The suffixOrderIndexRangeStart of the first child. + childrenSuffixOrderIndexRangeStart: number, +|}; + +// An index into SuffixOrderedCallNodes. +export type SuffixOrderIndex = number; + +/** + * The CallNodeInfo implementation for the inverted tree, with additional + * functionality for the inverted call tree. + * + * # The Suffix Order + * + * We define an alternative ordering of the *non-inverted* call nodes, called the + * "suffix order", which is useful when interacting with the *inverted* tree. + * The suffix order is stored by two Uint32Array side tables, returned by + * getSuffixOrderedCallNodes() and getSuffixOrderIndexes(). + * getSuffixOrderedCallNodes() maps a suffix order index to a non-inverted call + * node, and getSuffixOrderIndexes() is the reverse, mapping a non-inverted call + * node to its suffix order index. + * + * ## Background + * + * Many operations we do in the profiler require the ability to do an efficient + * "ancestor" check: + * + * - For a call node X in the call tree, what's its "total"? + * - When call node X in the call tree is selected, which samples should be + * highlighted in the activity graph, and which samples should contribute to + * the category breakdown in the sidebar? + * - For how many samples has the clicked call node X been observed in a certain + * line of code / in a certain instruction? + * + * We answer these questions by iterating over samples, getting the sample's + * call node Y, and checking whether the selected / clicked node X is an ancestor + * of Y. + * + * In the non-inverted call tree, the ordering in the call node table gives us a + * quick way to do these checks: For a call node X, all its descendant call nodes + * are in a contiguous range between X and callNodeTable.subtreeRangeEnd[X]. + * + * We want to have a similar ability for the *inverted* call tree, but without + * computing a full inverted call node table. The suffix order gives us this + * ability. It's based on the following insights: + * + * 1. Non-inverted call nodes are "enough" for many purposes even in inverted mode: + * + * When doing the per-sample checks listed above, we don't need an *inverted* + * call node for each sample. We just need an inverted call node for the + * clicked / selected node, and then we can check if the sample's + * *non-inverted* call node contributes to the selected / clicked *inverted* + * call node. + * A non-inverted call node is just a representation of a call path. You can + * read that call path from front to back, or you can read it from back to + * front. If you read it from back to front that's the inverted call path. + * + * 2. We can store multiple different orderings of the non-inverted call node + * table. + * + * The non-inverted call node table remains ordered in depth-first traversal + * order of the non-inverted tree, as described in the "Call node ordering" + * section on the CallNodeTable type. The suffix order is an additional, + * alternative ordering that we store on the side. + * + * ## Definition + * + * We define the suffix order as the lexicographic order of the inverted call path. + * Or as the lexicographic order of the non-inverted call paths "when reading back to front". + * + * D -> B comes before A -> C, because B comes before C. + * D -> B comes after A -> B, because B == B and D comes after A. + * D -> B comes before A -> D -> B, because B == B, D == D, and "end of path" comes before A. + * + * ## Example + * + * ### Non-inverted call tree: + * + * Legend: + * + * cnX: Non-inverted call node index X + * soX: Suffix order index X + * + * ``` + * Tree Left aligned Right aligned Reordered by suffix + * - [cn0] A = A = A [so0] [so0] [cn0] A + * - [cn1] B = A -> B = A -> B [so3] [so1] [cn4] A <- A + * - [cn2] A = A -> B -> A = A -> B -> A [so2] ↘↗ [so2] [cn2] A <- B <- A + * - [cn3] C = A -> B -> C = A -> B -> C [so6] ↗↘ [so3] [cn1] B <- A + * - [cn4] A = A -> A = A -> A [so1] [so4] [cn5] B <- A <- A + * - [cn5] B = A -> A -> B = A -> A -> B [so4] [so5] [cn6] C <- A + * - [cn6] C = A -> C = A -> C [so5] [so6] [cn3] C <- B <- A + * ``` + * + * ### Inverted call tree: + * + * Legend, continued: + * + * inX: Inverted call node index X (this index is somewhat arbitrary because + * it's based on the order in which callNodeInfoInverted.getChildren is + * called) + * so:X..Y: Suffix order index range soX..soY (soY excluded) + * + * ``` + * Represents call paths ending in + * - [in0] A (so:0..3) = A = ... A (cn0, cn4, cn2) + * - [in3] A (so:1..2) = A <- A = ... A -> A (cn4) + * - [in4] B (so:2..3) = A <- B = ... B -> A (cn2) + * - [in6] A (so:2..3) = A <- B <- A = ... A -> B -> A (cn2) + * - [in1] B (so:3..5) = B = ... B (cn1, cn5) + * - [in5] A (so:3..5) = B <- A = ... A -> B (cn1, cn5) + * - [in10] A (so:4..5) = B <- A <- A = ... A -> A -> B (cn5) + * - [in2] C (so:5..7) = C = ... C (cn6, cn3) + * - [in7] A (so:5..6) = C <- A = ... A -> C (cn6) + * - [in8] B (so:6..7) = C <- B = ... B -> C (cn3) + * - [in9] A (so:6..7) = C <- B <- A = ... A -> B -> C (cn3) + * ``` + * + * In the suffix order, call paths become grouped in such a way that call paths + * which belong to the same *inverted* tree node (i.e. which share a suffix) end + * up ordered next to each other. This makes it so that a node in the inverted + * tree can refer to all its represented call paths with a single contiguous range. + * + * In this example, inverted tree node `in5` represents all call paths which end + * in A -> B. Both `cn1` and `cn5` do so; `cn1` is A -> B and `cn5` is A -> A -> B. + * In the suffix order, `cn1` and `cn5` end up next to each other, at positions + * `so3` and `so4`. This means that the two paths can be referred to via the suffix + * order index range 3..5. + * + * Suffix ordered call nodes: [0, 4, 2, 1, 5, 6, 3] (soX -> cnY) + * Suffix order indexes: [0, 3, 2, 6, 1, 4, 5] (cnX -> soY) + * + * ## Incremental order refinement + * + * Sorting all non-inverted nodes upfront would take a long time on large profiles. + * So we don't do that. Instead, we refine the order as new inverted tree nodes + * are materialized on demand. + * + * The ground rules are: + * - For any inverted call node X, getSuffixOrderIndexRangeForCallNode(X) must + * always return the same range. + * - For any inverted call node X, the *set* of suffix ordered call nodes in the + * range returned by getSuffixOrderIndexRangeForCallNode(X) must always be the + * same. Notably, the order in the range does *not* necessarily need to remain + * the same. + * + * This means that, whenever you have a handle X of an inverted call node, you + * can be confident that your checks of the form "is non-inverted call node Y + * part of X's range" will work correctly. + * + * # On-demand node creation + * + * Inverted nodes are created in this order: + * + * 1. All root nodes have been created upfront. There is one root per func. + * 2. The first _createChildren call will be for a root node. We create non-root + * nodes for the root's children, and add them to _invertedNonRootCallNodeTable. + * 3. The next call to _createChildren can be for a non-root node. Again we + * create nodes for the children and add them to _invertedNonRootCallNodeTable. + * + * Example: + * + * ``` + * Non-inverted tree: + * + * Tree Left aligned Right aligned + * - [cn0] A = A = A [so0] + * - [cn1] B = A -> B = A -> B [so3] + * - [cn2] A = A -> B -> A = A -> B -> A [so2] + * - [cn3] C = A -> B -> C = A -> B -> C [so6] + * - [cn4] A = A -> A = A -> A [so1] + * - [cn5] B = A -> A -> B = A -> A -> B [so4] + * - [cn6] C = A -> C = A -> C [so5] + * + * Inverted tree: + * Represents call paths ending in + * - [in0] A (so:0..3) = A = ... A (cn0, cn4, cn2) + * - [in3] A (so:1..2) = A <- A = ... A -> A (cn4) + * - [in4] B (so:2..3) = A <- B = ... B -> A (cn2) + * - [in6] A (so:2..3) = A <- B <- A = ... A -> B -> A (cn2) + * - [in1] B (so:3..5) = B = ... B (cn1, cn5) + * - [in5] A (so:3..5) = B <- A = ... A -> B (cn1, cn5) + * - [in10] A (so:4..5) = B <- A <- A = ... A -> A -> B (cn5) + * - [in2] C (so:5..7) = C = ... C (cn6, cn3) + * - [in7] A (so:5..6) = C <- A = ... A -> C (cn6) + * - [in8] B (so:6..7) = C <- B = ... B -> C (cn3) + * - [in9] A (so:6..7) = C <- B <- A = ... A -> B -> C (cn3) + * ``` + * + * This inverted tree was built up as follows: + * + * Iteration 0: We create all roots. New nodes: in0, in1, in2 + * Iteration 1: _createChildren(in0) is called. New nodes: in3, in4 + * Iteration 2: _createChildren(in1) is called. New nodes: in5 + * Iteration 3: _createChildren(in4) is called. New nodes: in6 + * Iteration 4: _createChildren(in2) is called. New nodes: in7, in8 + * Iteration 5: _createChildren(in8) is called. New nodes: in9 + * Iteration 6: _createChildren(in5) is called. New nodes: in10 + * + * The order of the _createChildren calls depends on how the user interacts with the + * call tree. The user could uncollapse call tree nodes in a different order, + * which would cause _createChildren to be called in a different order, and the + * inverted nodes we create would have different indexes. + * + * There are two invariants about "which inverted nodes exist" at any given time: + * + * 1. For any inverted tree node inX, _invertedNonRootCallNodeTable either contains + * all or none of inX's children. + * 2. For any inverted non-root node `inQ` with parent node `inP`, + * _createChildren(inP) is called before _createChildren(inQ) is called. That's + * somewhat obvious: inQ is *created* by the _createChildren(inP) call; without + * _createChildren(inP) we would not have an inQ to pass to _createChildren(inQ). + * + * ## Computation of the children + * + * How do we know which children to create? We look at the parents in the + * *non-inverted* tree. + * + * First, let's create the children for the root `in0` (func: A, depth 0). + * in0 has three "self nodes": cn0 (func: A), cn4 (func: A), and cn2 (func: A). + * + * To create the children of in0, we need to look at the parents of cn0, cn4, and cn2. + * + * cn0 has no parent. + * cn4's parent is cn0 (func: A). + * cn2's parent is cn1 (func: B). + * + * This means that in0 has two children: One for func A and one for func B. + * + * Let's create the two children: + * - in3: func A, parent in0, depth 1, self nodes [cn4] + * - in4: func B, parent in0, depth 1, self nodes [cn2] + * + * ### Why do we keep a "deepNodes" property in the inverted table? + * + * In the next few paragraphs, we'll explain why we need to constantly iterate + * over the non-inverted parents, and that the "deepNodes" property is a cache + * to make it faster. Keep reading! + * + * Let's create the children of the non-root node in4 (func: B, depth 1). + * in4 represents the call path suffix "... -> B -> A". + * + * in4 has one self node: cn2 (func: A). cn2 is the only non-inverted node + * whose call path ends in "... -> B -> A". + * + * cn2's 0th parent (i.e. itself) is cn2 (func: A). + * cn2's 1st parent is cn1 (func: B). + * cn2's 2nd parent (i.e. its grandparent) is cn0 (func: A). <-- func A + * + * So in4 has one child, with func A. Let's create it: + * - in6: func A, parent in4, depth 2, self nodes [cn2] + * + * This example shows that, if we create inverted children of depth 2, + * we need to look at the grandparent ("2nd parent") of each self node. + * + * --- + * + * Let's try to go one level deeper and create the children of in6 (func A, depth 2): + * + * in6 has one self node: cn2. + * in6 has depth 2, its children would have depth 3. + * + * cn2's 0th parent is cn2 (func: A). + * cn2's 1st parent is cn1 (func: B). + * cn2's 2nd parent is cn0 (func: A). + * cn2's 3rd parent is ... it does not have one! + * + * So in6 has no children. + * + * --- + * + * More generically, we've shown that, in order to create inverted children of + * depth k, we need to look at the k'th parent of all self nodes for the inverted + * node whose children we're creating. + * + * If we had to get those k'th parent nodes from the self node all the time, we + * would spend a lot of time walking up the non-inverted tree! For example, if + * we wanted to create the children of an inverted node with depth 20, if that + * node had 500 self nodes, we would need to find the 21st parent node of each of + * those 500 self nodes! + * + * Walking up 21 steps is a bit silly, because we already walked up 20 steps for + * the same nodes when we created the inverted parent. Can we just store the result + * of the 20-step walk, and reuse it? Yes we can! + * + * So that's what why we have the "deepNodes" cache: On each inverted node, we + * don't only store its self nodes, we also store its "deep nodes", i.e. the k'th + * parent of each self node, with k being the depth of the inverted node. + * Then we only need to look at the immediate parent of each deep node in order + * to know which children to create for the inverted node. + * + * For an inverted root such as in0, k is 0, and the deep node for each self node + * is just the self node itself. (The 0'th parent of a node is that node itself.) + * + * in0: + * |-----------|-------------------------| + * | self node | corresponding deep node | + * |-----------|-------------------------| + * | cn0 | cn0 | + * | cn4 | cn4 | + * | cn2 | cn2 | + * |-----------|-------------------------| + * + * For in4, k is 1, and the deep node for each self node is the self node's + * immediate parent. + * + * in4: + * |-----------|-------------------------| + * | self node | corresponding deep node | + * |-----------|-------------------------| + * | cn2 | cn1 | + * |-----------|-------------------------| + * + * in6 (depth 2): + * |-----------|-------------------------| + * | self node | corresponding deep node | + * |-----------|-------------------------| + * | cn2 | cn0 | + * |-----------|-------------------------| + * + * So whenever we create the children of an inverted node, we start with its + * deep nodes and get their immediate parents. These parents become the deep + * nodes of the newly-created children. We store them on each new child. And + * this saves time because we don't have to walk up the parent chain by more + * than one step. + * + * Once we've created the children of an inverted node, we can discard its own + * deep nodes. They're not needed anymore. So _takeDeepNodesForInvertedNode + * nulls out the stored deepNodes for an inverted node when it's called. + */ +export class CallNodeInfoInverted implements CallNodeInfo { + // The non-inverted call node table. + _callNodeTable: CallNodeTable; + + // The part of the inverted call node table for the roots of the inverted tree. + _invertedRootCallNodeTable: InvertedRootCallNodeTable; + + // The dynamically growing part of the inverted call node table for just the + // non-root nodes. Entries are added to this table as needed, whenever a caller + // asks us for children of a node for which we haven't needed children before, + // or when a caller asks us to translate an inverted call path that we haven't + // seend before to an inverted call node index. + _invertedNonRootCallNodeTable: InvertedNonRootCallNodeTable; + + // The mapping of non-inverted stack index to non-inverted call node index. + _stackIndexToNonInvertedCallNodeIndex: Int32Array; + + // The number of roots, which is also the number of functions. Each root of + // the inverted tree represents a "self" function, i.e. all call paths which + // end in a certain function. + // We have roots even for functions which aren't used as "self" functions in + // any sampled stacks, for simplicity. The actual displayed number of roots + // in the call tree will usually be lower because roots with a zero total sample + // count will be filtered out. But any data in this class is fully independent + // from sample counts. + _rootCount: number; + + // This is a Map. + // It lists the non-inverted call nodes in "suffix order", i.e. ordered by + // comparing their call paths from back to front. + _suffixOrderedCallNodes: Uint32Array; + + // This is the inverse of _suffixOrderedCallNodes; i.e. it is a + // Map. + _suffixOrderIndexes: Uint32Array; + + // The default category (usually "Other"), used when creating new inverted + // call nodes based on divergently-categorized functions. + _defaultCategory: IndexIntoCategoryList; + + // This is a Map. This map speeds up + // the look-up process by caching every CallNodePath we handle which avoids + // repeatedly looking up parents. + _cache: Map = new Map(); + + // For every inverted call node, the list of its child nodes, if we've computed + // it already. Entries are inserted by getChildren(). + _children: Map = new Map(); + + constructor( + callNodeTable: CallNodeTable, + stackIndexToNonInvertedCallNodeIndex: Int32Array, + defaultCategory: IndexIntoCategoryList, + funcCount: number + ) { + this._callNodeTable = callNodeTable; + this._stackIndexToNonInvertedCallNodeIndex = + stackIndexToNonInvertedCallNodeIndex; + + const { + suffixOrderedCallNodes, + suffixOrderIndexes, + rootSuffixOrderIndexRangeEndCol, + } = _computeSuffixOrderForInvertedRoots(callNodeTable, funcCount); + + this._suffixOrderedCallNodes = suffixOrderedCallNodes; + this._suffixOrderIndexes = suffixOrderIndexes; + this._defaultCategory = defaultCategory; + this._rootCount = funcCount; + + const invertedRootCallNodeTable = _createInvertedRootCallNodeTable( + callNodeTable, + rootSuffixOrderIndexRangeEndCol, + suffixOrderedCallNodes, + defaultCategory + ); + this._invertedRootCallNodeTable = invertedRootCallNodeTable; + this._invertedNonRootCallNodeTable = + _createEmptyInvertedNonRootCallNodeTable(); + } + + isInverted(): boolean { + return true; + } + + asInverted(): CallNodeInfoInverted | null { + return this; + } + + getNonInvertedCallNodeTable(): CallNodeTable { + return this._callNodeTable; + } + + getStackIndexToNonInvertedCallNodeIndex(): Int32Array { + return this._stackIndexToNonInvertedCallNodeIndex; + } + + // Get a mapping SuffixOrderIndex -> IndexIntoNonInvertedCallNodeTable. + // This array contains all non-inverted call node indexes, ordered by + // call path suffix. See "suffix order" in the documentation above. + // Note that the contents of this array will be mutated by CallNodeInfoInverted + // when new inverted nodes are created on demand (e.g. during a call to + // getChildren or to getCallNodeIndexFromPath). So callers should not hold on + // to this array across calls which can create new inverted call nodes. + getSuffixOrderedCallNodes(): Uint32Array { + return this._suffixOrderedCallNodes; + } + + // Returns the inverse of getSuffixOrderedCallNodes(), i.e. a mapping + // IndexIntoNonInvertedCallNodeTable -> SuffixOrderIndex. + // Note that the contents of this array will be mutated by CallNodeInfoInverted + // when new inverted nodes are created on demand (e.g. during a call to + // getChildren or to getCallNodeIndexFromPath). So callers should not hold on + // to this array across calls which can create new inverted call nodes. + getSuffixOrderIndexes(): Uint32Array { + return this._suffixOrderIndexes; + } + + // Get the number of functions. There is one root per function. + // So this is also the number of roots at the same time. + // The inverted call node index for a root is the same as the function index. + getFuncCount(): number { + return this._rootCount; + } + + // Returns whether the given node is a root node. + isRoot(nodeHandle: InvertedCallNodeHandle): boolean { + return nodeHandle < this._rootCount; + } + + // Get the [start, exclusiveEnd] range of suffix order indexes for this + // inverted tree node. This lets you list the non-inverted call nodes which + // "contribute to" the given inverted call node. Or put differently, it lets + // you iterate over the non-inverted call nodes whose call paths "end with" + // the call path suffix represented by the inverted node. + // By the definition of the suffix order, all non-inverted call nodes whose + // call path ends with the suffix defined by the inverted call node `callNodeIndex` + // will be in a contiguous range in the suffix order. + getSuffixOrderIndexRangeForCallNode( + nodeHandle: InvertedCallNodeHandle + ): [SuffixOrderIndex, SuffixOrderIndex] { + if (nodeHandle < this._rootCount) { + // nodeHandle is a root. For roots, the node handle IS the func index. + const funcIndex = nodeHandle; + const rangeStart = + funcIndex === 0 + ? 0 + : this._invertedRootCallNodeTable.suffixOrderIndexRangeEnd[ + funcIndex - 1 + ]; + const rangeEnd = + this._invertedRootCallNodeTable.suffixOrderIndexRangeEnd[funcIndex]; + return [rangeStart, rangeEnd]; + } + + const nonRootIndex = nodeHandle - this._rootCount; + const rangeStart = + this._invertedNonRootCallNodeTable.suffixOrderIndexRangeStart[ + nonRootIndex + ]; + const rangeEnd = + this._invertedNonRootCallNodeTable.suffixOrderIndexRangeEnd[nonRootIndex]; + return [rangeStart, rangeEnd]; + } + + /** + * Materialize inverted call nodes for parentNodeHandle's children in the + * inverted tree. + * + * The returned array of call node handles is sorted by func. + */ + _createChildren( + parentNodeHandle: InvertedCallNodeHandle + ): InvertedCallNodeHandle[] { + const parentDeepNodes = + this._takeDeepNodesForInvertedNode(parentNodeHandle); + const childrenInfo = this._computeChildrenInfo( + parentNodeHandle, + parentDeepNodes + ); + if (childrenInfo === null) { + // This node has no children. + return []; + } + + this._applyRefinedSuffixOrderForNode( + parentNodeHandle, + childrenInfo.selfNodesWhichEndAtParent, + childrenInfo.childrenSelfNodes + ); + + return this._createChildrenForInfo(childrenInfo, parentNodeHandle); + } + + /** + * Compute the information needed to create the children of parentNodeHandle, + * and the information needed to refine the suffix order for the parent's + * suffix order index range. + * + * As we go deeper into the inverted tree, we go higher up in the non-inverted + * tree: To create the children of an inverted node, we need to look at the + * parents / "prefixes" of the corresponding non-inverted "deep nodes". + * + * See the class documentation for more details and examples. + */ + _computeChildrenInfo( + parentNodeHandle: InvertedCallNodeHandle, + parentDeepNodes: Uint32Array + ): ChildrenInfo | null { + const parentDeepNodeCount = parentDeepNodes.length; + const [parentIndexRangeStart, parentIndexRangeEnd] = + this.getSuffixOrderIndexRangeForCallNode(parentNodeHandle); + const parentSelfNodes = this._suffixOrderedCallNodes.subarray( + parentIndexRangeStart, + parentIndexRangeEnd + ); + + if (parentSelfNodes.length !== parentDeepNodes.length) { + throw new Error('indexes out of sync'); + } + + // We have the parent's self nodes and their corresponding deep nodes. + // These nodes are currently only sorted up to the parent's depth: + // we know that every parentDeepNode has the parent's func. + // But if we look at the prefix of each parentDeepNode, we'll encounter + // funcs in an arbitrary order. + // + // It is this function's responsibility to come up with a re-arranged order + // such that each of the newly-created child nodes can have a contiguous + // range of suffix ordered call nodes. + // + // To compute the new order, we do the following: + // + // 1. We iterate over all the deep nodes in the parent's range, and count + // how many there are, per deep node func. + // 2. We reserve space based on those counts, by computing a start index + // for each collection of deep nodes (one partition per func). + // 3. We create ordered arrays, by taking the unordered nodes and putting + // them in the right spot based on the computed start indexes. + // + // The parent may also have deep nodes which don't have a prefix. We track + // those separately. Once the suffix order is updated, the corresponding + // self nodes for these deep nodes will come *before* the ordered-by-func + // nodes. + + // These three columns write down { selfNode, deepNode, func } per + // non-inverted call node in the parent's range, but only for the nodes + // where the deep node has a parent. If the deep node does not have a + // parent, then it's not relevant for the inverted node's children, and its + // corresponding self node is stored in `selfNodesWhichEndHere`. + const unsortedCallNodesSelfNodeCol = []; + const unsortedCallNodesDeepNodeCol = []; + const unsortedCallNodesFuncCol = []; + + const selfNodesWhichEndHere = []; + + // Pass 1: Count the deep nodes per func, and build up a list of funcs. + // We will need to create a child for each deep node func, and each child will + // need to know how many deep nodes it has. + const deepNodeCountPerFunc = new Map(); + const callNodeTable = this._callNodeTable; + for (let i = 0; i < parentDeepNodeCount; i++) { + const selfNode = parentSelfNodes[i]; + const parentDeepNode = parentDeepNodes[i]; + const deepNode = callNodeTable.prefix[parentDeepNode]; + if (deepNode !== -1) { + const func = callNodeTable.func[deepNode]; + const previousCountForThisFunc = deepNodeCountPerFunc.get(func); + if (previousCountForThisFunc === undefined) { + deepNodeCountPerFunc.set(func, 1); + } else { + deepNodeCountPerFunc.set(func, previousCountForThisFunc + 1); + } + + unsortedCallNodesSelfNodeCol.push(selfNode); + unsortedCallNodesDeepNodeCol.push(deepNode); + unsortedCallNodesFuncCol.push(func); + } else { + selfNodesWhichEndHere.push(selfNode); + } + } + + const nodesWhichEndHereCount = selfNodesWhichEndHere.length; + const childrenDeepNodeCount = unsortedCallNodesDeepNodeCol.length; + if ( + nodesWhichEndHereCount + childrenDeepNodeCount !== + parentDeepNodeCount + ) { + throw new Error('indexes out of sync'); + } + + if (nodesWhichEndHereCount === parentDeepNodeCount) { + // All deep nodes ended at the parent's depth. The parent has no children. + // Also, the suffix order is already fully refined for the parent's range. + return null; + } + + // We create one child for each distinct func we found. We order the children + // by func so that _getChildWithFunc can use bisection. + const funcPerChild = new Uint32Array(deepNodeCountPerFunc.keys()); + funcPerChild.sort(); // Fast typed-array sort + const childCount = funcPerChild.length; + + // Pass 2: Using the counts in deepNodeCountPerFunc, reserve the right amount + // of slots in the sorted arrays, by computing accumulated start indexes in + // startIndexPerChild. + // These start indexes slice the range 0..childrenDeepNodeCount into + // partitions; one partition per child, in the right order. + const startIndexPerChild = new Uint32Array(childCount); + const deepNodeCountPerChild = new Uint32Array(childCount); + const funcToChildIndex = new Map(); + + let nextChildStartIndex = 0; + for (let childIndex = 0; childIndex < childCount; childIndex++) { + const func = funcPerChild[childIndex]; + funcToChildIndex.set(func, childIndex); + + const deepNodeCount = ensureExists(deepNodeCountPerFunc.get(func)); + deepNodeCountPerChild[childIndex] = deepNodeCount; + startIndexPerChild[childIndex] = nextChildStartIndex; + nextChildStartIndex += deepNodeCount; + } + + // Pass 3: Compute the ordered selfNode and deepNode arrays. + const nextIndexPerChild = startIndexPerChild; + const childrenDeepNodes = new Uint32Array(childrenDeepNodeCount); + const childrenSelfNodes = new Uint32Array(childrenDeepNodeCount); + for (let i = 0; i < childrenDeepNodeCount; i++) { + const func = unsortedCallNodesFuncCol[i]; + const childIndex = ensureExists(funcToChildIndex.get(func)); + + const selfNode = unsortedCallNodesSelfNodeCol[i]; + const deepNode = unsortedCallNodesDeepNodeCol[i]; + + const newIndex = nextIndexPerChild[childIndex]++; + childrenDeepNodes[newIndex] = deepNode; + childrenSelfNodes[newIndex] = selfNode; + } + + const childrenSuffixOrderIndexRangeStart = + parentIndexRangeStart + nodesWhichEndHereCount; + + return { + funcPerChild, + deepNodeCountPerChild, + childrenSuffixOrderIndexRangeStart, + selfNodesWhichEndAtParent: selfNodesWhichEndHere, + childrenSelfNodes, + childrenDeepNodes, + }; + } + + /** + * Within the suffix order index range of the given inverted node call node, + * replace the current suffixOrderedCallNodes with + * [...selfNodesWhichEndHere, ...selfNodesOrderedByDeepFunc]. + * Those must be the same nodes, just in a different order. + * + * This updates both this._suffixOrderedCallNodes and this._suffixOrderIndexes + * so that the two remain in sync. + * + * After this call, the suffix order will be accurate up to depth k + 1 for + * the given range, k being the depth of the inverted call node identified by + * nodeHandle. + * + * Preconditions: + * - All call nodes in the range must share the call path suffix which is + * represented by the inverted node `nodeHandle`; the length of this suffix + * is k + 1 (because nodeHandle's depth in the inverted tree is k). + * - selfNodesWhichEndHere must be the subset of call nodes in that range + * which do not have a (k + 1)'th parent. + * - selfNodesOrderedByDeepFunc must be the subset of call nodes which *do* + * have a (k + 1)'th parent, and they must be ordered by that parent's func. + */ + _applyRefinedSuffixOrderForNode( + nodeHandle: InvertedCallNodeHandle, + selfNodesWhichEndHere: IndexIntoCallNodeTable[], + selfNodesOrderedByDeepFunc: Uint32Array + ) { + const [suffixOrderIndexRangeStart, suffixOrderIndexRangeEnd] = + this.getSuffixOrderIndexRangeForCallNode(nodeHandle); + const suffixOrderIndexes = this._suffixOrderIndexes; + const suffixOrderedCallNodes = this._suffixOrderedCallNodes; + + let nextSuffixOrderIndex = suffixOrderIndexRangeStart; + for (let i = 0; i < selfNodesWhichEndHere.length; i++) { + const selfNode = selfNodesWhichEndHere[i]; + const orderIndex = nextSuffixOrderIndex++; + suffixOrderIndexes[selfNode] = orderIndex; + suffixOrderedCallNodes[orderIndex] = selfNode; + } + for (let i = 0; i < selfNodesOrderedByDeepFunc.length; i++) { + const selfNode = selfNodesOrderedByDeepFunc[i]; + const orderIndex = nextSuffixOrderIndex++; + suffixOrderIndexes[selfNode] = orderIndex; + suffixOrderedCallNodes[orderIndex] = selfNode; + } + + if (nextSuffixOrderIndex !== suffixOrderIndexRangeEnd) { + throw new Error('Indexes out of sync'); + } + } + + /** + * Create the children for parentNodeHandle based on the information in + * childrenInfo. + * + * Returns the handles of the created children. The returned array is ordered + * by func. + */ + _createChildrenForInfo( + childrenInfo: ChildrenInfo, + parentNodeHandle: InvertedCallNodeHandle + ): InvertedCallNodeHandle[] { + const parentNodeCallPathHash = this._pathHashForNode(parentNodeHandle); + const childrenDepth = this.depthForNode(parentNodeHandle) + 1; + + const { + funcPerChild, + deepNodeCountPerChild, + childrenSuffixOrderIndexRangeStart, + childrenDeepNodes, + } = childrenInfo; + + const childCount = funcPerChild.length; + const childCallNodes = []; + let nextChildDeepNodeIndex = 0; + let nextChildSuffixOrderIndexRangeStart = + childrenSuffixOrderIndexRangeStart; + for (let childIndex = 0; childIndex < childCount; childIndex++) { + const func = funcPerChild[childIndex]; + const deepNodeCount = deepNodeCountPerChild[childIndex]; + + const suffixOrderIndexRangeStart = nextChildSuffixOrderIndexRangeStart; + const childDeepNodes = childrenDeepNodes.subarray( + nextChildDeepNodeIndex, + nextChildDeepNodeIndex + deepNodeCount + ); + nextChildSuffixOrderIndexRangeStart += deepNodeCount; + nextChildDeepNodeIndex += deepNodeCount; + + const childHandle = this._createNonRootNode( + func, + childDeepNodes, + suffixOrderIndexRangeStart, + childrenDepth, + parentNodeHandle, + parentNodeCallPathHash + ); + childCallNodes.push(childHandle); + } + return childCallNodes; + } + + /** + * Create a single non-root node in this._invertedNonRootCallNodeTable and + * return its handle. + * + * All deepNodes have the same func, matching the func of this new inverted node. + * + * For all i in 0..deepNodes.length, deepNodes[i] is the k'th parent node + * of suffixOrderedCallNodes[suffixOrderIndexRangeStart + i] in the non-inverted tree, + * with k being the depth of the new inverted node. + */ + _createNonRootNode( + func: IndexIntoFuncTable, + deepNodes: Uint32Array, + suffixOrderIndexRangeStart: number, + depth: number, + parentNodeHandle: InvertedCallNodeHandle, + parentNodeCallPathHash: string + ): InvertedCallNodeHandle { + const deepNodeCount = deepNodes.length; + // assert(deepNodeCount > 0); + + const callNodeTable = this._callNodeTable; + + const firstDeepNode = deepNodes[0]; + let currentCategory = callNodeTable.category[firstDeepNode]; + let currentSubcategory = callNodeTable.subcategory[firstDeepNode]; + const currentInnerWindowID = callNodeTable.innerWindowID[firstDeepNode]; + let currentSourceFramesInlinedIntoSymbol = + callNodeTable.sourceFramesInlinedIntoSymbol[firstDeepNode]; + + const invertedNonRootCallNodeTable = this._invertedNonRootCallNodeTable; + for (let i = 1; i < deepNodeCount; i++) { + const deepNode = deepNodes[i]; + + // Resolve category conflicts, by resetting a conflicting subcategory or + // category to the default category. + if (currentCategory !== callNodeTable.category[deepNode]) { + // Conflicting origin stack categories -> default category + subcategory. + currentCategory = this._defaultCategory; + currentSubcategory = 0; + } else if (currentSubcategory !== callNodeTable.subcategory[deepNode]) { + // Conflicting origin stack subcategories -> "Other" subcategory. + currentSubcategory = 0; + } + + // Resolve "inlined into" conflicts. This can happen if you have two + // function calls A -> B where only one of the B calls is inlined, or + // if you use call tree transforms in such a way that a function B which + // was inlined into two different callers (A -> B, C -> B) gets collapsed + // into one call node. + if ( + currentSourceFramesInlinedIntoSymbol !== + callNodeTable.sourceFramesInlinedIntoSymbol[deepNode] + ) { + // Conflicting inlining: -1. + currentSourceFramesInlinedIntoSymbol = -1; + } + + // FIXME: Resolve conflicts of InnerWindowID + } + + const newIndex = invertedNonRootCallNodeTable.length++; + const newHandle = this._rootCount + newIndex; + + const pathHash = concatHash(parentNodeCallPathHash, func); + invertedNonRootCallNodeTable.prefix[newIndex] = parentNodeHandle; + invertedNonRootCallNodeTable.func[newIndex] = func; + invertedNonRootCallNodeTable.pathHash[newIndex] = pathHash; + invertedNonRootCallNodeTable.category[newIndex] = currentCategory; + invertedNonRootCallNodeTable.subcategory[newIndex] = currentSubcategory; + invertedNonRootCallNodeTable.innerWindowID[newIndex] = currentInnerWindowID; + invertedNonRootCallNodeTable.sourceFramesInlinedIntoSymbol[newIndex] = + currentSourceFramesInlinedIntoSymbol; + invertedNonRootCallNodeTable.deepNodes[newIndex] = deepNodes; + invertedNonRootCallNodeTable.suffixOrderIndexRangeStart[newIndex] = + suffixOrderIndexRangeStart; + invertedNonRootCallNodeTable.suffixOrderIndexRangeEnd[newIndex] = + suffixOrderIndexRangeStart + deepNodeCount; + invertedNonRootCallNodeTable.depth[newIndex] = depth; + + this._cache.set(pathHash, newHandle); + return newHandle; + } + + /** + * Returns the array of child node handles for the given inverted call node. + * The returned array of call node handles is sorted by func. + */ + getChildren(nodeIndex: InvertedCallNodeHandle): InvertedCallNodeHandle[] { + let childCallNodes = this._children.get(nodeIndex); + if (childCallNodes === undefined) { + childCallNodes = this._createChildren(nodeIndex); + this._children.set(nodeIndex, childCallNodes); + } + return childCallNodes; + } + + /** + * For an inverted call node whose children haven't been created yet, this + * returns the "deep nodes" corresponding to its suffix ordered call nodes. + * A deep node is the k'th parent node of a non-inverted call node, where k + * is the depth of the *inverted* call node. + */ + _takeDeepNodesForInvertedNode( + callNodeHandle: InvertedCallNodeHandle + ): Uint32Array { + if (callNodeHandle < this._rootCount) { + // This is a root. + // The "deep nodes" of a root are just the suffix ordered call nodes of the root. + // Going by the definition above, k == 0 (because the depth of the inverted + // call node is zero), and the 0'th parent of the non-inverted call nodes is + // just that node itself. + const [rangeStart, rangeEnd] = + this.getSuffixOrderIndexRangeForCallNode(callNodeHandle); + return this._suffixOrderedCallNodes.subarray(rangeStart, rangeEnd); + } + + // callNodeHandle is a non-root node. + const nonRootIndex: IndexIntoInvertedNonRootCallNodeTable = + callNodeHandle - this._rootCount; + const deepNodes = ensureExists( + this._invertedNonRootCallNodeTable.deepNodes[nonRootIndex], + '_takeDeepNodesForInvertedNode should only be called once for each node, and only after its parent created its children.' + ); + // Null it out the stored deep nodes, because we won't need them after this, + // and because their order may become out of sync after refinement. + this._invertedNonRootCallNodeTable.deepNodes[nonRootIndex] = null; + return deepNodes; + } + + // This function returns an inverted CallNodePath from a InvertedCallNodeHandle. + getCallNodePathFromIndex( + callNodeHandle: InvertedCallNodeHandle | null + ): CallNodePath { + if (callNodeHandle === null || callNodeHandle === -1) { + return []; + } + + const rootCount = this._rootCount; + const callNodePath = []; + let currentHandle = callNodeHandle; + while (currentHandle >= rootCount) { + const nonRootIndex = currentHandle - rootCount; + callNodePath.push(this._invertedNonRootCallNodeTable.func[nonRootIndex]); + currentHandle = this._invertedNonRootCallNodeTable.prefix[nonRootIndex]; + } + const rootFunc = currentHandle; + callNodePath.push(rootFunc); + callNodePath.reverse(); + return callNodePath; + } + + // Returns a CallNodeIndex from an inverted CallNodePath. + // + // This method will lazily populate new items in the table on demand, when + // necessary. + getCallNodeIndexFromPath( + callNodePath: CallNodePath + ): InvertedCallNodeHandle | null { + if (callNodePath.length === 0) { + return null; + } + + if (callNodePath.length === 1) { + return callNodePath[0]; // For roots, IndexIntoFuncTable === InvertedCallNodeHandle + } + + const pathDepth = callNodePath.length - 1; + + // Get the deepest ancestor already present in the inverted table. + let deepestExistingInvertedAncestorNode = + this._findDeepestExistingInvertedAncestorNode(callNodePath); + let deepestExistingInvertedAncestorNodeDepth = this.depthForNode( + deepestExistingInvertedAncestorNode + ); + + // Now create the necessary children until the end of the requested call node path. + while (deepestExistingInvertedAncestorNodeDepth < pathDepth) { + const currentChildFunc = + callNodePath[deepestExistingInvertedAncestorNodeDepth + 1]; + const children = this.getChildren(deepestExistingInvertedAncestorNode); + const childMatchingFunc = this._getChildWithFunc( + children, + currentChildFunc + ); + if (childMatchingFunc === null) { + // No child matches the func we were looking for. + // This can happen when the provided call path doesn't exist. In that case + // we return null. + return null; + } + deepestExistingInvertedAncestorNode = childMatchingFunc; + deepestExistingInvertedAncestorNodeDepth++; + } + return deepestExistingInvertedAncestorNode; + } + + // If the inverted call path `callPath` describes an existing inverted node, + // return its handle. Otherwise, the node for `callPath` doesn't exist yet, and + // we need to find an ancestor node for which we haven't called `_createChildren` + // yet. This ancestor is the "deepest existing" ancestor. That's the node which + // this function returns. + _findDeepestExistingInvertedAncestorNode( + callPath: CallNodePath + ): InvertedCallNodeHandle { + const completePathNode = this._cache.get(hashPath(callPath)); + if (completePathNode !== undefined) { + return completePathNode; + } + + // Find the depth of the deepest existing ancestor node using bisection. + // For each tested depth `currentDepth`, we create a partial inverted call + // path `callPath.slice(0, currentDepth + 1)` and check whether we have an + // inverted node for that partial call path. We find the largest value of + // `currentDepth` for which the partial call path refers to an existing node, + // and set `bestNode` to that node. + let bestNode = callPath[0]; + let remainingDepthRangeStart = 1; + let remainingDepthRangeEnd = callPath.length - 1; + while (remainingDepthRangeStart < remainingDepthRangeEnd) { + const currentDepth = + (remainingDepthRangeStart + remainingDepthRangeEnd) >> 1; + // assert(currentDepth < remainingDepthRangeEnd); + const currentPartialPath = callPath.slice(0, currentDepth + 1); + const currentNode = this._cache.get(hashPath(currentPartialPath)); + if (currentNode !== undefined) { + bestNode = currentNode; + remainingDepthRangeStart = currentDepth + 1; + } else { + remainingDepthRangeEnd = currentDepth; + } + } + return bestNode; + } + + // Return the element in `childrenSortedByFunc` whose func matches `func`, or + // null if no such element exist. + _getChildWithFunc( + childrenSortedByFunc: InvertedCallNodeHandle[], + func: IndexIntoFuncTable + ): InvertedCallNodeHandle | null { + // Use bisection to find the right child. This is valid because the caller + // promises that the array is sorted by func. + // As a reminder, the returned index is where the func would be inserted in + // the sorted array, at the right of potentially equal values. + const index = bisectionRightByKey(childrenSortedByFunc, func, (node) => + this.funcForNode(node) + ); + if (index === 0) { + return null; + } + + // If a child with our func is present in the array, it'll be left of the + // "insertion position", i.e. at childrenSortedByFunc[index - 1]. + const childNodeHandle = childrenSortedByFunc[index - 1]; + if (this.funcForNode(childNodeHandle) !== func) { + return null; + } + return childNodeHandle; + } + + // Returns the CallNodeIndex that matches the function `func` and whose parent's + // CallNodeIndex is `parent`. + getCallNodeIndexFromParentAndFunc( + parent: InvertedCallNodeHandle | -1, + func: IndexIntoFuncTable + ): InvertedCallNodeHandle | null { + if (parent === -1) { + return func; // For roots, IndexIntoFuncTable === InvertedCallNodeHandle + } + const children = this.getChildren(parent); + return this._getChildWithFunc(children, func); + } + + _pathHashForNode(callNodeHandle: InvertedCallNodeHandle): string { + if (callNodeHandle < this._rootCount) { + // callNodeHandle is a root, and for roots, InvertedCallNodeHandle === IndexIntoFuncTable. + return hashPathSingleFunc(callNodeHandle); + } + const nonRootIndex = callNodeHandle - this._rootCount; + return this._invertedNonRootCallNodeTable.pathHash[nonRootIndex]; + } + + prefixForNode( + callNodeHandle: InvertedCallNodeHandle + ): InvertedCallNodeHandle | -1 { + if (callNodeHandle < this._rootCount) { + // This is a root. + return -1; + } + const nonRootIndex = callNodeHandle - this._rootCount; + return this._invertedNonRootCallNodeTable.prefix[nonRootIndex]; + } + + funcForNode(callNodeHandle: InvertedCallNodeHandle): IndexIntoFuncTable { + if (callNodeHandle < this._rootCount) { + // This is a root. For roots, InvertedCallNodeHandle === IndexIntoFuncTable. + return callNodeHandle; + } + const nonRootIndex = callNodeHandle - this._rootCount; + return this._invertedNonRootCallNodeTable.func[nonRootIndex]; + } + + categoryForNode( + callNodeHandle: InvertedCallNodeHandle + ): IndexIntoCategoryList { + if (callNodeHandle < this._rootCount) { + const rootFunc = callNodeHandle; + return this._invertedRootCallNodeTable.category[rootFunc]; + } + const nonRootIndex = callNodeHandle - this._rootCount; + return this._invertedNonRootCallNodeTable.category[nonRootIndex]; + } + + subcategoryForNode( + callNodeHandle: InvertedCallNodeHandle + ): IndexIntoCategoryList { + if (callNodeHandle < this._rootCount) { + const rootFunc = callNodeHandle; + return this._invertedRootCallNodeTable.subcategory[rootFunc]; + } + const nonRootIndex = callNodeHandle - this._rootCount; + return this._invertedNonRootCallNodeTable.subcategory[nonRootIndex]; + } + + innerWindowIDForNode( + callNodeHandle: InvertedCallNodeHandle + ): IndexIntoCategoryList { + if (callNodeHandle < this._rootCount) { + const rootFunc = callNodeHandle; + return this._invertedRootCallNodeTable.innerWindowID[rootFunc]; + } + const nonRootIndex = callNodeHandle - this._rootCount; + return this._invertedNonRootCallNodeTable.innerWindowID[nonRootIndex]; + } + + depthForNode(callNodeHandle: InvertedCallNodeHandle): number { + if (callNodeHandle < this._rootCount) { + // Roots have depth 0. + return 0; + } + const nonRootIndex = callNodeHandle - this._rootCount; + return this._invertedNonRootCallNodeTable.depth[nonRootIndex]; + } + + sourceFramesInlinedIntoSymbolForNode( + callNodeHandle: InvertedCallNodeHandle + ): IndexIntoNativeSymbolTable | -1 | -2 { + if (callNodeHandle < this._rootCount) { + const rootFunc = callNodeHandle; + return this._invertedRootCallNodeTable.sourceFramesInlinedIntoSymbol[ + rootFunc + ]; + } + const nonRootIndex = callNodeHandle - this._rootCount; + return this._invertedNonRootCallNodeTable.sourceFramesInlinedIntoSymbol[ + nonRootIndex + ]; + } } diff --git a/src/profile-logic/call-tree.js b/src/profile-logic/call-tree.js index f4e10833eb..e05103e5ed 100644 --- a/src/profile-logic/call-tree.js +++ b/src/profile-logic/call-tree.js @@ -21,13 +21,12 @@ import type { CallNodeTable, CallNodePath, IndexIntoCallNodeTable, - CallNodeInfo, CallNodeData, CallNodeDisplayData, Milliseconds, ExtraBadgeInfo, BottomBoxInfo, - CallNodeLeafAndSummary, + CallNodeSelfAndSummary, SelfAndTotal, } from 'firefox-profiler/types'; @@ -36,16 +35,47 @@ import { formatCallNodeNumber, formatPercent } from '../utils/format-numbers'; import { assertExhaustiveCheck, ensureExists } from '../utils/flow'; import * as ProfileData from './profile-data'; import type { CallTreeSummaryStrategy } from '../types/actions'; +import type { CallNodeInfo, CallNodeInfoInverted } from './call-node-info'; type CallNodeChildren = IndexIntoCallNodeTable[]; -export type CallTreeTimings = { +export type CallTreeTimingsNonInverted = {| callNodeHasChildren: Uint8Array, self: Float32Array, - leaf: Float32Array, total: Float32Array, + rootTotalSummary: number, // sum of absolute values, this is used for computing percentages +|}; + +type TotalAndHasChildren = {| total: number, hasChildren: boolean |}; + +export type InvertedCallTreeRoot = {| + totalAndHasChildren: TotalAndHasChildren, + func: IndexIntoFuncTable, +|}; + +export type CallTreeTimingsInverted = {| + callNodeSelf: Float32Array, rootTotalSummary: number, -}; + sortedRoots: IndexIntoFuncTable[], + totalPerRootFunc: Float32Array, + hasChildrenPerRootFunc: Uint8Array, +|}; + +export type CallTreeTimings = + | {| type: 'NON_INVERTED', timings: CallTreeTimingsNonInverted |} + | {| type: 'INVERTED', timings: CallTreeTimingsInverted |}; + +/** + * Gets the CallTreeTimingsNonInverted out of a CallTreeTimings object. + */ +export function extractNonInvertedCallTreeTimings( + callTreeTimings: CallTreeTimings +): CallTreeTimingsNonInverted | null { + if (callTreeTimings.type === 'NON_INVERTED') { + return callTreeTimings.timings; + } + return null; +} function extractFaviconFromLibname(libname: string): string | null { try { @@ -74,15 +104,18 @@ interface CallTreeInternal { ): CallNodePath; } -export class CallTreeInternalImpl implements CallTreeInternal { +export class CallTreeInternalNonInverted implements CallTreeInternal { _callNodeInfo: CallNodeInfo; _callNodeTable: CallNodeTable; - _callTreeTimings: CallTreeTimings; + _callTreeTimings: CallTreeTimingsNonInverted; _callNodeHasChildren: Uint8Array; // A table column matching the callNodeTable - constructor(callNodeInfo: CallNodeInfo, callTreeTimings: CallTreeTimings) { + constructor( + callNodeInfo: CallNodeInfo, + callTreeTimings: CallTreeTimingsNonInverted + ) { this._callNodeInfo = callNodeInfo; - this._callNodeTable = callNodeInfo.getCallNodeTable(); + this._callNodeTable = callNodeInfo.getNonInvertedCallNodeTable(); this._callTreeTimings = callTreeTimings; this._callNodeHasChildren = callTreeTimings.callNodeHasChildren; } @@ -142,14 +175,14 @@ export class CallTreeInternalImpl implements CallTreeInternal { ): CallNodePath { const rangeEnd = this._callNodeTable.subtreeRangeEnd[callNodeIndex]; - // Find the call node with the highest leaf time. + // Find the call node with the highest self time. let maxNode = -1; let maxAbs = 0; for (let nodeIndex = callNodeIndex; nodeIndex < rangeEnd; nodeIndex++) { - const nodeLeaf = Math.abs(this._callTreeTimings.leaf[nodeIndex]); - if (maxNode === -1 || nodeLeaf > maxAbs) { + const nodeSelf = Math.abs(this._callTreeTimings.self[nodeIndex]); + if (maxNode === -1 || nodeSelf > maxAbs) { maxNode = nodeIndex; - maxAbs = nodeLeaf; + maxAbs = nodeSelf; } } @@ -157,11 +190,131 @@ export class CallTreeInternalImpl implements CallTreeInternal { } } +class CallTreeInternalInverted implements CallTreeInternal { + _callNodeInfo: CallNodeInfoInverted; + _nonInvertedCallNodeTable: CallNodeTable; + _callNodeSelf: Float32Array; + _rootNodes: IndexIntoCallNodeTable[]; + _funcCount: number; + _totalPerRootFunc: Float32Array; + _hasChildrenPerRootFunc: Uint8Array; + _totalAndHasChildrenPerNonRootNode: Map< + IndexIntoCallNodeTable, + TotalAndHasChildren, + > = new Map(); + + constructor( + callNodeInfo: CallNodeInfoInverted, + callTreeTimingsInverted: CallTreeTimingsInverted + ) { + this._callNodeInfo = callNodeInfo; + this._nonInvertedCallNodeTable = callNodeInfo.getNonInvertedCallNodeTable(); + this._callNodeSelf = callTreeTimingsInverted.callNodeSelf; + const { sortedRoots, totalPerRootFunc, hasChildrenPerRootFunc } = + callTreeTimingsInverted; + this._totalPerRootFunc = totalPerRootFunc; + this._hasChildrenPerRootFunc = hasChildrenPerRootFunc; + this._rootNodes = sortedRoots; + } + + createRoots(): IndexIntoCallNodeTable[] { + return this._rootNodes; + } + + hasChildren(callNodeIndex: IndexIntoCallNodeTable): boolean { + if (this._callNodeInfo.isRoot(callNodeIndex)) { + return this._hasChildrenPerRootFunc[callNodeIndex] !== 0; + } + return this._getTotalAndHasChildren(callNodeIndex).hasChildren; + } + + createChildren(nodeIndex: IndexIntoCallNodeTable): CallNodeChildren { + if (!this.hasChildren(nodeIndex)) { + return []; + } + + const children = this._callNodeInfo + .getChildren(nodeIndex) + .filter((child) => { + const { total, hasChildren } = this._getTotalAndHasChildren(child); + return total !== 0 || hasChildren; + }); + children.sort( + (a, b) => + Math.abs(this._getTotalAndHasChildren(b).total) - + Math.abs(this._getTotalAndHasChildren(a).total) + ); + return children; + } + + getSelfAndTotal(callNodeIndex: IndexIntoCallNodeTable): SelfAndTotal { + if (this._callNodeInfo.isRoot(callNodeIndex)) { + const total = this._totalPerRootFunc[callNodeIndex]; + return { self: total, total }; + } + const { total } = this._getTotalAndHasChildren(callNodeIndex); + return { self: 0, total }; + } + + _getTotalAndHasChildren( + callNodeIndex: IndexIntoCallNodeTable + ): TotalAndHasChildren { + if (this._callNodeInfo.isRoot(callNodeIndex)) { + throw new Error('This function should not be called for roots'); + } + + const cached = this._totalAndHasChildrenPerNonRootNode.get(callNodeIndex); + if (cached !== undefined) { + return cached; + } + + const totalAndHasChildren = _getInvertedTreeNodeTotalAndHasChildren( + callNodeIndex, + this._callNodeInfo, + this._callNodeSelf + ); + this._totalAndHasChildrenPerNonRootNode.set( + callNodeIndex, + totalAndHasChildren + ); + return totalAndHasChildren; + } + + findHeaviestPathInSubtree( + callNodeIndex: IndexIntoCallNodeTable + ): CallNodePath { + const [rangeStart, rangeEnd] = + this._callNodeInfo.getSuffixOrderIndexRangeForCallNode(callNodeIndex); + const orderedCallNodes = this._callNodeInfo.getSuffixOrderedCallNodes(); + + // Find the non-inverted node with the highest self time. + let maxNode = -1; + let maxAbs = 0; + for (let i = rangeStart; i < rangeEnd; i++) { + const nodeIndex = orderedCallNodes[i]; + const nodeSelf = Math.abs(this._callNodeSelf[nodeIndex]); + if (maxNode === -1 || nodeSelf > maxAbs) { + maxNode = nodeIndex; + maxAbs = nodeSelf; + } + } + + const callPath = []; + for ( + let currentNode = maxNode; + currentNode !== -1; + currentNode = this._nonInvertedCallNodeTable.prefix[currentNode] + ) { + callPath.push(this._nonInvertedCallNodeTable.func[currentNode]); + } + return callPath; + } +} + export class CallTree { _categories: CategoryList; _internal: CallTreeInternal; _callNodeInfo: CallNodeInfo; - _callNodeTable: CallNodeTable; _thread: Thread; _rootTotalSummary: number; _displayDataByIndex: Map; @@ -184,7 +337,6 @@ export class CallTree { this._categories = categories; this._internal = internal; this._callNodeInfo = callNodeInfo; - this._callNodeTable = callNodeInfo.getCallNodeTable(); this._thread = thread; this._rootTotalSummary = rootTotalSummary; this._displayDataByIndex = new Map(); @@ -232,15 +384,15 @@ export class CallTree { getParent( callNodeIndex: IndexIntoCallNodeTable ): IndexIntoCallNodeTable | -1 { - return this._callNodeTable.prefix[callNodeIndex]; + return this._callNodeInfo.prefixForNode(callNodeIndex); } getDepth(callNodeIndex: IndexIntoCallNodeTable): number { - return this._callNodeTable.depth[callNodeIndex]; + return this._callNodeInfo.depthForNode(callNodeIndex); } getNodeData(callNodeIndex: IndexIntoCallNodeTable): CallNodeData { - const funcIndex = this._callNodeTable.func[callNodeIndex]; + const funcIndex = this._callNodeInfo.funcForNode(callNodeIndex); const funcName = this._thread.stringTable.getString( this._thread.funcTable.name[funcIndex] ); @@ -264,8 +416,8 @@ export class CallTree { ): ExtraBadgeInfo | void { const calledFunction = getFunctionName(funcName); const inlinedIntoNativeSymbol = - this._callNodeTable.sourceFramesInlinedIntoSymbol[callNodeIndex]; - if (inlinedIntoNativeSymbol === null) { + this._callNodeInfo.sourceFramesInlinedIntoSymbolForNode(callNodeIndex); + if (inlinedIntoNativeSymbol === -2) { return undefined; } @@ -299,9 +451,10 @@ export class CallTree { if (displayData === undefined) { const { funcName, total, totalRelative, self } = this.getNodeData(callNodeIndex); - const funcIndex = this._callNodeTable.func[callNodeIndex]; - const categoryIndex = this._callNodeTable.category[callNodeIndex]; - const subcategoryIndex = this._callNodeTable.subcategory[callNodeIndex]; + const funcIndex = this._callNodeInfo.funcForNode(callNodeIndex); + const categoryIndex = this._callNodeInfo.categoryForNode(callNodeIndex); + const subcategoryIndex = + this._callNodeInfo.subcategoryForNode(callNodeIndex); const badge = this._getInliningBadge(callNodeIndex, funcName); const resourceIndex = this._thread.funcTable.resource[funcIndex]; const resourceType = this._thread.resourceTable.type[resourceIndex]; @@ -424,14 +577,14 @@ export class CallTree { } /** - * Take a CallNodeIndex, and compute an inverted path for it. + * Take a IndexIntoCallNodeTable, and compute an inverted path for it. * * e.g: * (invertedPath, invertedCallTree) => path * (path, callTree) => invertedPath * * Call trees are sorted with the CallNodes with the heaviest total time as the first - * entry. This function walks to the tip of the heaviest branches to find the leaf node, + * entry. This function walks to the tip of the heaviest branches to find the self node, * then construct an inverted CallNodePath with the result. This gives a pretty decent * result, but it doesn't guarantee that it will select the heaviest CallNodePath for the * INVERTED call tree. This would require doing a round trip through the reducers or @@ -447,59 +600,22 @@ export class CallTree { } const heaviestPath = this._internal.findHeaviestPathInSubtree(callNodeIndex); - const startingDepth = this._callNodeTable.depth[callNodeIndex]; + const startingDepth = this._callNodeInfo.depthForNode(callNodeIndex); const partialPath = heaviestPath.slice(startingDepth); return partialPath.reverse(); } } -// In an inverted profile, all the amount of self unit (time, bytes, count, etc.) is -// accounted to the root nodes. So `callNodeSelf` will be 0 for all non-root nodes. -function _getInvertedCallNodeSelf( - callNodeLeaf: Float32Array, - callNodeTable: CallNodeTable -): Float32Array { - // Compute an array that maps the callNodeIndex to its root. - const callNodeToRoot = new Int32Array(callNodeTable.length); - - // Compute the self time during the same loop. - const callNodeSelf = new Float32Array(callNodeTable.length); - - for ( - let callNodeIndex = 0; - callNodeIndex < callNodeTable.length; - callNodeIndex++ - ) { - const prefixCallNode = callNodeTable.prefix[callNodeIndex]; - if (prefixCallNode === -1) { - // callNodeIndex is a root node - callNodeToRoot[callNodeIndex] = callNodeIndex; - } else { - // The callNodeTable guarantees that a callNode's prefix always comes - // before the callNode; prefix references are always to lower callNode - // indexes and never to higher indexes. - // We are iterating the callNodeTable in forwards direction (starting at - // index 0) so we know that we have already visited the current call - // node's prefix call node and can reuse its stored root node, which - // recursively is the value we're looking for. - callNodeToRoot[callNodeIndex] = callNodeToRoot[prefixCallNode]; - } - callNodeSelf[callNodeToRoot[callNodeIndex]] += callNodeLeaf[callNodeIndex]; - } - - return callNodeSelf; -} - /** - * Compute the leaf time for each call node, and the sum of the absolute leaf + * Compute the self time for each call node, and the sum of the absolute self * values. */ -export function computeCallNodeLeafAndSummary( +export function computeCallNodeSelfAndSummary( samples: SamplesLikeTable, sampleIndexToCallNodeIndex: Array, callNodeCount: number -): CallNodeLeafAndSummary { - const callNodeLeaf = new Float32Array(callNodeCount); +): CallNodeSelfAndSummary { + const callNodeSelf = new Float32Array(callNodeCount); for ( let sampleIndex = 0; sampleIndex < sampleIndexToCallNodeIndex.length; @@ -508,7 +624,7 @@ export function computeCallNodeLeafAndSummary( const callNodeIndex = sampleIndexToCallNodeIndex[sampleIndex]; if (callNodeIndex !== null) { const weight = samples.weight ? samples.weight[sampleIndex] : 1; - callNodeLeaf[callNodeIndex] += weight; + callNodeSelf[callNodeIndex] += weight; } } @@ -517,28 +633,168 @@ export function computeCallNodeLeafAndSummary( let rootTotalSummary = 0; for (let callNodeIndex = 0; callNodeIndex < callNodeCount; callNodeIndex++) { - rootTotalSummary += abs(callNodeLeaf[callNodeIndex]); + rootTotalSummary += abs(callNodeSelf[callNodeIndex]); } - return { callNodeLeaf, rootTotalSummary }; + return { callNodeSelf, rootTotalSummary }; +} + +export function getSelfAndTotalForCallNode( + callNodeIndex: IndexIntoCallNodeTable, + callNodeInfo: CallNodeInfo, + callTreeTimings: CallTreeTimings +): SelfAndTotal { + switch (callTreeTimings.type) { + case 'NON_INVERTED': { + const { timings } = callTreeTimings; + const self = timings.self[callNodeIndex]; + const total = timings.total[callNodeIndex]; + return { self, total }; + } + case 'INVERTED': { + const callNodeInfoInverted = ensureExists(callNodeInfo.asInverted()); + const { timings } = callTreeTimings; + const { callNodeSelf, totalPerRootFunc } = timings; + if (callNodeInfoInverted.isRoot(callNodeIndex)) { + const total = totalPerRootFunc[callNodeIndex]; + return { self: total, total }; + } + const { total } = _getInvertedTreeNodeTotalAndHasChildren( + callNodeIndex, + callNodeInfoInverted, + callNodeSelf + ); + return { self: 0, total }; + } + default: + throw assertExhaustiveCheck(callTreeTimings.type); + } +} + +function _getInvertedTreeNodeTotalAndHasChildren( + callNodeIndex: IndexIntoCallNodeTable, + callNodeInfo: CallNodeInfoInverted, + callNodeSelf: Float32Array +): TotalAndHasChildren { + const nodeDepth = callNodeInfo.depthForNode(callNodeIndex); + const [rangeStart, rangeEnd] = + callNodeInfo.getSuffixOrderIndexRangeForCallNode(callNodeIndex); + const suffixOrderedCallNodes = callNodeInfo.getSuffixOrderedCallNodes(); + const callNodeTableDepthCol = + callNodeInfo.getNonInvertedCallNodeTable().depth; + + // Warning: This function can be quite confusing. That's because we are dealing + // with both inverted call nodes and non-inverted call nodes. + // `callNodeIndex` is a node in the *inverted* tree. + // The suffixOrderedCallNodes we iterate over below are nodes in the + // *non-inverted* tree. + // The total time of a node in the inverted tree is the sum of the self times + // of all the non-inverted nodes that contribute to the inverted node. + + let total = 0; + let hasChildren = false; + for (let i = rangeStart; i < rangeEnd; i++) { + const selfNode = suffixOrderedCallNodes[i]; + const self = callNodeSelf[selfNode]; + total += self; + + // The inverted call node has children if it has any inverted child nodes + // with non-zero total time. The total time of such an inverted child node + // is the sum of the self times of the non-inverted call nodes which + // contribute to it. Does `selfNode` contribute to one of our children? + // Maybe. To do so, it would need to describe a call path whose length is at + // least as long as the inverted call paths of our children - if not, it only + // contributes to `callNodeIndex` and not to our children. + // Rather than comparing the length of the call paths, we can just compare + // the depths. + // + // In other words: + // The inverted call node has children if any deeper call paths with non-zero + // self time contribute to it. + hasChildren = + hasChildren || + (self !== 0 && callNodeTableDepthCol[selfNode] > nodeDepth); + } + return { total, hasChildren }; +} + +export function computeCallTreeTimingsInverted( + callNodeInfo: CallNodeInfoInverted, + { callNodeSelf, rootTotalSummary }: CallNodeSelfAndSummary +): CallTreeTimingsInverted { + const funcCount = callNodeInfo.getFuncCount(); + const callNodeTable = callNodeInfo.getNonInvertedCallNodeTable(); + const callNodeTableFuncCol = callNodeTable.func; + const callNodeTableDepthCol = callNodeTable.depth; + const totalPerRootFunc = new Float32Array(funcCount); + const hasChildrenPerRootFunc = new Uint8Array(funcCount); + const seenPerRootFunc = new Uint8Array(funcCount); + const sortedRoots = []; + for (let i = 0; i < callNodeSelf.length; i++) { + const self = callNodeSelf[i]; + if (self === 0) { + continue; + } + + // Map the non-inverted call node to its corresponding root in the inverted + // call tree. This is done by finding the inverted root which corresponds to + // the self function of the non-inverted call node. + const func = callNodeTableFuncCol[i]; + + totalPerRootFunc[func] += self; + if (seenPerRootFunc[func] === 0) { + seenPerRootFunc[func] = 1; + sortedRoots.push(func); + } + if (callNodeTableDepthCol[i] !== 0) { + hasChildrenPerRootFunc[func] = 1; + } + } + sortedRoots.sort( + (a, b) => Math.abs(totalPerRootFunc[b]) - Math.abs(totalPerRootFunc[a]) + ); + return { + callNodeSelf, + rootTotalSummary, + sortedRoots, + totalPerRootFunc, + hasChildrenPerRootFunc, + }; +} + +export function computeCallTreeTimings( + callNodeInfo: CallNodeInfo, + CallNodeSelfAndSummary: CallNodeSelfAndSummary +): CallTreeTimings { + const callNodeInfoInverted = callNodeInfo.asInverted(); + if (callNodeInfoInverted !== null) { + return { + type: 'INVERTED', + timings: computeCallTreeTimingsInverted( + callNodeInfoInverted, + CallNodeSelfAndSummary + ), + }; + } + return { + type: 'NON_INVERTED', + timings: computeCallTreeTimingsNonInverted( + callNodeInfo, + CallNodeSelfAndSummary + ), + }; } /** * This computes all of the count and timing information displayed in the calltree. * It takes into account both the normal tree, and the inverted tree. */ -export function computeCallTreeTimings( +export function computeCallTreeTimingsNonInverted( callNodeInfo: CallNodeInfo, - callNodeLeafAndSummary: CallNodeLeafAndSummary -): CallTreeTimings { - const callNodeTable = callNodeInfo.getCallNodeTable(); - const { callNodeLeaf, rootTotalSummary } = callNodeLeafAndSummary; - - // The self values depend on whether the call tree is inverted: In an inverted - // tree, all the self time is in the roots. - const callNodeSelf = callNodeInfo.isInverted() - ? _getInvertedCallNodeSelf(callNodeLeaf, callNodeTable) - : callNodeLeaf; + CallNodeSelfAndSummary: CallNodeSelfAndSummary +): CallTreeTimingsNonInverted { + const callNodeTable = callNodeInfo.getNonInvertedCallNodeTable(); + const { callNodeSelf, rootTotalSummary } = CallNodeSelfAndSummary; // Compute the following variables: const callNodeTotalSummary = new Float32Array(callNodeTable.length); @@ -552,7 +808,7 @@ export function computeCallTreeTimings( callNodeIndex >= 0; callNodeIndex-- ) { - callNodeTotalSummary[callNodeIndex] += callNodeLeaf[callNodeIndex]; + callNodeTotalSummary[callNodeIndex] += callNodeSelf[callNodeIndex]; const hasChildren = callNodeHasChildren[callNodeIndex] !== 0; const hasTotalValue = callNodeTotalSummary[callNodeIndex] !== 0; @@ -570,7 +826,6 @@ export function computeCallTreeTimings( return { self: callNodeSelf, - leaf: callNodeLeaf, total: callNodeTotalSummary, callNodeHasChildren, rootTotalSummary, @@ -588,15 +843,40 @@ export function getCallTree( weightType: WeightType ): CallTree { return timeCode('getCallTree', () => { - return new CallTree( - thread, - categories, - callNodeInfo, - new CallTreeInternalImpl(callNodeInfo, callTreeTimings), - callTreeTimings.rootTotalSummary, - Boolean(thread.isJsTracer), - weightType - ); + switch (callTreeTimings.type) { + case 'NON_INVERTED': { + const { timings } = callTreeTimings; + return new CallTree( + thread, + categories, + callNodeInfo, + new CallTreeInternalNonInverted(callNodeInfo, timings), + timings.rootTotalSummary, + Boolean(thread.isJsTracer), + weightType + ); + } + case 'INVERTED': { + const { timings } = callTreeTimings; + return new CallTree( + thread, + categories, + callNodeInfo, + new CallTreeInternalInverted( + ensureExists(callNodeInfo.asInverted()), + timings + ), + timings.rootTotalSummary, + Boolean(thread.isJsTracer), + weightType + ); + } + default: + throw assertExhaustiveCheck( + callTreeTimings.type, + 'Unhandled CallTreeTimings type.' + ); + } }); } @@ -713,7 +993,7 @@ export function extractUnfilteredSamplesLikeTable( } /** - * This function is extremely similar to computeCallNodeLeafAndSummary, + * This function is extremely similar to computeCallNodeSelfAndSummary, * but is specialized for converting sample counts into traced timing. Samples * don't have duration information associated with them, it's mostly how long they * were observed to be running. This function computes the timing the exact same @@ -723,12 +1003,12 @@ export function extractUnfilteredSamplesLikeTable( * did not agree. In order to remove confusion, we can show the sample counts, * plus the traced timing, which is a compromise between correctness, and consistency. */ -export function computeCallNodeTracedLeafAndSummary( +export function computeCallNodeTracedSelfAndSummary( samples: SamplesLikeTable, sampleIndexToCallNodeIndex: Array, callNodeCount: number, interval: Milliseconds -): CallNodeLeafAndSummary | null { +): CallNodeSelfAndSummary | null { if (samples.weightType !== 'samples' || samples.weight) { // Only compute for the samples weight types that have no weights. If a samples // table has weights then it's a diff profile. Currently, we aren't calculating @@ -740,7 +1020,7 @@ export function computeCallNodeTracedLeafAndSummary( return null; } - const callNodeLeaf = new Float32Array(callNodeCount); + const callNodeSelf = new Float32Array(callNodeCount); let rootTotalSummary = 0; for (let sampleIndex = 0; sampleIndex < samples.length - 1; sampleIndex++) { @@ -748,7 +1028,7 @@ export function computeCallNodeTracedLeafAndSummary( if (callNodeIndex !== null) { const sampleTracedTime = samples.time[sampleIndex + 1] - samples.time[sampleIndex]; - callNodeLeaf[callNodeIndex] += sampleTracedTime; + callNodeSelf[callNodeIndex] += sampleTracedTime; rootTotalSummary += sampleTracedTime; } } @@ -758,10 +1038,10 @@ export function computeCallNodeTracedLeafAndSummary( if (callNodeIndex !== null) { // Use the sampling interval for the last sample. const sampleTracedTime = interval; - callNodeLeaf[callNodeIndex] += sampleTracedTime; + callNodeSelf[callNodeIndex] += sampleTracedTime; rootTotalSummary += sampleTracedTime; } } - return { callNodeLeaf, rootTotalSummary }; + return { callNodeSelf, rootTotalSummary }; } diff --git a/src/profile-logic/data-structures.js b/src/profile-logic/data-structures.js index caf8d989a5..53bcaf2e81 100644 --- a/src/profile-logic/data-structures.js +++ b/src/profile-logic/data-structures.js @@ -455,7 +455,7 @@ export function getEmptyCallNodeTable(): CallNodeTable { category: new Int32Array(0), subcategory: new Int32Array(0), innerWindowID: new Float64Array(0), - sourceFramesInlinedIntoSymbol: [], + sourceFramesInlinedIntoSymbol: new Int32Array(0), depth: [], maxDepth: -1, length: 0, diff --git a/src/profile-logic/flame-graph.js b/src/profile-logic/flame-graph.js index 14d221289f..6d7cb751ee 100644 --- a/src/profile-logic/flame-graph.js +++ b/src/profile-logic/flame-graph.js @@ -10,7 +10,7 @@ import type { IndexIntoCallNodeTable, } from 'firefox-profiler/types'; import type { StringTable } from 'firefox-profiler/utils/string-table'; -import type { CallTreeTimings } from './call-tree'; +import type { CallTreeTimingsNonInverted } from './call-tree'; import { bisectionRightByStrKey } from 'firefox-profiler/utils/bisect'; @@ -229,7 +229,7 @@ export function computeFlameGraphRows( export function getFlameGraphTiming( flameGraphRows: FlameGraphRows, callNodeTable: CallNodeTable, - callTreeTimings: CallTreeTimings + callTreeTimings: CallTreeTimingsNonInverted ): FlameGraphTiming { const { total, self, rootTotalSummary } = callTreeTimings; const { prefix } = callNodeTable; diff --git a/src/profile-logic/line-timings.js b/src/profile-logic/line-timings.js index 0d0065f25c..eac106e0af 100644 --- a/src/profile-logic/line-timings.js +++ b/src/profile-logic/line-timings.js @@ -9,7 +9,6 @@ import type { FuncTable, StackTable, SamplesLikeTable, - CallNodeInfo, IndexIntoCallNodeTable, IndexIntoStringTable, StackLineInfo, @@ -18,6 +17,7 @@ import type { } from 'firefox-profiler/types'; import { getMatchingAncestorStackForInvertedCallNode } from './profile-data'; +import type { CallNodeInfo, CallNodeInfoInverted } from './call-node-info'; /** * For each stack in `stackTable`, and one specific source file, compute the @@ -125,12 +125,13 @@ export function getStackLineInfoForCallNode( callNodeIndex: IndexIntoCallNodeTable, callNodeInfo: CallNodeInfo ): StackLineInfo { - return callNodeInfo.isInverted() + const callNodeInfoInverted = callNodeInfo.asInverted(); + return callNodeInfoInverted !== null ? getStackLineInfoForCallNodeInverted( stackTable, frameTable, callNodeIndex, - callNodeInfo + callNodeInfoInverted ) : getStackLineInfoForCallNodeNonInverted( stackTable, @@ -282,15 +283,16 @@ export function getStackLineInfoForCallNodeInverted( stackTable: StackTable, frameTable: FrameTable, callNodeIndex: IndexIntoCallNodeTable, - callNodeInfo: CallNodeInfo + callNodeInfo: CallNodeInfoInverted ): StackLineInfo { - const invertedCallNodeTable = callNodeInfo.getCallNodeTable(); - const depth = invertedCallNodeTable.depth[callNodeIndex]; - const endIndex = invertedCallNodeTable.subtreeRangeEnd[callNodeIndex]; - const callNodeIsRootOfInvertedTree = - invertedCallNodeTable.prefix[callNodeIndex] === -1; - const stackIndexToCallNodeIndex = callNodeInfo.getStackIndexToCallNodeIndex(); + const depth = callNodeInfo.depthForNode(callNodeIndex); + const [rangeStart, rangeEnd] = + callNodeInfo.getSuffixOrderIndexRangeForCallNode(callNodeIndex); + const callNodeIsRootOfInvertedTree = callNodeInfo.isRoot(callNodeIndex); + const stackIndexToCallNodeIndex = + callNodeInfo.getStackIndexToNonInvertedCallNodeIndex(); const stackTablePrefixCol = stackTable.prefix; + const suffixOrderIndexes = callNodeInfo.getSuffixOrderIndexes(); // "self line" == "the line which a stack's self time is contributed to" const callNodeSelfLineForAllStacks = []; @@ -304,8 +306,9 @@ export function getStackLineInfoForCallNodeInverted( const stackForCallNode = getMatchingAncestorStackForInvertedCallNode( stackIndex, - callNodeIndex, - endIndex, + rangeStart, + rangeEnd, + suffixOrderIndexes, depth, stackIndexToCallNodeIndex, stackTablePrefixCol diff --git a/src/profile-logic/profile-data.js b/src/profile-logic/profile-data.js index 002b3d730a..2dbe0e794e 100644 --- a/src/profile-logic/profile-data.js +++ b/src/profile-logic/profile-data.js @@ -14,7 +14,10 @@ import { shallowCloneFrameTable, shallowCloneFuncTable, } from './data-structures'; -import { CallNodeInfoImpl } from './call-node-info'; +import { + CallNodeInfoNonInverted, + CallNodeInfoInverted, +} from './call-node-info'; import { computeThreadCPURatio } from './cpu'; import { INSTANT, @@ -70,7 +73,6 @@ import type { BalancedNativeAllocationsTable, IndexIntoFrameTable, PageList, - CallNodeInfo, CallNodeTable, CallNodePath, CallNodeAndCategoryPath, @@ -96,6 +98,7 @@ import type { ThreadWithReservedFunctions, TabID, } from 'firefox-profiler/types'; +import type { CallNodeInfo, SuffixOrderIndex } from './call-node-info'; /** * Various helpers for dealing with the profile as a data structure. @@ -117,13 +120,7 @@ export function getCallNodeInfo( funcTable, defaultCategory ); - return new CallNodeInfoImpl( - callNodeTable, - callNodeTable, - stackIndexToCallNodeIndex, - stackIndexToCallNodeIndex, - false - ); + return new CallNodeInfoNonInverted(callNodeTable, stackIndexToCallNodeIndex); } type CallNodeTableAndStackMap = { @@ -158,7 +155,7 @@ export function computeCallNodeTable( const subcategory: Array = []; const innerWindowID: Array = []; const sourceFramesInlinedIntoSymbol: Array< - IndexIntoNativeSymbolTable | -1 | null, + IndexIntoNativeSymbolTable | -1 | -2, > = []; let length = 0; @@ -178,7 +175,7 @@ export function computeCallNodeTable( categoryIndex: IndexIntoCategoryList, subcategoryIndex: IndexIntoSubcategoryListForCategory, windowID: InnerWindowID, - inlinedIntoSymbol: IndexIntoNativeSymbolTable | null + inlinedIntoSymbol: IndexIntoNativeSymbolTable | -1 | -2 ) { const index = length++; prefix[index] = prefixIndex; @@ -231,8 +228,8 @@ export function computeCallNodeTable( const subcategoryIndex = stackTable.subcategory[stackIndex]; const inlinedIntoSymbol = frameTable.inlineDepth[frameIndex] > 0 - ? frameTable.nativeSymbol[frameIndex] - : null; + ? (frameTable.nativeSymbol[frameIndex] ?? -2) + : -2; const funcIndex = frameTable.func[frameIndex]; // Check if the call node for this stack already exists. @@ -328,7 +325,7 @@ function _createCallNodeTableFromUnorderedComponents( category: Array, subcategory: Array, innerWindowID: Array, - sourceFramesInlinedIntoSymbol: Array, + sourceFramesInlinedIntoSymbol: Array, length: number, stackIndexToCallNodeIndex: Int32Array ): CallNodeTableAndStackMap { @@ -347,7 +344,7 @@ function _createCallNodeTableFromUnorderedComponents( const categorySorted = new Int32Array(length); const subcategorySorted = new Int32Array(length); const innerWindowIDSorted = new Float64Array(length); - const sourceFramesInlinedIntoSymbolSorted = new Array(length); + const sourceFramesInlinedIntoSymbolSorted = new Int32Array(length); const depthSorted = new Array(length); let maxDepth = 0; @@ -440,59 +437,57 @@ function _createCallNodeTableFromUnorderedComponents( * Generate the inverted CallNodeInfo for a thread. */ export function getInvertedCallNodeInfo( - thread: Thread, nonInvertedCallNodeTable: CallNodeTable, stackIndexToNonInvertedCallNodeIndex: Int32Array, - defaultCategory: IndexIntoCategoryList -): CallNodeInfo { - // We compute an inverted stack table, but we don't let it escape this function. - const { - invertedThread, - oldStackToNewStack: nonInvertedStackToInvertedStack, - } = _computeThreadWithInvertedStackTable(thread, defaultCategory); - - // Create an inverted call node table based on the inverted stack table. - const { - callNodeTable, - stackIndexToCallNodeIndex: invertedStackIndexToCallNodeIndex, - } = computeCallNodeTable( - invertedThread.stackTable, - invertedThread.frameTable, - invertedThread.funcTable, - defaultCategory + defaultCategory: IndexIntoCategoryList, + funcCount: number +): CallNodeInfoInverted { + return new CallNodeInfoInverted( + nonInvertedCallNodeTable, + stackIndexToNonInvertedCallNodeIndex, + defaultCategory, + funcCount ); +} - // Create a mapping that maps a stack index from the non-inverted thread to - // its corresponding call node in the inverted tree. - const nonInvertedStackIndexToCallNodeIndex = new Int32Array( - thread.stackTable.length - ); - for ( - let nonInvertedStackIndex = 0; - nonInvertedStackIndex < nonInvertedStackIndexToCallNodeIndex.length; - nonInvertedStackIndex++ - ) { - const invertedStackIndex = nonInvertedStackToInvertedStack.get( - nonInvertedStackIndex - ); - if (invertedStackIndex === undefined) { - // This stack is not used as a self stack, only as a prefix stack. - // There may or may not be an inverted call node that corresponds to it, - // but we haven't checked that and we don't need to know it. - // nonInvertedStackIndexToCallNodeIndex only needs useful values for self stacks. - nonInvertedStackIndexToCallNodeIndex[nonInvertedStackIndex] = -1; - } else { - nonInvertedStackIndexToCallNodeIndex[nonInvertedStackIndex] = - invertedStackIndexToCallNodeIndex[invertedStackIndex]; +// Compare two non-inverted call nodes in "suffix order". +// The suffix order is defined as the lexicographical order of the inverted call +// path, or, in other words, the "backwards" lexicographical order of the +// non-inverted call paths. +// +// Example of some suffix ordered non-inverted call paths: +// [0] +// [0, 0] +// [2, 0] +// [4, 5, 1] +// [4, 5] +function _compareNonInvertedCallNodesInSuffixOrder( + callNodeA: IndexIntoCallNodeTable, + callNodeB: IndexIntoCallNodeTable, + nonInvertedCallNodeTable: CallNodeTable +): number { + // Walk up both and stop at the first non-matching function. + // Walking up the non-inverted tree is equivalent to walking down the + // inverted tree. + while (true) { + const funcA = nonInvertedCallNodeTable.func[callNodeA]; + const funcB = nonInvertedCallNodeTable.func[callNodeB]; + if (funcA !== funcB) { + return funcA - funcB; + } + callNodeA = nonInvertedCallNodeTable.prefix[callNodeA]; + callNodeB = nonInvertedCallNodeTable.prefix[callNodeB]; + if (callNodeA === callNodeB) { + break; + } + if (callNodeA === -1) { + return -1; + } + if (callNodeB === -1) { + return 1; } } - return new CallNodeInfoImpl( - callNodeTable, - nonInvertedCallNodeTable, - nonInvertedStackIndexToCallNodeIndex, - stackIndexToNonInvertedCallNodeIndex, - true - ); + return 0; } // Given a stack index `needleStack` and a call node in the inverted tree @@ -502,6 +497,17 @@ export function getInvertedCallNodeInfo( // // Also returns null for any stacks which aren't used as self stacks. // +// Note: This function doesn't actually have a parameter named `invertedCallTreeNode`. +// Instead, it has two parameters for the node's suffix order index range. This +// range is obtained by the caller and is enough to check whether a stack's call +// path ends with the path suffix represented by the inverted call node. The caller +// gets the suffix order index range as follows: +// +// ``` +// const [rangeStart, rangeEnd] = +// callNodeInfo.getSuffixOrderIndexRangeForCallNode(callNodeIndex); +// ``` +// // Example: // // Stack table (`:`): Inverted call tree: @@ -529,33 +535,32 @@ export function getInvertedCallNodeInfo( // that frame, for example the frame's address or line. export function getMatchingAncestorStackForInvertedCallNode( needleStack: IndexIntoStackTable, - invertedTreeCallNode: IndexIntoCallNodeTable, - invertedTreeCallNodeSubtreeEnd: IndexIntoCallNodeTable, + suffixOrderIndexRangeStart: SuffixOrderIndex, + suffixOrderIndexRangeEnd: SuffixOrderIndex, + suffixOrderIndexes: Uint32Array, invertedTreeCallNodeDepth: number, - stackIndexToInvertedCallNodeIndex: Int32Array, + stackIndexToCallNodeIndex: Int32Array, stackTablePrefixCol: Array ): IndexIntoStackTable | null { - // Get the inverted call tree node for the (non-inverted) stack. + // Get the non-inverted call tree node for the (non-inverted) stack. // For example, if the stack has the call path A -> B -> C, - // this will give us the node C <- B <- A in the inverted tree. - const needleCallNode = stackIndexToInvertedCallNodeIndex[needleStack]; + // this will give us the node A -> B -> C in the non-inverted tree. + const needleCallNode = stackIndexToCallNodeIndex[needleStack]; + const needleSuffixOrderIndex = suffixOrderIndexes[needleCallNode]; - // Check if needleCallNode is a descendant of invertedTreeCallNode in the - // inverted tree. + // Check if needleCallNode's call path ends with the call path suffix represented + // by the inverted call node. if ( - needleCallNode >= invertedTreeCallNode && - needleCallNode < invertedTreeCallNodeSubtreeEnd + needleSuffixOrderIndex >= suffixOrderIndexRangeStart && + needleSuffixOrderIndex < suffixOrderIndexRangeEnd ) { - // needleCallNode is a descendant of invertedTreeCallNode in the inverted tree. - // That means that needleStack's self time contributes to the total time of - // invertedTreeCallNode. It also means that the non-inverted call path of - // needleStack "ends with" the suffix described by invertedTreeCallNode. - // For example, if invertedTreeCallNode is C <- B, and needleStack has the + // Yes, needleCallNode's call path ends with the call path suffix represented + // by the inverted call node. + // For example, if our node is C <- B in the inverted tree, and needleStack has the // non-inverted call path A -> B -> C, then we now know that A -> B -> C ends // with B -> C. - // Now we strip off this suffix. In the example, we strip off "-> C" at the - // end so that we end up with a stack for A -> B. - // Stripping off the suffix is equivalent to "walking down" in the inverted tree. + // Now we strip off this suffix. In the example, invertedTreeCallNodeDepth is 1 + // so we strip off "-> C" at the end and return a stack for A -> B. return getNthPrefixStack( needleStack, invertedTreeCallNodeDepth, @@ -563,7 +568,7 @@ export function getMatchingAncestorStackForInvertedCallNode( ); } - // Not a descendant; return null. + // The stack's call path doesn't end with the suffix we were looking for; return null. return null; } @@ -603,7 +608,7 @@ export function getSampleIndexToCallNodeIndex( * This is an implementation of getSamplesSelectedStates for just the case where * no call node is selected. */ -function getSamplesSelectedStatesForNoSelection( +function _getSamplesSelectedStatesForNoSelection( sampleCallNodes: Array, activeTabFilteredCallNodes: Array ): SelectedState[] { @@ -636,7 +641,7 @@ function getSamplesSelectedStatesForNoSelection( } /** - * Given the call node for each sample and the call node selected states, + * Given the call node for each sample and the selected call node, * compute each sample's selected state. * * For samples that are not filtered out, the sample's selected state is based @@ -682,12 +687,15 @@ function getSamplesSelectedStatesForNoSelection( * In this example, the selected node has index 13 and the "selected index range" * is the range from 13 to 21 (not including 21). */ -function mapCallNodeSelectedStatesToSamples( +function _getSamplesSelectedStatesNonInverted( sampleCallNodes: Array, activeTabFilteredCallNodes: Array, selectedCallNodeIndex: IndexIntoCallNodeTable, - selectedCallNodeDescendantsEndIndex: IndexIntoCallNodeTable + callNodeInfo: CallNodeInfo ): SelectedState[] { + const callNodeTable = callNodeInfo.getNonInvertedCallNodeTable(); + const selectedCallNodeDescendantsEndIndex = + callNodeTable.subtreeRangeEnd[selectedCallNodeIndex]; const sampleCount = sampleCallNodes.length; const samplesSelectedStates = new Array(sampleCount); for (let sampleIndex = 0; sampleIndex < sampleCount; sampleIndex++) { @@ -715,6 +723,48 @@ function mapCallNodeSelectedStatesToSamples( return samplesSelectedStates; } +/** + * The implementation of getSamplesSelectedStates for the inverted tree. + * + * This uses the suffix order, see the documentation of CallNodeInfoInverted. + */ +function _getSamplesSelectedStatesInverted( + sampleNonInvertedCallNodes: Array, + activeTabFilteredNonInvertedCallNodes: Array, + selectedInvertedCallNodeIndex: IndexIntoCallNodeTable, + callNodeInfo: CallNodeInfoInverted +): SelectedState[] { + const suffixOrderIndexes = callNodeInfo.getSuffixOrderIndexes(); + const [selectedSubtreeRangeStart, selectedSubtreeRangeEnd] = + callNodeInfo.getSuffixOrderIndexRangeForCallNode( + selectedInvertedCallNodeIndex + ); + const sampleCount = sampleNonInvertedCallNodes.length; + const samplesSelectedStates = new Array(sampleCount); + for (let sampleIndex = 0; sampleIndex < sampleCount; sampleIndex++) { + let sampleSelectedState: SelectedState = 'SELECTED'; + const callNodeIndex = sampleNonInvertedCallNodes[sampleIndex]; + if (callNodeIndex !== null) { + const suffixOrderIndex = suffixOrderIndexes[callNodeIndex]; + if (suffixOrderIndex < selectedSubtreeRangeStart) { + sampleSelectedState = 'UNSELECTED_ORDERED_BEFORE_SELECTED'; + } else if (suffixOrderIndex >= selectedSubtreeRangeEnd) { + sampleSelectedState = 'UNSELECTED_ORDERED_AFTER_SELECTED'; + } + } else { + // This sample was filtered out. + sampleSelectedState = + activeTabFilteredNonInvertedCallNodes[sampleIndex] === null + ? // This sample was not part of the active tab. + 'FILTERED_OUT_BY_ACTIVE_TAB' + : // This sample was filtered out in the transform pipeline. + 'FILTERED_OUT_BY_TRANSFORM'; + } + samplesSelectedStates[sampleIndex] = sampleSelectedState; + } + return samplesSelectedStates; +} + /** * Go through the samples, and determine their current state with respect to * the selection. @@ -724,24 +774,31 @@ function mapCallNodeSelectedStatesToSamples( */ export function getSamplesSelectedStates( callNodeInfo: CallNodeInfo, - sampleCallNodes: Array, - activeTabFilteredCallNodes: Array, + sampleNonInvertedCallNodes: Array, + activeTabFilteredNonInvertedCallNodes: Array, selectedCallNodeIndex: IndexIntoCallNodeTable | null ): SelectedState[] { if (selectedCallNodeIndex === null || selectedCallNodeIndex === -1) { - return getSamplesSelectedStatesForNoSelection( - sampleCallNodes, - activeTabFilteredCallNodes + return _getSamplesSelectedStatesForNoSelection( + sampleNonInvertedCallNodes, + activeTabFilteredNonInvertedCallNodes ); } - const callNodeTable = callNodeInfo.getCallNodeTable(); - return mapCallNodeSelectedStatesToSamples( - sampleCallNodes, - activeTabFilteredCallNodes, - selectedCallNodeIndex, - callNodeTable.subtreeRangeEnd[selectedCallNodeIndex] - ); + const callNodeInfoInverted = callNodeInfo.asInverted(); + return callNodeInfoInverted !== null + ? _getSamplesSelectedStatesInverted( + sampleNonInvertedCallNodes, + activeTabFilteredNonInvertedCallNodes, + selectedCallNodeIndex, + callNodeInfoInverted + ) + : _getSamplesSelectedStatesNonInverted( + sampleNonInvertedCallNodes, + activeTabFilteredNonInvertedCallNodes, + selectedCallNodeIndex, + callNodeInfo + ); } /** @@ -1017,51 +1074,78 @@ export function getTimingsForCallNodeIndex( return { forPath: pathTimings, rootTime }; } - const callNodeTable = callNodeInfo.getCallNodeTable(); - const stackIndexToCallNodeIndex = callNodeInfo.getStackIndexToCallNodeIndex(); - - const needleDescendantsEndIndex = - callNodeTable.subtreeRangeEnd[needleNodeIndex]; - - const isInvertedTree = callNodeInfo.isInverted(); - const needleNodeIsRootOfInvertedTree = - isInvertedTree && callNodeTable.prefix[needleNodeIndex] === -1; + const callNodeTable = callNodeInfo.getNonInvertedCallNodeTable(); + const stackIndexToCallNodeIndex = + callNodeInfo.getStackIndexToNonInvertedCallNodeIndex(); + const callNodeInfoInverted = callNodeInfo.asInverted(); + if (callNodeInfoInverted !== null) { + // Inverted case + const needleNodeIsRootOfInvertedTree = + callNodeInfoInverted.isRoot(needleNodeIndex); + const suffixOrderIndexes = callNodeInfoInverted.getSuffixOrderIndexes(); + const [rangeStart, rangeEnd] = + callNodeInfoInverted.getSuffixOrderIndexRangeForCallNode(needleNodeIndex); + + // Loop over each sample and accumulate the self time, running time, and + // the implementation breakdown. + for (let sampleIndex = 0; sampleIndex < samples.length; sampleIndex++) { + // Get the call node for this sample. + // TODO: Consider using sampleCallNodes for this, to save one indirection on + // a hot path. + const thisStackIndex = samples.stack[sampleIndex]; + if (thisStackIndex === null) { + continue; + } + const thisNodeIndex = stackIndexToCallNodeIndex[thisStackIndex]; + const thisNodeSuffixOrderIndex = suffixOrderIndexes[thisNodeIndex]; + const weight = samples.weight ? samples.weight[sampleIndex] : 1; + rootTime += Math.abs(weight); - // Loop over each sample and accumulate the self time, running time, and - // the implementation breakdown. - for (let sampleIndex = 0; sampleIndex < samples.length; sampleIndex++) { - // Get the call node for this sample. - // TODO: Consider using sampleCallNodes for this, to save one indirection on - // a hot path. - const thisStackIndex = samples.stack[sampleIndex]; - if (thisStackIndex === null) { - continue; + if ( + thisNodeSuffixOrderIndex >= rangeStart && + thisNodeSuffixOrderIndex < rangeEnd + ) { + // One of the parents is the exact passed path. + accumulateDataToTimings(pathTimings.totalTime, sampleIndex, weight); + + if (needleNodeIsRootOfInvertedTree) { + // This root node matches the passed call node path. + // Just increment the selfTime value. + // We don't call accumulateDataToTimings(pathTimings.selfTime, ...) + // here, mainly because this would be the same as for the total time. + pathTimings.selfTime.value += weight; + } + } } - const thisNodeIndex = stackIndexToCallNodeIndex[thisStackIndex]; - - const weight = samples.weight ? samples.weight[sampleIndex] : 1; - - rootTime += Math.abs(weight); + } else { + // Non-inverted case + const needleSubtreeRangeEnd = + callNodeTable.subtreeRangeEnd[needleNodeIndex]; + + // Loop over each sample and accumulate the self time, running time, and + // the implementation breakdown. + for (let sampleIndex = 0; sampleIndex < samples.length; sampleIndex++) { + // Get the call node for this sample. + // TODO: Consider using sampleCallNodes for this, to save one indirection on + // a hot path. + const thisStackIndex = samples.stack[sampleIndex]; + if (thisStackIndex === null) { + continue; + } + const thisNodeIndex = stackIndexToCallNodeIndex[thisStackIndex]; + const weight = samples.weight ? samples.weight[sampleIndex] : 1; + rootTime += Math.abs(weight); - if (!isInvertedTree) { // For non-inverted trees, we compute the self time from the stacks' leaf nodes. if (thisNodeIndex === needleNodeIndex) { accumulateDataToTimings(pathTimings.selfTime, sampleIndex, weight); } - } - - if ( - thisNodeIndex >= needleNodeIndex && - thisNodeIndex < needleDescendantsEndIndex - ) { - // One of the parents is the exact passed path. - accumulateDataToTimings(pathTimings.totalTime, sampleIndex, weight); - - if (needleNodeIsRootOfInvertedTree) { - // This root node matches the passed call node path. - // This is the only place where we don't accumulate timings, mainly - // because this would be the same as for the total time. - pathTimings.selfTime.value += weight; + if ( + thisNodeIndex >= needleNodeIndex && + thisNodeIndex < needleSubtreeRangeEnd + ) { + // One of the parents is the exact passed path. + accumulateDataToTimings(pathTimings.totalTime, sampleIndex, weight); } } } @@ -2242,98 +2326,6 @@ export function computeCallNodeMaxDepthPlusOne( return maxDepth + 1; } -function _computeThreadWithInvertedStackTable( - thread: Thread, - defaultCategory: IndexIntoCategoryList -): { - invertedThread: Thread, - oldStackToNewStack: Map, -} { - return timeCode('_computeThreadWithInvertedStackTable', () => { - const { stackTable, frameTable } = thread; - - const newStackTable = { - length: 0, - frame: [], - category: [], - subcategory: [], - prefix: [], - }; - // Create a Map that keys off of two values, both the prefix and frame combination - // by using a bit of math: prefix * frameCount + frame => stackIndex - const prefixAndFrameToStack = new Map(); - const frameCount = frameTable.length; - - // Returns the stackIndex for a specific frame (that is, a function and its - // context), and a specific prefix. If it doesn't exist yet it will create - // a new stack entry and return its index. - function stackFor(prefix, frame, category, subcategory) { - const prefixAndFrameIndex = - (prefix === null ? -1 : prefix) * frameCount + frame; - let stackIndex = prefixAndFrameToStack.get(prefixAndFrameIndex); - if (stackIndex === undefined) { - stackIndex = newStackTable.length++; - newStackTable.prefix[stackIndex] = prefix; - newStackTable.frame[stackIndex] = frame; - newStackTable.category[stackIndex] = category; - newStackTable.subcategory[stackIndex] = subcategory; - prefixAndFrameToStack.set(prefixAndFrameIndex, stackIndex); - } else if (newStackTable.category[stackIndex] !== category) { - // If two stack nodes from the non-inverted stack tree with different - // categories happen to collapse into the same stack node in the - // inverted tree, discard their category and set the category to the - // default category. - newStackTable.category[stackIndex] = defaultCategory; - newStackTable.subcategory[stackIndex] = 0; - } else if (newStackTable.subcategory[stackIndex] !== subcategory) { - // If two stack nodes from the non-inverted stack tree with the same - // category but different subcategories happen to collapse into the same - // stack node in the inverted tree, discard their subcategory and set it - // to the "Other" subcategory. - newStackTable.subcategory[stackIndex] = 0; - } - return stackIndex; - } - - const oldStackToNewStack = new Map(); - - // For one specific stack, this will ensure that stacks are created for all - // of its ancestors, by walking its prefix chain up to the root. - function convertStack(stackIndex) { - if (stackIndex === null) { - return null; - } - let newStack = oldStackToNewStack.get(stackIndex); - if (newStack === undefined) { - newStack = null; - for ( - let currentStack = stackIndex; - currentStack !== null; - currentStack = stackTable.prefix[currentStack] - ) { - // Notice how we reuse the previous stack as the prefix. This is what - // effectively inverts the call tree. - newStack = stackFor( - newStack, - stackTable.frame[currentStack], - stackTable.category[currentStack], - stackTable.subcategory[currentStack] - ); - } - oldStackToNewStack.set(stackIndex, ensureExists(newStack)); - } - return newStack; - } - - const invertedThread = updateThreadStacks( - thread, - newStackTable, - convertStack - ); - return { invertedThread, oldStackToNewStack }; - }); -} - /** * Compute the derived samples table. */ @@ -3056,6 +3048,19 @@ export function getFuncNamesAndOriginsForPath( * highlighted area for a selected subtree is contiguous in the graph. */ export function getTreeOrderComparator( + sampleNonInvertedCallNodes: Array, + callNodeInfo: CallNodeInfo +): (IndexIntoSamplesTable, IndexIntoSamplesTable) => number { + const callNodeInfoInverted = callNodeInfo.asInverted(); + return callNodeInfoInverted !== null + ? _getTreeOrderComparatorInverted( + sampleNonInvertedCallNodes, + callNodeInfoInverted + ) + : _getTreeOrderComparatorNonInverted(sampleNonInvertedCallNodes); +} + +export function _getTreeOrderComparatorNonInverted( sampleCallNodes: Array ): (IndexIntoSamplesTable, IndexIntoSamplesTable) => number { /** @@ -3089,6 +3094,38 @@ export function getTreeOrderComparator( }; } +function _getTreeOrderComparatorInverted( + sampleNonInvertedCallNodes: Array, + callNodeInfo: CallNodeInfoInverted +): (IndexIntoSamplesTable, IndexIntoSamplesTable) => number { + const callNodeTable = callNodeInfo.getNonInvertedCallNodeTable(); + return function treeOrderComparator( + sampleA: IndexIntoSamplesTable, + sampleB: IndexIntoSamplesTable + ): number { + const callNodeA = sampleNonInvertedCallNodes[sampleA]; + const callNodeB = sampleNonInvertedCallNodes[sampleB]; + + if (callNodeA === callNodeB) { + // Both are filtered out or both are the same. + return 0; + } + if (callNodeA === null) { + // A filtered out, B not filtered out. A goes after B. + return 1; + } + if (callNodeB === null) { + // B filtered out, A not filtered out. B goes after A. + return -1; + } + return _compareNonInvertedCallNodesInSuffixOrder( + callNodeA, + callNodeB, + callNodeTable + ); + }; +} + export function getFriendlyStackTypeName( implementation: StackImplementation ): string { @@ -3886,20 +3923,20 @@ export function getNativeSymbolsForCallNode( stackTable: StackTable, frameTable: FrameTable ): IndexIntoNativeSymbolTable[] { - if (callNodeInfo.isInverted()) { - return getNativeSymbolsForCallNodeInverted( - callNodeIndex, - callNodeInfo, - stackTable, - frameTable - ); - } - return getNativeSymbolsForCallNodeNonInverted( - callNodeIndex, - callNodeInfo, - stackTable, - frameTable - ); + const callNodeInfoInverted = callNodeInfo.asInverted(); + return callNodeInfoInverted !== null + ? getNativeSymbolsForCallNodeInverted( + callNodeIndex, + callNodeInfoInverted, + stackTable, + frameTable + ) + : getNativeSymbolsForCallNodeNonInverted( + callNodeIndex, + callNodeInfo, + stackTable, + frameTable + ); } export function getNativeSymbolsForCallNodeNonInverted( @@ -3925,21 +3962,24 @@ export function getNativeSymbolsForCallNodeNonInverted( export function getNativeSymbolsForCallNodeInverted( callNodeIndex: IndexIntoCallNodeTable, - callNodeInfo: CallNodeInfo, + callNodeInfo: CallNodeInfoInverted, stackTable: StackTable, frameTable: FrameTable ): IndexIntoNativeSymbolTable[] { - const invertedCallNodeTable = callNodeInfo.getCallNodeTable(); - const depth = invertedCallNodeTable.depth[callNodeIndex]; - const endIndex = invertedCallNodeTable.subtreeRangeEnd[callNodeIndex]; + const depth = callNodeInfo.depthForNode(callNodeIndex); + const [rangeStart, rangeEnd] = + callNodeInfo.getSuffixOrderIndexRangeForCallNode(callNodeIndex); const stackTablePrefixCol = stackTable.prefix; - const stackIndexToCallNodeIndex = callNodeInfo.getStackIndexToCallNodeIndex(); + const stackIndexToCallNodeIndex = + callNodeInfo.getStackIndexToNonInvertedCallNodeIndex(); + const suffixOrderIndexes = callNodeInfo.getSuffixOrderIndexes(); const set = new Set(); for (let stackIndex = 0; stackIndex < stackTable.length; stackIndex++) { const stackForNode = getMatchingAncestorStackForInvertedCallNode( stackIndex, - callNodeIndex, - endIndex, + rangeStart, + rangeEnd, + suffixOrderIndexes, depth, stackIndexToCallNodeIndex, stackTablePrefixCol @@ -4004,8 +4044,7 @@ export function getBottomBoxInfoForCallNode( nativeSymbols, } = thread; - const callNodeTable = callNodeInfo.getCallNodeTable(); - const funcIndex = callNodeTable.func[callNodeIndex]; + const funcIndex = callNodeInfo.funcForNode(callNodeIndex); const fileName = funcTable.fileName[funcIndex]; const sourceFile = fileName !== null ? stringTable.getString(fileName) : null; const resource = funcTable.resource[funcIndex]; diff --git a/src/profile-logic/stack-timing.js b/src/profile-logic/stack-timing.js index fc8c728a69..44bea2f3fa 100644 --- a/src/profile-logic/stack-timing.js +++ b/src/profile-logic/stack-timing.js @@ -6,9 +6,10 @@ import type { SamplesLikeTable, Milliseconds, - CallNodeInfo, IndexIntoCallNodeTable, } from 'firefox-profiler/types'; +import type { CallNodeInfo } from './call-node-info'; + /** * The StackTimingByDepth data structure organizes stack frames by their depth, and start * and end times. This optimizes sample data for Stack Chart views. It @@ -66,7 +67,7 @@ export function getStackTimingByDepth( maxDepthPlusOne: number, interval: Milliseconds ): StackTimingByDepth { - const callNodeTable = callNodeInfo.getCallNodeTable(); + const callNodeTable = callNodeInfo.getNonInvertedCallNodeTable(); const { prefix: callNodeTablePrefixColumn, subtreeRangeEnd: callNodeTableSubtreeRangeEndColumn, diff --git a/src/profile-logic/transforms.js b/src/profile-logic/transforms.js index 0a661c2566..7ad297d947 100644 --- a/src/profile-logic/transforms.js +++ b/src/profile-logic/transforms.js @@ -34,7 +34,6 @@ import type { CallNodePath, CallNodeAndCategoryPath, CallNodeTable, - CallNodeInfo, StackType, ImplementationFilter, Transform, @@ -49,6 +48,7 @@ import type { CategoryList, Milliseconds, } from 'firefox-profiler/types'; +import type { CallNodeInfo } from 'firefox-profiler/profile-logic/call-node-info'; import type { StringTable } from 'firefox-profiler/utils/string-table'; /** @@ -602,8 +602,6 @@ function _removeOtherCategoryFunctionsInNodePathWithFunction( callNodePath: CallNodePath, callNodeInfo: CallNodeInfo ): CallNodePath { - const callNodeTable = callNodeInfo.getCallNodeTable(); - const newCallNodePath = []; let prefix = -1; @@ -618,7 +616,7 @@ function _removeOtherCategoryFunctionsInNodePathWithFunction( ); } - if (callNodeTable.category[callNodeIndex] === category) { + if (callNodeInfo.categoryForNode(callNodeIndex) === category) { newCallNodePath.push(funcIndex); } diff --git a/src/selectors/per-thread/index.js b/src/selectors/per-thread/index.js index 4094b48b7f..9620bb7dde 100644 --- a/src/selectors/per-thread/index.js +++ b/src/selectors/per-thread/index.js @@ -287,8 +287,7 @@ export const selectedNodeSelectors: NodeSelectors = (() => { if (sourceViewFile === null || selectedCallNodeIndex === null) { return null; } - const callNodeTable = callNodeInfo.getCallNodeTable(); - const selectedFunc = callNodeTable.func[selectedCallNodeIndex]; + const selectedFunc = callNodeInfo.funcForNode(selectedCallNodeIndex); const selectedFuncFile = funcTable.fileName[selectedFunc]; if ( selectedFuncFile === null || diff --git a/src/selectors/per-thread/stack-sample.js b/src/selectors/per-thread/stack-sample.js index 7240710930..c48cb0ccc9 100644 --- a/src/selectors/per-thread/stack-sample.js +++ b/src/selectors/per-thread/stack-sample.js @@ -30,7 +30,6 @@ import type { ThreadIndex, IndexIntoSamplesTable, WeightType, - CallNodeInfo, CallNodePath, StackLineInfo, StackAddressInfo, @@ -44,7 +43,9 @@ import type { $ReturnType, ThreadsKey, SelfAndTotal, + CallNodeSelfAndSummary, } from 'firefox-profiler/types'; +import type { CallNodeInfo } from 'firefox-profiler/profile-logic/call-node-info'; import type { ThreadSelectorsPerThread } from './thread'; import type { MarkerSelectorsPerThread } from './markers'; @@ -115,15 +116,15 @@ export function getStackAndSampleSelectorsPerThread( const _getInvertedCallNodeInfo: Selector = createSelectorWithTwoCacheSlots( - threadSelectors.getFilteredThread, _getNonInvertedCallNodeInfo, ProfileSelectors.getDefaultCategory, - (thread, nonInvertedCallNodeInfo, defaultCategory) => { + (state) => threadSelectors.getFilteredThread(state).funcTable.length, + (nonInvertedCallNodeInfo, defaultCategory, funcCount) => { return ProfileData.getInvertedCallNodeInfo( - thread, nonInvertedCallNodeInfo.getNonInvertedCallNodeTable(), nonInvertedCallNodeInfo.getStackIndexToNonInvertedCallNodeIndex(), - defaultCategory + defaultCategory, + funcCount ); } ); @@ -230,23 +231,11 @@ export function getStackAndSampleSelectorsPerThread( ) ); - const getSampleIndexToCallNodeIndexForFilteredThread: Selector< - Array, - > = createSelector( - (state) => threadSelectors.getFilteredThread(state).samples.stack, - (state) => getCallNodeInfo(state).getStackIndexToCallNodeIndex(), - (filteredThreadSampleStacks, stackIndexToCallNodeIndex) => - ProfileData.getSampleIndexToCallNodeIndex( - filteredThreadSampleStacks, - stackIndexToCallNodeIndex - ) - ); - - const _getPreviewFilteredCtssSampleIndexToCallNodeIndex: Selector< + const _getPreviewFilteredCtssSampleIndexToNonInvertedCallNodeIndex: Selector< Array, > = createSelector( (state) => threadSelectors.getPreviewFilteredCtssSamples(state).stack, - (state) => getCallNodeInfo(state).getStackIndexToCallNodeIndex(), + (state) => getCallNodeInfo(state).getStackIndexToNonInvertedCallNodeIndex(), ProfileData.getSampleIndexToCallNodeIndex ); @@ -262,35 +251,35 @@ export function getStackAndSampleSelectorsPerThread( ) ); - const getSampleIndexToCallNodeIndexForTabFilteredThread: Selector< + const _getSampleIndexToNonInvertedCallNodeIndexForTabFilteredThread: Selector< Array, > = createSelector( (state) => threadSelectors.getTabFilteredThread(state).samples.stack, - (state) => getCallNodeInfo(state).getStackIndexToCallNodeIndex(), - (tabFilteredThreadSampleStacks, stackIndexToCallNodeIndex) => + (state) => getCallNodeInfo(state).getStackIndexToNonInvertedCallNodeIndex(), + (tabFilteredThreadSampleStacks, stackIndexToNonInvertedCallNodeIndex) => ProfileData.getSampleIndexToCallNodeIndex( tabFilteredThreadSampleStacks, - stackIndexToCallNodeIndex + stackIndexToNonInvertedCallNodeIndex ) ); const getSamplesSelectedStatesInFilteredThread: Selector< null | SelectedState[], > = createSelector( - getSampleIndexToCallNodeIndexForFilteredThread, - getSampleIndexToCallNodeIndexForTabFilteredThread, + getSampleIndexToNonInvertedCallNodeIndexForFilteredThread, + _getSampleIndexToNonInvertedCallNodeIndexForTabFilteredThread, getCallNodeInfo, getSelectedCallNodeIndex, ( - sampleIndexToCallNodeIndex, - activeTabFilteredCallNodeIndex, + sampleIndexToNonInvertedCallNodeIndex, + activeTabFilteredNonInvertedCallNodes, callNodeInfo, selectedCallNode ) => { return ProfileData.getSamplesSelectedStates( callNodeInfo, - sampleIndexToCallNodeIndex, - activeTabFilteredCallNodeIndex, + sampleIndexToNonInvertedCallNodeIndex, + activeTabFilteredNonInvertedCallNodes, selectedCallNode ); } @@ -299,7 +288,8 @@ export function getStackAndSampleSelectorsPerThread( const getTreeOrderComparatorInFilteredThread: Selector< (IndexIntoSamplesTable, IndexIntoSamplesTable) => number, > = createSelector( - getSampleIndexToCallNodeIndexForFilteredThread, + getSampleIndexToNonInvertedCallNodeIndexForFilteredThread, + getCallNodeInfo, ProfileData.getTreeOrderComparator ); @@ -321,23 +311,33 @@ export function getStackAndSampleSelectorsPerThread( (samples) => samples.weightType || 'samples' ); + const getCallNodeSelfAndSummary: Selector = + createSelector( + threadSelectors.getPreviewFilteredCtssSamples, + _getPreviewFilteredCtssSampleIndexToNonInvertedCallNodeIndex, + getCallNodeInfo, + (samples, sampleIndexToCallNodeIndex, callNodeInfo) => { + return CallTree.computeCallNodeSelfAndSummary( + samples, + sampleIndexToCallNodeIndex, + callNodeInfo.getNonInvertedCallNodeTable().length + ); + } + ); + const getCallTreeTimings: Selector = createSelector( - threadSelectors.getPreviewFilteredCtssSamples, - _getPreviewFilteredCtssSampleIndexToCallNodeIndex, getCallNodeInfo, - (samples, sampleIndexToCallNodeIndex, callNodeInfo) => { - const callNodeLeafAndSummary = CallTree.computeCallNodeLeafAndSummary( - samples, - sampleIndexToCallNodeIndex, - callNodeInfo.getCallNodeTable().length - ); - return CallTree.computeCallTreeTimings( - callNodeInfo, - callNodeLeafAndSummary - ); - } + getCallNodeSelfAndSummary, + CallTree.computeCallTreeTimings ); + const getCallTreeTimingsNonInverted: Selector = + createSelector( + getCallNodeInfo, + getCallNodeSelfAndSummary, + CallTree.computeCallTreeTimingsNonInverted + ); + const getCallTree: Selector = createSelector( threadSelectors.getFilteredThread, getCallNodeInfo, @@ -363,23 +363,23 @@ export function getStackAndSampleSelectorsPerThread( const getTracedTiming: Selector = createSelector( threadSelectors.getPreviewFilteredCtssSamples, - _getPreviewFilteredCtssSampleIndexToCallNodeIndex, + _getPreviewFilteredCtssSampleIndexToNonInvertedCallNodeIndex, getCallNodeInfo, ProfileSelectors.getProfileInterval, (samples, sampleIndexToCallNodeIndex, callNodeInfo, interval) => { - const callNodeLeafAndSummary = - CallTree.computeCallNodeTracedLeafAndSummary( + const CallNodeSelfAndSummary = + CallTree.computeCallNodeTracedSelfAndSummary( samples, sampleIndexToCallNodeIndex, - callNodeInfo.getCallNodeTable().length, + callNodeInfo.getNonInvertedCallNodeTable().length, interval ); - if (callNodeLeafAndSummary === null) { + if (CallNodeSelfAndSummary === null) { return null; } return CallTree.computeCallTreeTimings( callNodeInfo, - callNodeLeafAndSummary + CallNodeSelfAndSummary ); } ); @@ -387,21 +387,24 @@ export function getStackAndSampleSelectorsPerThread( const getTracedSelfAndTotalForSelectedCallNode: Selector = createSelector( getSelectedCallNodeIndex, + getCallNodeInfo, getTracedTiming, - (selectedCallNodeIndex, tracedTiming) => { + (selectedCallNodeIndex, callNodeInfo, tracedTiming) => { if (selectedCallNodeIndex === null || tracedTiming === null) { return null; } - const total = tracedTiming.total[selectedCallNodeIndex]; - const self = tracedTiming.self[selectedCallNodeIndex]; - return { total, self }; + return CallTree.getSelfAndTotalForCallNode( + selectedCallNodeIndex, + callNodeInfo, + tracedTiming + ); } ); const getStackTimingByDepth: Selector = createSelector( threadSelectors.getFilteredCtssSamples, - getSampleIndexToCallNodeIndexForFilteredThread, // Bug! #5327 + getSampleIndexToNonInvertedCallNodeIndexForFilteredThread, // Bug! #5327 getCallNodeInfo, getFilteredCallNodeMaxDepthPlusOne, ProfileSelectors.getProfileInterval, @@ -419,7 +422,7 @@ export function getStackAndSampleSelectorsPerThread( createSelector( getFlameGraphRows, (state) => getCallNodeInfo(state).getNonInvertedCallNodeTable(), - getCallTreeTimings, + getCallTreeTimingsNonInverted, FlameGraph.getFlameGraphTiming ); @@ -452,9 +455,7 @@ export function getStackAndSampleSelectorsPerThread( getSelectedCallNodeIndex, getExpandedCallNodePaths, getExpandedCallNodeIndexes, - getSampleIndexToCallNodeIndexForFilteredThread, getSampleIndexToNonInvertedCallNodeIndexForFilteredThread, - getSampleIndexToCallNodeIndexForTabFilteredThread, getSamplesSelectedStatesInFilteredThread, getTreeOrderComparatorInFilteredThread, getCallTree, diff --git a/src/test/components/__snapshots__/ProfileCallTreeView.test.js.snap b/src/test/components/__snapshots__/ProfileCallTreeView.test.js.snap index 18baa59885..f347850326 100644 --- a/src/test/components/__snapshots__/ProfileCallTreeView.test.js.snap +++ b/src/test/components/__snapshots__/ProfileCallTreeView.test.js.snap @@ -4374,7 +4374,7 @@ for understanding where time was actually spent in a program." class="react-contextmenu-wrapper treeViewContextMenu" >
@@ -4751,7 +4751,7 @@ for understanding where time was actually spent in a program." aria-level="2" aria-selected="false" class="treeViewRow treeViewRowScrolledColumns odd" - id="treeViewRow-6" + id="treeViewRow-9" role="treeitem" style="height: 16px; line-height: 16px;" > @@ -4781,7 +4781,7 @@ for understanding where time was actually spent in a program." aria-level="3" aria-selected="false" class="treeViewRow treeViewRowScrolledColumns even" - id="treeViewRow-7" + id="treeViewRow-10" role="treeitem" style="height: 16px; line-height: 16px;" > @@ -4813,7 +4813,7 @@ for understanding where time was actually spent in a program." aria-level="4" aria-selected="false" class="treeViewRow treeViewRowScrolledColumns odd" - id="treeViewRow-8" + id="treeViewRow-11" role="treeitem" style="height: 16px; line-height: 16px;" > @@ -4842,7 +4842,7 @@ for understanding where time was actually spent in a program." aria-level="5" aria-selected="true" class="treeViewRow treeViewRowScrolledColumns even isSelected" - id="treeViewRow-9" + id="treeViewRow-13" role="treeitem" style="height: 16px; line-height: 16px;" > @@ -4872,7 +4872,7 @@ for understanding where time was actually spent in a program." aria-level="4" aria-selected="false" class="treeViewRow treeViewRowScrolledColumns odd" - id="treeViewRow-10" + id="treeViewRow-12" role="treeitem" style="height: 16px; line-height: 16px;" > @@ -4902,7 +4902,7 @@ for understanding where time was actually spent in a program." aria-level="1" aria-selected="false" class="treeViewRow treeViewRowScrolledColumns even" - id="treeViewRow-0" + id="treeViewRow-4" role="treeitem" style="height: 16px; line-height: 16px;" > diff --git a/src/test/fixtures/utils.js b/src/test/fixtures/utils.js index 7dbe88843f..e6e75d5b6d 100644 --- a/src/test/fixtures/utils.js +++ b/src/test/fixtures/utils.js @@ -4,7 +4,7 @@ // @flow import { getCallTree, - computeCallNodeLeafAndSummary, + computeCallNodeSelfAndSummary, computeCallTreeTimings, type CallTree, } from 'firefox-profiler/profile-logic/call-tree'; @@ -165,13 +165,13 @@ export function callTreeFromProfile( ); const callTreeTimings = computeCallTreeTimings( callNodeInfo, - computeCallNodeLeafAndSummary( + computeCallNodeSelfAndSummary( thread.samples, getSampleIndexToCallNodeIndex( thread.samples.stack, - callNodeInfo.getStackIndexToCallNodeIndex() + callNodeInfo.getStackIndexToNonInvertedCallNodeIndex() ), - callNodeInfo.getCallNodeTable().length + callNodeInfo.getNonInvertedCallNodeTable().length ) ); return getCallTree( diff --git a/src/test/store/__snapshots__/profile-view.test.js.snap b/src/test/store/__snapshots__/profile-view.test.js.snap index 325e784ca9..65a2e11431 100644 --- a/src/test/store/__snapshots__/profile-view.test.js.snap +++ b/src/test/store/__snapshots__/profile-view.test.js.snap @@ -2147,7 +2147,7 @@ Object { `; exports[`snapshots of selectors/profile matches the last stored run of selectedThreadSelector.getCallNodeInfo 1`] = ` -CallNodeInfoImpl { +CallNodeInfoNonInverted { "_cache": Map {}, "_callNodeTable": Object { "category": Int32Array [ @@ -2218,16 +2218,16 @@ CallNodeInfoImpl { 1, 7, ], - "sourceFramesInlinedIntoSymbol": Array [ - null, - null, - null, - null, - null, - null, - null, - null, - null, + "sourceFramesInlinedIntoSymbol": Int32Array [ + -2, + -2, + -2, + -2, + -2, + -2, + -2, + -2, + -2, ], "subcategory": Int32Array [ 0, @@ -2252,121 +2252,6 @@ CallNodeInfoImpl { 9, ], }, - "_isInverted": false, - "_nonInvertedCallNodeTable": Object { - "category": Int32Array [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - ], - "depth": Array [ - 0, - 1, - 2, - 2, - 3, - 2, - 3, - 2, - 3, - ], - "func": Int32Array [ - 0, - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 8, - ], - "innerWindowID": Float64Array [ - 0, - 0, - 0, - 0, - 0, - 0, - 2, - 0, - 0, - ], - "length": 9, - "maxDepth": 3, - "nextSibling": Int32Array [ - -1, - -1, - 3, - 5, - -1, - 7, - -1, - -1, - -1, - ], - "prefix": Int32Array [ - -1, - 0, - 1, - 1, - 3, - 1, - 5, - 1, - 7, - ], - "sourceFramesInlinedIntoSymbol": Array [ - null, - null, - null, - null, - null, - null, - null, - null, - null, - ], - "subcategory": Int32Array [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - ], - "subtreeRangeEnd": Uint32Array [ - 9, - 9, - 3, - 5, - 5, - 7, - 7, - 9, - 9, - ], - }, - "_stackIndexToCallNodeIndex": Int32Array [ - 0, - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 8, - ], "_stackIndexToNonInvertedCallNodeIndex": Int32Array [ 0, 1, @@ -2383,7 +2268,7 @@ CallNodeInfoImpl { exports[`snapshots of selectors/profile matches the last stored run of selectedThreadSelector.getCallTree 1`] = ` CallTree { - "_callNodeInfo": CallNodeInfoImpl { + "_callNodeInfo": CallNodeInfoNonInverted { "_cache": Map {}, "_callNodeTable": Object { "category": Int32Array [ @@ -2454,120 +2339,16 @@ CallTree { 1, 7, ], - "sourceFramesInlinedIntoSymbol": Array [ - null, - null, - null, - null, - null, - null, - null, - null, - null, - ], - "subcategory": Int32Array [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - ], - "subtreeRangeEnd": Uint32Array [ - 9, - 9, - 3, - 5, - 5, - 7, - 7, - 9, - 9, - ], - }, - "_isInverted": false, - "_nonInvertedCallNodeTable": Object { - "category": Int32Array [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - ], - "depth": Array [ - 0, - 1, - 2, - 2, - 3, - 2, - 3, - 2, - 3, - ], - "func": Int32Array [ - 0, - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 8, - ], - "innerWindowID": Float64Array [ - 0, - 0, - 0, - 0, - 0, - 0, - 2, - 0, - 0, - ], - "length": 9, - "maxDepth": 3, - "nextSibling": Int32Array [ - -1, - -1, - 3, - 5, - -1, - 7, - -1, - -1, - -1, - ], - "prefix": Int32Array [ - -1, - 0, - 1, - 1, - 3, - 1, - 5, - 1, - 7, - ], - "sourceFramesInlinedIntoSymbol": Array [ - null, - null, - null, - null, - null, - null, - null, - null, - null, + "sourceFramesInlinedIntoSymbol": Int32Array [ + -2, + -2, + -2, + -2, + -2, + -2, + -2, + -2, + -2, ], "subcategory": Int32Array [ 0, @@ -2592,17 +2373,6 @@ CallTree { 9, ], }, - "_stackIndexToCallNodeIndex": Int32Array [ - 0, - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 8, - ], "_stackIndexToNonInvertedCallNodeIndex": Int32Array [ 0, 1, @@ -2615,109 +2385,6 @@ CallTree { 8, ], }, - "_callNodeTable": Object { - "category": Int32Array [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - ], - "depth": Array [ - 0, - 1, - 2, - 2, - 3, - 2, - 3, - 2, - 3, - ], - "func": Int32Array [ - 0, - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 8, - ], - "innerWindowID": Float64Array [ - 0, - 0, - 0, - 0, - 0, - 0, - 2, - 0, - 0, - ], - "length": 9, - "maxDepth": 3, - "nextSibling": Int32Array [ - -1, - -1, - 3, - 5, - -1, - 7, - -1, - -1, - -1, - ], - "prefix": Int32Array [ - -1, - 0, - 1, - 1, - 3, - 1, - 5, - 1, - 7, - ], - "sourceFramesInlinedIntoSymbol": Array [ - null, - null, - null, - null, - null, - null, - null, - null, - null, - ], - "subcategory": Int32Array [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - ], - "subtreeRangeEnd": Uint32Array [ - 9, - 9, - 3, - 5, - 5, - 7, - 7, - 9, - 9, - ], - }, "_categories": Array [ Object { "color": "grey", @@ -2778,7 +2445,7 @@ CallTree { ], "_children": Array [], "_displayDataByIndex": Map {}, - "_internal": CallTreeInternalImpl { + "_internal": CallTreeInternalNonInverted { "_callNodeHasChildren": Uint8Array [ 1, 1, @@ -2790,7 +2457,7 @@ CallTree { 0, 0, ], - "_callNodeInfo": CallNodeInfoImpl { + "_callNodeInfo": CallNodeInfoNonInverted { "_cache": Map {}, "_callNodeTable": Object { "category": Int32Array [ @@ -2861,120 +2528,16 @@ CallTree { 1, 7, ], - "sourceFramesInlinedIntoSymbol": Array [ - null, - null, - null, - null, - null, - null, - null, - null, - null, - ], - "subcategory": Int32Array [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - ], - "subtreeRangeEnd": Uint32Array [ - 9, - 9, - 3, - 5, - 5, - 7, - 7, - 9, - 9, - ], - }, - "_isInverted": false, - "_nonInvertedCallNodeTable": Object { - "category": Int32Array [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - ], - "depth": Array [ - 0, - 1, - 2, - 2, - 3, - 2, - 3, - 2, - 3, - ], - "func": Int32Array [ - 0, - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 8, - ], - "innerWindowID": Float64Array [ - 0, - 0, - 0, - 0, - 0, - 0, - 2, - 0, - 0, - ], - "length": 9, - "maxDepth": 3, - "nextSibling": Int32Array [ - -1, - -1, - 3, - 5, - -1, - 7, - -1, - -1, - -1, - ], - "prefix": Int32Array [ - -1, - 0, - 1, - 1, - 3, - 1, - 5, - 1, - 7, - ], - "sourceFramesInlinedIntoSymbol": Array [ - null, - null, - null, - null, - null, - null, - null, - null, - null, + "sourceFramesInlinedIntoSymbol": Int32Array [ + -2, + -2, + -2, + -2, + -2, + -2, + -2, + -2, + -2, ], "subcategory": Int32Array [ 0, @@ -2999,17 +2562,6 @@ CallTree { 9, ], }, - "_stackIndexToCallNodeIndex": Int32Array [ - 0, - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 8, - ], "_stackIndexToNonInvertedCallNodeIndex": Int32Array [ 0, 1, @@ -3091,16 +2643,16 @@ CallTree { 1, 7, ], - "sourceFramesInlinedIntoSymbol": Array [ - null, - null, - null, - null, - null, - null, - null, - null, - null, + "sourceFramesInlinedIntoSymbol": Int32Array [ + -2, + -2, + -2, + -2, + -2, + -2, + -2, + -2, + -2, ], "subcategory": Int32Array [ 0, @@ -3137,17 +2689,6 @@ CallTree { 0, 0, ], - "leaf": Float32Array [ - 0, - 0, - 0, - 0, - 0, - 0, - 2, - 0, - 0, - ], "rootTotalSummary": 2, "self": Float32Array [ 0, diff --git a/src/test/store/profile-view.test.js b/src/test/store/profile-view.test.js index 955c6e105d..735e03942a 100644 --- a/src/test/store/profile-view.test.js +++ b/src/test/store/profile-view.test.js @@ -52,6 +52,7 @@ import { processCounter, type BreakdownByCategory, } from '../../profile-logic/profile-data'; +import { getSelfAndTotalForCallNode } from '../../profile-logic/call-tree'; import type { TrackReference, @@ -3629,7 +3630,7 @@ describe('traced timing', function () { const callNodeInfo = selectedThreadSelectors.getCallNodeInfo(getState()); - const { total, self } = ensureExists( + const tracedTiming = ensureExists( selectedThreadSelectors.getTracedTiming(getState()), 'Expected to get a traced timing.' ); @@ -3640,7 +3641,11 @@ describe('traced timing', function () { const callNodeIndex = ensureExists( callNodeInfo.getCallNodeIndexFromPath(callNodePath) ); - return { self: self[callNodeIndex], total: total[callNodeIndex] }; + return getSelfAndTotalForCallNode( + callNodeIndex, + callNodeInfo, + tracedTiming + ); }, profile, }; diff --git a/src/test/store/transforms.test.js b/src/test/store/transforms.test.js index 176f3f5f18..f65fb30e60 100644 --- a/src/test/store/transforms.test.js +++ b/src/test/store/transforms.test.js @@ -625,12 +625,11 @@ describe('"focus-function" transform', function () { }); describe('"focus-category" transform', function () { - describe('on a tiny call tree', function () { - const { profile } = getProfileFromTextSamples(` - A[cat:Graphics] - B[cat:Layout] - C[cat:Graphics] - `); + function setup(textSamples: string) { + const { + profile, + funcNamesDictPerThread: [funcNamesDict], + } = getProfileFromTextSamples(textSamples); const threadIndex = 0; if (profile.meta.categories === undefined) { throw new Error('Expected profile to have categories'); @@ -639,7 +638,20 @@ describe('"focus-category" transform', function () { .map((c, i) => (c.name === 'Graphics' ? i : -1)) .filter((i) => i !== -1)[0]; - const { dispatch, getState } = storeWithProfile(profile); + return { + threadIndex, + categoryIndex, + funcNamesDict, + ...storeWithProfile(profile), + }; + } + + describe('on a tiny call tree', function () { + const { threadIndex, categoryIndex, getState, dispatch } = setup(` + A[cat:Graphics] + B[cat:Layout] + C[cat:Graphics] + `); const originalCallTree = selectedThreadSelectors.getCallTree(getState()); it('starts as an unfiltered call tree', function () { @@ -666,21 +678,12 @@ describe('"focus-category" transform', function () { }); describe('on a slightly larger call tree', function () { - const { profile } = getProfileFromTextSamples(` + const { threadIndex, categoryIndex, getState, dispatch } = setup(` A[cat:Graphics] B[cat:Layout] D[cat:Layout] C[cat:Graphics] `); - const threadIndex = 0; - if (profile.meta.categories === undefined) { - throw new Error('Expected profile to have categories'); - } - const categoryIndex = profile.meta.categories - .map((c, i) => (c.name === 'Graphics' ? i : -1)) - .filter((i) => i !== -1)[0]; - - const { dispatch, getState } = storeWithProfile(profile); it('category Graphics can be focused', function () { dispatch( @@ -698,19 +701,10 @@ describe('"focus-category" transform', function () { }); describe('on a tinier call tree', function () { - const { profile } = getProfileFromTextSamples(` + const { threadIndex, categoryIndex, getState, dispatch } = setup(` A[cat:Graphics] B[cat:Layout] `); - const threadIndex = 0; - if (profile.meta.categories === undefined) { - throw new Error('Expected profile to have categories'); - } - const categoryIndex = profile.meta.categories - .map((c, i) => (c.name === 'Graphics' ? i : -1)) - .filter((i) => i !== -1)[0]; - - const { dispatch, getState } = storeWithProfile(profile); it('category Graphics can be focused', function () { dispatch( @@ -725,19 +719,10 @@ describe('"focus-category" transform', function () { }); describe('on a small call tree', function () { - const { profile } = getProfileFromTextSamples(` + const { threadIndex, categoryIndex, getState, dispatch } = setup(` A[cat:Graphics] A[cat:Graphics] B[cat:Layout] `); - const threadIndex = 0; - if (profile.meta.categories === undefined) { - throw new Error('Expected profile to have categories'); - } - const categoryIndex = profile.meta.categories - .map((c, i) => (c.name === 'Graphics' ? i : -1)) - .filter((i) => i !== -1)[0]; - - const { dispatch, getState } = storeWithProfile(profile); it('category Graphics can be focused', function () { dispatch( @@ -752,19 +737,10 @@ describe('"focus-category" transform', function () { }); describe('on a small call tree 2', function () { - const { profile } = getProfileFromTextSamples(` + const { threadIndex, categoryIndex, getState, dispatch } = setup(` B[cat:Layout] A[cat:Graphics] A[cat:Graphics] `); - const threadIndex = 0; - if (profile.meta.categories === undefined) { - throw new Error('Expected profile to have categories'); - } - const categoryIndex = profile.meta.categories - .map((c, i) => (c.name === 'Graphics' ? i : -1)) - .filter((i) => i !== -1)[0]; - - const { dispatch, getState } = storeWithProfile(profile); it('category Graphics can be focused', function () { dispatch( @@ -779,24 +755,18 @@ describe('"focus-category" transform', function () { }); describe('on a longer larger call tree', function () { - const { profile } = getProfileFromTextSamples(` - A[cat:Graphics] - B[cat:Layout] - D[cat:Layout] - A[cat:Graphics] - D[cat:Layout] - `); - const threadIndex = 0; - if (profile.meta.categories === undefined) { - throw new Error('Expected profile to have categories'); - } - const categoryIndex = profile.meta.categories - .map((c, i) => (c.name === 'Graphics' ? i : -1)) - .filter((i) => i !== -1)[0]; - - const { dispatch, getState } = storeWithProfile(profile); + const { threadIndex, categoryIndex, getState, dispatch, funcNamesDict } = + setup(` + A[cat:Graphics] + B[cat:Layout] + D[cat:Layout] + A[cat:Graphics] + D[cat:Layout] + `); it('category Graphics can be focused', function () { + const { A, B, D } = funcNamesDict; + dispatch(changeSelectedCallNode(threadIndex, [A, B, D, A, D])); dispatch( addTransformToStack(threadIndex, { type: 'focus-category', @@ -808,6 +778,48 @@ describe('"focus-category" transform', function () { '- A (total: 1, self: —)', ' - A (total: 1, self: 1)', ]); + const selectedCallNodePath = + selectedThreadSelectors.getSelectedCallNodePath(getState()); + expect(selectedCallNodePath).toEqual([A, A]); + }); + }); + + describe('on an inverted call tree', function () { + const { threadIndex, categoryIndex, getState, dispatch, funcNamesDict } = + setup(` + A[cat:Graphics] + B[cat:Layout] + D[cat:Layout] + A[cat:Graphics] + D[cat:Layout] + `); + + it('category Graphics can be focused after inversion', function () { + dispatch(changeInvertCallstack(true)); + const callTree = selectedThreadSelectors.getCallTree(getState()); + expect(formatTree(callTree)).toEqual([ + '- D (total: 1, self: 1)', + ' - A (total: 1, self: —)', + ' - D (total: 1, self: —)', + ' - B (total: 1, self: —)', + ' - A (total: 1, self: —)', + ]); + const { A, B, D } = funcNamesDict; + dispatch(changeSelectedCallNode(threadIndex, [D, A, D, B, A])); + dispatch( + addTransformToStack(threadIndex, { + type: 'focus-category', + category: categoryIndex, + }) + ); + const callTree2 = selectedThreadSelectors.getCallTree(getState()); + expect(formatTree(callTree2)).toEqual([ + '- A (total: 1, self: 1)', + ' - A (total: 1, self: —)', + ]); + const selectedCallNodePath = + selectedThreadSelectors.getSelectedCallNodePath(getState()); + expect(selectedCallNodePath).toEqual([A, A]); }); }); }); diff --git a/src/test/unit/address-timings.test.js b/src/test/unit/address-timings.test.js index e9d67a466d..5b7c06ab09 100644 --- a/src/test/unit/address-timings.test.js +++ b/src/test/unit/address-timings.test.js @@ -170,10 +170,10 @@ describe('getAddressTimings for getStackAddressInfoForCallNode', function () { ); const callNodeInfo = isInverted ? getInvertedCallNodeInfo( - thread, nonInvertedCallNodeInfo.getNonInvertedCallNodeTable(), nonInvertedCallNodeInfo.getStackIndexToNonInvertedCallNodeIndex(), - defaultCategory + defaultCategory, + funcTable.length ) : nonInvertedCallNodeInfo; const callNodeIndex = ensureExists( diff --git a/src/test/unit/line-timings.test.js b/src/test/unit/line-timings.test.js index 135f515e5e..0b677ed602 100644 --- a/src/test/unit/line-timings.test.js +++ b/src/test/unit/line-timings.test.js @@ -129,10 +129,10 @@ describe('getLineTimings for getStackLineInfoForCallNode', function () { ); const callNodeInfo = isInverted ? getInvertedCallNodeInfo( - thread, nonInvertedCallNodeInfo.getNonInvertedCallNodeTable(), nonInvertedCallNodeInfo.getStackIndexToNonInvertedCallNodeIndex(), - defaultCategory + defaultCategory, + funcTable.length ) : nonInvertedCallNodeInfo; const callNodeIndex = ensureExists( diff --git a/src/test/unit/profile-data.test.js b/src/test/unit/profile-data.test.js index acf112cf31..373e2a10fa 100644 --- a/src/test/unit/profile-data.test.js +++ b/src/test/unit/profile-data.test.js @@ -571,6 +571,106 @@ describe('profile-data', function () { }); }); +describe('getInvertedCallNodeInfo', function () { + function setup(plaintextSamples) { + const { derivedThreads, funcNamesDictPerThread, defaultCategory } = + getProfileFromTextSamples(plaintextSamples); + + const [thread] = derivedThreads; + const [funcNamesDict] = funcNamesDictPerThread; + const nonInvertedCallNodeInfo = getCallNodeInfo( + thread.stackTable, + thread.frameTable, + thread.funcTable, + defaultCategory + ); + + const invertedCallNodeInfo = getInvertedCallNodeInfo( + nonInvertedCallNodeInfo.getNonInvertedCallNodeTable(), + nonInvertedCallNodeInfo.getStackIndexToNonInvertedCallNodeIndex(), + defaultCategory, + thread.funcTable.length + ); + + // This function is used to test `getSuffixOrderIndexRangeForCallNode` and + // `getSuffixOrderedCallNodes`. To find the non-inverted call nodes with + // a call path suffix, `nodesWithSuffix` gets the inverted node X for the + // given call path suffix, and lists non-inverted nodes in X's "suffix + // order index range". + // These are the nodes whose call paths, if inverted, would correspond to + // inverted call nodes that are descendants of X. + function nodesWithSuffix(callPathSuffix) { + const invertedNodeForSuffix = ensureExists( + invertedCallNodeInfo.getCallNodeIndexFromPath( + [...callPathSuffix].reverse() + ) + ); + const [rangeStart, rangeEnd] = + invertedCallNodeInfo.getSuffixOrderIndexRangeForCallNode( + invertedNodeForSuffix + ); + const suffixOrderedCallNodes = + invertedCallNodeInfo.getSuffixOrderedCallNodes(); + const nonInvertedCallNodes = new Set(); + for (let i = rangeStart; i < rangeEnd; i++) { + nonInvertedCallNodes.add(suffixOrderedCallNodes[i]); + } + return nonInvertedCallNodes; + } + + return { + funcNamesDict, + nonInvertedCallNodeInfo, + invertedCallNodeInfo, + nodesWithSuffix, + }; + } + + it('creates a correct suffix order for this example profile', function () { + const { + funcNamesDict: { A, B, C }, + nonInvertedCallNodeInfo, + nodesWithSuffix, + } = setup(` + A A A A A A A + B B B A A C + A C B + `); + + const cnA = nonInvertedCallNodeInfo.getCallNodeIndexFromPath([A]); + const cnAB = nonInvertedCallNodeInfo.getCallNodeIndexFromPath([A, B]); + const cnABA = nonInvertedCallNodeInfo.getCallNodeIndexFromPath([A, B, A]); + const cnABC = nonInvertedCallNodeInfo.getCallNodeIndexFromPath([A, B, C]); + const cnAA = nonInvertedCallNodeInfo.getCallNodeIndexFromPath([A, A]); + const cnAAB = nonInvertedCallNodeInfo.getCallNodeIndexFromPath([A, A, B]); + const cnAC = nonInvertedCallNodeInfo.getCallNodeIndexFromPath([A, C]); + + expect(nodesWithSuffix([A])).toEqual(new Set([cnA, cnABA, cnAA])); + expect(nodesWithSuffix([B])).toEqual(new Set([cnAB, cnAAB])); + expect(nodesWithSuffix([A, B])).toEqual(new Set([cnAB, cnAAB])); + expect(nodesWithSuffix([A, A, B])).toEqual(new Set([cnAAB])); + expect(nodesWithSuffix([C])).toEqual(new Set([cnABC, cnAC])); + }); + + it('creates a correct suffix order for a different example profile', function () { + const { + funcNamesDict: { A, B, C }, + nonInvertedCallNodeInfo, + nodesWithSuffix, + } = setup(` + A A A C + B B + C + `); + + const cnABC = nonInvertedCallNodeInfo.getCallNodeIndexFromPath([A, B, C]); + const cnC = nonInvertedCallNodeInfo.getCallNodeIndexFromPath([C]); + + expect(nodesWithSuffix([B, C])).toEqual(new Set([cnABC])); + expect(nodesWithSuffix([C])).toEqual(new Set([cnABC, cnC])); + }); +}); + describe('symbolication', function () { describe('AddressLocator', function () { const libs = [ @@ -784,14 +884,14 @@ describe('funcHasDirectRecursiveCall and funcHasRecursiveCall', function () { thread.frameTable, thread.funcTable, defaultCategory - ).getCallNodeTable(); + ).getNonInvertedCallNodeTable(); const jsOnlyThread = filterThreadByImplementation(thread, 'js'); const jsOnlyCallNodeTable = getCallNodeInfo( jsOnlyThread.stackTable, jsOnlyThread.frameTable, jsOnlyThread.funcTable, defaultCategory - ).getCallNodeTable(); + ).getNonInvertedCallNodeTable(); return { callNodeTable, jsOnlyCallNodeTable, funcNames }; } @@ -874,22 +974,15 @@ describe('getSamplesSelectedStates', function () { ); const callNodeInfoInverted = getInvertedCallNodeInfo( - thread, callNodeInfo.getNonInvertedCallNodeTable(), stackIndexToCallNodeIndex, - defaultCategory - ); - const stackIndexToInvertedCallNodeIndex = - callNodeInfoInverted.getStackIndexToCallNodeIndex(); - const sampleInvertedCallNodes = getSampleIndexToCallNodeIndex( - thread.samples.stack, - stackIndexToInvertedCallNodeIndex + defaultCategory, + thread.funcTable.length ); return { callNodeInfo, callNodeInfoInverted, - sampleInvertedCallNodes, sampleCallNodes, funcNamesDict, }; @@ -971,7 +1064,7 @@ describe('getSamplesSelectedStates', function () { }); it('can sort the samples based on their selection status', function () { - const comparator = getTreeOrderComparator(sampleCallNodes); + const comparator = getTreeOrderComparator(sampleCallNodes, callNodeInfo); const samples = [4, 1, 3, 0, 2]; // some random order samples.sort(comparator); expect(samples).toEqual([0, 2, 4, 1, 3]); @@ -985,30 +1078,30 @@ describe('getSamplesSelectedStates', function () { describe('inverted', function () { /** - * - [cn0] A = A - * - [cn1] B = A -> B - * - [cn2] A = A -> B -> A - * - [cn3] C = A -> B -> C - * - [cn4] A = A -> A - * - [cn5] B = A -> A -> B - * - [cn6] C = A -> C - * + * - [cn0] A = A = A [so0] [so0] [cn0] A + * - [cn1] B = A -> B = A -> B [so3] [so1] [cn4] A <- A + * - [cn2] A = A -> B -> A = A -> B -> A [so2] ↘↗ [so2] [cn2] A <- B <- A + * - [cn3] C = A -> B -> C = A -> B -> C [so6] ↗↘ [so3] [cn1] B <- A + * - [cn4] A = A -> A = A -> A [so1] [so4] [cn5] B <- A <- A + * - [cn5] B = A -> A -> B = A -> A -> B [so4] [so5] [cn6] C <- A + * - [cn6] C = A -> C = A -> C [so5] [so6] [cn3] C <- B <- A * - * - [in0] A - * - [in1] A - * - [in2] B - * - [in3] A - * - [in4] B - * - [in5] A - * - [in6] A - * - [in7] C - * - [in8] A - * - [in9] B - * - [in10] A + * Represents call paths ending in + * - [in0] A (so:0..3) = A = ... A (cn0, cn4, cn2) + * - [in1] A (so:1..2) = A <- A = ... A -> A (cn4) + * - [in2] B (so:2..3) = A <- B = ... B -> A (cn2) + * - [in3] A (so:2..3) = A <- B <- A = ... A -> B -> A (cn2) + * - [in4] B (so:3..5) = B = ... B (cn1, cn5) + * - [in5] A (so:3..5) = B <- A = ... A -> B (cn1, cn5) + * - [in6] A (so:4..5) = B <- A <- A = ... A -> A -> B (cn5) + * - [in7] C (so:5..7) = C = ... C (cn6, cn3) + * - [in8] A (so:5..6) = C <- A = ... A -> C (cn6) + * - [in9] B (so:6..7) = C <- B = ... B -> C (cn3) + * - [in10] A (so:6..7) = C <- B <- A = ... A -> B -> C (cn3) * */ const { callNodeInfoInverted, - sampleInvertedCallNodes, + sampleCallNodes, funcNamesDict: { A, B, C }, } = setup(` A A A A A A A @@ -1031,8 +1124,8 @@ describe('getSamplesSelectedStates', function () { expect( getSamplesSelectedStates( callNodeInfoInverted, - sampleInvertedCallNodes, - sampleInvertedCallNodes, + sampleCallNodes, + sampleCallNodes, inBA ) ).toEqual([ @@ -1049,8 +1142,8 @@ describe('getSamplesSelectedStates', function () { expect( getSamplesSelectedStates( callNodeInfoInverted, - sampleInvertedCallNodes, - sampleInvertedCallNodes, + sampleCallNodes, + sampleCallNodes, inCBA ) ).toEqual([ @@ -1067,8 +1160,8 @@ describe('getSamplesSelectedStates', function () { expect( getSamplesSelectedStates( callNodeInfoInverted, - sampleInvertedCallNodes, - sampleInvertedCallNodes, + sampleCallNodes, + sampleCallNodes, inB ) ).toEqual([ @@ -1083,16 +1176,19 @@ describe('getSamplesSelectedStates', function () { }); it('can sort the samples based on their selection status', function () { - const comparator = getTreeOrderComparator(sampleInvertedCallNodes); + const comparator = getTreeOrderComparator( + sampleCallNodes, + callNodeInfoInverted + ); /** - * original order (non-inverted): + * in original order: * 0 1 2 3 4 5 6 * A A A A A A A * A B B C B A * A C B * - * sorted order ("inverted" if you read from bottom to top): + * in suffix order: * A A A * A B A A A B * A A A B B C C @@ -1424,10 +1520,10 @@ describe('getNativeSymbolsForCallNode', function () { defaultCategory ); const callNodeInfo = getInvertedCallNodeInfo( - thread, nonInvertedCallNodeInfo.getNonInvertedCallNodeTable(), nonInvertedCallNodeInfo.getStackIndexToNonInvertedCallNodeIndex(), - defaultCategory + defaultCategory, + thread.funcTable.length ); const c = callNodeInfo.getCallNodeIndexFromPath([funC]); expect(c).not.toBeNull(); diff --git a/src/test/unit/profile-tree.test.js b/src/test/unit/profile-tree.test.js index bf16e39c28..908a9c2f0a 100644 --- a/src/test/unit/profile-tree.test.js +++ b/src/test/unit/profile-tree.test.js @@ -8,7 +8,7 @@ import { } from '../fixtures/profiles/processed-profile'; import { getCallTree, - computeCallNodeLeafAndSummary, + computeCallNodeSelfAndSummary, computeCallTreeTimings, } from '../../profile-logic/call-tree'; import { computeFlameGraphRows } from '../../profile-logic/flame-graph'; @@ -69,21 +69,23 @@ describe('unfiltered call tree', function () { it('yields expected results', function () { const callTreeTimings = computeCallTreeTimings( callNodeInfo, - computeCallNodeLeafAndSummary( + computeCallNodeSelfAndSummary( thread.samples, getSampleIndexToCallNodeIndex( thread.samples.stack, - callNodeInfo.getStackIndexToCallNodeIndex() + callNodeInfo.getStackIndexToNonInvertedCallNodeIndex() ), - callNodeInfo.getCallNodeTable().length + callNodeInfo.getNonInvertedCallNodeTable().length ) ); expect(callTreeTimings).toEqual({ - rootTotalSummary: 3, - callNodeHasChildren: new Uint8Array([1, 1, 1, 1, 0, 1, 0, 1, 0]), - self: new Float32Array([0, 0, 0, 0, 1, 0, 1, 0, 1]), - leaf: new Float32Array([0, 0, 0, 0, 1, 0, 1, 0, 1]), - total: new Float32Array([3, 3, 2, 1, 1, 1, 1, 1, 1]), + type: 'NON_INVERTED', + timings: { + rootTotalSummary: 3, + callNodeHasChildren: new Uint8Array([1, 1, 1, 1, 0, 1, 0, 1, 0]), + self: new Float32Array([0, 0, 0, 0, 1, 0, 1, 0, 1]), + total: new Float32Array([3, 3, 2, 1, 1, 1, 1, 1, 1]), + }, }); }); }); @@ -420,13 +422,13 @@ describe('inverted call tree', function () { ); const callTreeTimings = computeCallTreeTimings( callNodeInfo, - computeCallNodeLeafAndSummary( + computeCallNodeSelfAndSummary( thread.samples, getSampleIndexToCallNodeIndex( thread.samples.stack, - callNodeInfo.getStackIndexToCallNodeIndex() + callNodeInfo.getStackIndexToNonInvertedCallNodeIndex() ), - callNodeInfo.getCallNodeTable().length + callNodeInfo.getNonInvertedCallNodeTable().length ) ); const callTree = getCallTree( @@ -455,20 +457,20 @@ describe('inverted call tree', function () { // Now compute the inverted tree and check it. const invertedCallNodeInfo = getInvertedCallNodeInfo( - thread, callNodeInfo.getNonInvertedCallNodeTable(), callNodeInfo.getStackIndexToNonInvertedCallNodeIndex(), - defaultCategory + defaultCategory, + thread.funcTable.length ); const invertedCallTreeTimings = computeCallTreeTimings( invertedCallNodeInfo, - computeCallNodeLeafAndSummary( + computeCallNodeSelfAndSummary( thread.samples, getSampleIndexToCallNodeIndex( thread.samples.stack, - invertedCallNodeInfo.getStackIndexToCallNodeIndex() + invertedCallNodeInfo.getStackIndexToNonInvertedCallNodeIndex() ), - invertedCallNodeInfo.getCallNodeTable().length + invertedCallNodeInfo.getNonInvertedCallNodeTable().length ) ); const invertedCallTree = getCallTree( @@ -612,16 +614,16 @@ describe('diffing trees', function () { ); const callTreeTimings = computeCallTreeTimings( callNodeInfo, - computeCallNodeLeafAndSummary( + computeCallNodeSelfAndSummary( thread.samples, getSampleIndexToCallNodeIndex( thread.samples.stack, - callNodeInfo.getStackIndexToCallNodeIndex() + callNodeInfo.getStackIndexToNonInvertedCallNodeIndex() ), - callNodeInfo.getCallNodeTable().length + callNodeInfo.getNonInvertedCallNodeTable().length ) ); - expect(callTreeTimings.rootTotalSummary).toBe(12); + expect(callTreeTimings.timings.rootTotalSummary).toBe(12); }); }); diff --git a/src/types/actions.js b/src/types/actions.js index 44e6c9ad43..2b928ed405 100644 --- a/src/types/actions.js +++ b/src/types/actions.js @@ -18,7 +18,6 @@ import type { import type { Thread, CallNodePath, - CallNodeInfo, GlobalTrack, LocalTrack, TrackIndex, @@ -32,6 +31,7 @@ import type { FuncToFuncsMap } from '../profile-logic/symbolication'; import type { TemporaryError } from '../utils/errors'; import type { Transform, TransformStacksPerThread } from './transforms'; import type { IndexIntoZipFileTable } from '../profile-logic/zip-files'; +import type { CallNodeInfo } from '../profile-logic/call-node-info'; import type { TabSlug } from '../app-logic/tabs-handling'; import type { PseudoStrategy, diff --git a/src/types/profile-derived.js b/src/types/profile-derived.js index bec4c24762..b504632c7e 100644 --- a/src/types/profile-derived.js +++ b/src/types/profile-derived.js @@ -12,7 +12,6 @@ import type { IndexIntoJsTracerEvents, IndexIntoCategoryList, IndexIntoResourceTable, - IndexIntoNativeSymbolTable, IndexIntoLibs, CounterIndex, GraphColor, @@ -292,10 +291,10 @@ export type CallNodeTable = { category: Int32Array, // IndexIntoCallNodeTable -> IndexIntoCategoryList subcategory: Int32Array, // IndexIntoCallNodeTable -> IndexIntoSubcategoryListForCategory innerWindowID: Float64Array, // IndexIntoCallNodeTable -> InnerWindowID - // null: no inlining // IndexIntoNativeSymbolTable: all frames that collapsed into this call node inlined into the same native symbol // -1: divergent: not all frames that collapsed into this call node were inlined, or they are from different symbols - sourceFramesInlinedIntoSymbol: Array, + // -2: no inlining + sourceFramesInlinedIntoSymbol: Int32Array, // The depth of the call node. Roots have depth 0. depth: number[], // The maximum value in the depth column, or -1 if this table is empty. @@ -304,63 +303,6 @@ export type CallNodeTable = { length: number, }; -/** - * Wraps the call node table and provides associated functionality. - */ -export interface CallNodeInfo { - // If true, call node indexes describe nodes in the inverted call tree. - isInverted(): boolean; - - // Returns the call node table. If isInverted() is true, this is an inverted - // call node table, otherwise this is the non-inverted call node table. - getCallNodeTable(): CallNodeTable; - - // Returns the non-inverted call node table. - // This is always the non-inverted call node table, regardless of isInverted(). - getNonInvertedCallNodeTable(): CallNodeTable; - - // Returns a mapping from the stack table to the call node table. - // The Int32Array should be used as if it were a - // Map. - // - // If this CallNodeInfo is for the non-inverted tree, this maps the stack index - // to its corresponding call node index, and all entries are >= 0. - // If this CallNodeInfo is for the inverted tree, this maps the non-inverted - // stack index to the inverted call node index. For example, the stack - // A -> B -> C -> D is mapped to the inverted call node describing the - // call path D <- C <- B <- A, i.e. the node with function A under the D root - // of the inverted tree. Stacks which are only used as prefixes are not mapped - // to an inverted call node; for those, the entry will be -1. In the example - // above, if the stack node A -> B -> C only exists so that it can be the prefix - // of the A -> B -> C -> D stack and no sample / marker / allocation has - // A -> B -> C as its stack, then there is no need to have a call node - // C <- B <- A in the inverted call node table. - getStackIndexToCallNodeIndex(): Int32Array; - - // Returns a mapping from the stack table to the non-inverted call node table. - // This always maps to the non-inverted call node table, regardless of isInverted(). - getStackIndexToNonInvertedCallNodeIndex(): Int32Array; - - // Converts a call node index into a call node path. - getCallNodePathFromIndex( - callNodeIndex: IndexIntoCallNodeTable | null - ): CallNodePath; - - // Converts a call node path into a call node index. - getCallNodeIndexFromPath( - callNodePath: CallNodePath - ): IndexIntoCallNodeTable | null; - - // Returns the call node index that matches the function `func` and whose - // parent's index is `parent`. If `parent` is -1, this returns the index of - // the root node with function `func`. - // Returns null if the described call node doesn't exist. - getCallNodeIndexFromParentAndFunc( - parent: IndexIntoCallNodeTable | -1, - func: IndexIntoFuncTable - ): IndexIntoCallNodeTable | null; -} - export type LineNumber = number; // Stores the line numbers which are hit by each stack, for one specific source @@ -856,11 +798,11 @@ export type SortedTabPageData = Array<{| pageData: ProfileFilterPageData, |}>; -export type CallNodeLeafAndSummary = {| - // This property stores the amount of unit (time, bytes, count, etc.) spent in the - // stacks' leaf nodes. - callNodeLeaf: Float32Array, - // The sum of absolute values in callNodeLeaf. +export type CallNodeSelfAndSummary = {| + // This property stores the amount of unit (time, bytes, count, etc.) spent in + // this call node and not in any of its descendant nodes. + callNodeSelf: Float32Array, + // The sum of absolute values in callNodeSelf. // This is used for computing the percentages displayed in the call tree. rootTotalSummary: number, |}; diff --git a/src/utils/path.js b/src/utils/path.js index 7bc9bc8d09..814d741858 100644 --- a/src/utils/path.js +++ b/src/utils/path.js @@ -4,7 +4,7 @@ // @flow -import type { CallNodePath } from 'firefox-profiler/types'; +import type { CallNodePath, IndexIntoFuncTable } from 'firefox-profiler/types'; export function arePathsEqual(a: CallNodePath, b: CallNodePath): boolean { if (a === b) { @@ -34,6 +34,17 @@ export function hashPath(a: CallNodePath): string { return a.join('-'); } +export function concatHash( + hash: string, + extraFunc: IndexIntoFuncTable +): string { + return hash + '-' + extraFunc; +} + +export function hashPathSingleFunc(func: IndexIntoFuncTable): string { + return '' + func; +} + // This class implements all of the methods of the native Set, but provides a // unique list of CallNodePaths. These paths can be different objects, but as // long as they contain the same data, they are considered to be the same.