From db716a591e0e33332fdb4b126fe24d8a426db359 Mon Sep 17 00:00:00 2001 From: Markus Stange Date: Tue, 23 Jan 2024 11:45:42 -0500 Subject: [PATCH 01/31] Add CallNodeInfoInverted. This just adds a new interface that we can hang functionality off of which is specific to the inverted tree. No functional changes. --- src/profile-logic/call-node-info.js | 50 +++++++++++++++---- src/profile-logic/profile-data.js | 13 +++-- .../__snapshots__/profile-view.test.js.snap | 3 -- src/types/profile-derived.js | 13 +++++ 4 files changed, 58 insertions(+), 21 deletions(-) diff --git a/src/profile-logic/call-node-info.js b/src/profile-logic/call-node-info.js index cf28cd6747..3b42803169 100644 --- a/src/profile-logic/call-node-info.js +++ b/src/profile-logic/call-node-info.js @@ -9,6 +9,7 @@ import { hashPath } from 'firefox-profiler/utils/path'; import type { IndexIntoFuncTable, CallNodeInfo, + CallNodeInfoInverted, CallNodeTable, CallNodePath, IndexIntoCallNodeTable, @@ -16,26 +17,27 @@ import type { /** * The implementation of the CallNodeInfo interface. + * + * CallNodeInfoInvertedImpl inherits from this class and shares this implementation. + * By the end of this commit stack, it will no longer inherit from this class and + * will have its own implementation. */ export class CallNodeInfoImpl implements CallNodeInfo { - // If true, call node indexes describe nodes in the inverted call tree. - _isInverted: boolean; - // The call node table. This is either the inverted or the non-inverted call - // node table, depending on _isInverted. + // node table, depending on isInverted(). _callNodeTable: CallNodeTable; - // The non-inverted call node table, regardless of _isInverted. + // The non-inverted call node table, regardless of isInverted(). _nonInvertedCallNodeTable: CallNodeTable; // 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. + // isInverted(). _stackIndexToCallNodeIndex: Int32Array; // 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. + // isInverted(). _stackIndexToNonInvertedCallNodeIndex: Int32Array; // This is a Map. This map speeds up @@ -47,19 +49,23 @@ export class CallNodeInfoImpl implements CallNodeInfo { 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; + // Overridden in subclass + return false; + } + + asInverted(): CallNodeInfoInverted | null { + // Overridden in subclass + return null; } getCallNodeTable(): CallNodeTable { @@ -202,3 +208,25 @@ export class CallNodeInfoImpl implements CallNodeInfo { return null; } } + +/** + * A subclass of CallNodeInfoImpl for "invert call stack" mode. + * + * This currently shares its implementation with CallNodeInfoImpl; + * this._callNodeTable is the inverted call node table. + * + * By the end of this commit stack, we will no longer have an inverted call node + * table and this class will stop inheriting from CallNodeInfoImpl. + */ +export class CallNodeInfoInvertedImpl + extends CallNodeInfoImpl + implements CallNodeInfoInverted +{ + isInverted(): boolean { + return true; + } + + asInverted(): CallNodeInfoInverted | null { + return this; + } +} diff --git a/src/profile-logic/profile-data.js b/src/profile-logic/profile-data.js index 002b3d730a..eb5a8263a7 100644 --- a/src/profile-logic/profile-data.js +++ b/src/profile-logic/profile-data.js @@ -14,7 +14,7 @@ import { shallowCloneFrameTable, shallowCloneFuncTable, } from './data-structures'; -import { CallNodeInfoImpl } from './call-node-info'; +import { CallNodeInfoImpl, CallNodeInfoInvertedImpl } from './call-node-info'; import { computeThreadCPURatio } from './cpu'; import { INSTANT, @@ -71,6 +71,7 @@ import type { IndexIntoFrameTable, PageList, CallNodeInfo, + CallNodeInfoInverted, CallNodeTable, CallNodePath, CallNodeAndCategoryPath, @@ -121,8 +122,7 @@ export function getCallNodeInfo( callNodeTable, callNodeTable, stackIndexToCallNodeIndex, - stackIndexToCallNodeIndex, - false + stackIndexToCallNodeIndex ); } @@ -444,7 +444,7 @@ export function getInvertedCallNodeInfo( nonInvertedCallNodeTable: CallNodeTable, stackIndexToNonInvertedCallNodeIndex: Int32Array, defaultCategory: IndexIntoCategoryList -): CallNodeInfo { +): CallNodeInfoInverted { // We compute an inverted stack table, but we don't let it escape this function. const { invertedThread, @@ -486,12 +486,11 @@ export function getInvertedCallNodeInfo( invertedStackIndexToCallNodeIndex[invertedStackIndex]; } } - return new CallNodeInfoImpl( + return new CallNodeInfoInvertedImpl( callNodeTable, nonInvertedCallNodeTable, nonInvertedStackIndexToCallNodeIndex, - stackIndexToNonInvertedCallNodeIndex, - true + stackIndexToNonInvertedCallNodeIndex ); } diff --git a/src/test/store/__snapshots__/profile-view.test.js.snap b/src/test/store/__snapshots__/profile-view.test.js.snap index 73dfbdab27..d7da837f60 100644 --- a/src/test/store/__snapshots__/profile-view.test.js.snap +++ b/src/test/store/__snapshots__/profile-view.test.js.snap @@ -2235,7 +2235,6 @@ CallNodeInfoImpl { 9, ], }, - "_isInverted": false, "_nonInvertedCallNodeTable": Object { "category": Int32Array [ 0, @@ -2471,7 +2470,6 @@ CallTree { 9, ], }, - "_isInverted": false, "_nonInvertedCallNodeTable": Object { "category": Int32Array [ 0, @@ -2878,7 +2876,6 @@ CallTree { 9, ], }, - "_isInverted": false, "_nonInvertedCallNodeTable": Object { "category": Int32Array [ 0, diff --git a/src/types/profile-derived.js b/src/types/profile-derived.js index bec4c24762..32c611e60f 100644 --- a/src/types/profile-derived.js +++ b/src/types/profile-derived.js @@ -311,6 +311,9 @@ export interface CallNodeInfo { // If true, call node indexes describe nodes in the inverted call tree. isInverted(): boolean; + // Returns this object as CallNodeInfoInverted if isInverted(), otherwise null. + asInverted(): CallNodeInfoInverted | null; + // 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; @@ -361,6 +364,16 @@ export interface CallNodeInfo { ): IndexIntoCallNodeTable | null; } +// An index into SuffixOrderedCallNodes. +export type SuffixOrderIndex = number; + +/** + * A sub-interface of CallNodeInfo with additional functionality for the inverted + * call tree. + */ +export interface CallNodeInfoInverted extends CallNodeInfo { +} + export type LineNumber = number; // Stores the line numbers which are hit by each stack, for one specific source From 68bbf6bdc010cfe2301a4e1b91f99bff5daf2321 Mon Sep 17 00:00:00 2001 From: Markus Stange Date: Tue, 6 Aug 2024 14:58:34 -0400 Subject: [PATCH 02/31] Implement the "suffix order". This is the main new concept in this PR that allows us to make the inverted tree fast. See the comment above CallNodeInfoInverted in src/types/profile-derived.js for details. The PR is structured as follows: - Implement the suffix order in a brute force manner (this commit). - Use the suffix order to re-implement everything that was using the inverted call node table. - Once nothing is using the inverted call node table directly anymore, make it fast. We make it fast by rewriting the computation of the inverted call node table and of the suffix order so that we only materialize inverted call nodes that are displayed in the call tree, and not for every sample. And we only compute the suffix order to the level of precision needed to have correct ranges for all materialized inverted call nodes. --- src/profile-logic/call-node-info.js | 55 +++++++++++ src/profile-logic/profile-data.js | 87 ++++++++++++++++- src/test/unit/profile-data.test.js | 100 ++++++++++++++++++++ src/types/profile-derived.js | 139 ++++++++++++++++++++++++++++ src/utils/bisect.js | 129 ++++++++++++++++++++++++++ 5 files changed, 509 insertions(+), 1 deletion(-) diff --git a/src/profile-logic/call-node-info.js b/src/profile-logic/call-node-info.js index 3b42803169..b1ba5634ec 100644 --- a/src/profile-logic/call-node-info.js +++ b/src/profile-logic/call-node-info.js @@ -5,6 +5,8 @@ // @flow import { hashPath } from 'firefox-profiler/utils/path'; +import { bisectEqualRange } from 'firefox-profiler/utils/bisect'; +import { compareNonInvertedCallNodesInSuffixOrderWithPath } from 'firefox-profiler/profile-logic/profile-data'; import type { IndexIntoFuncTable, @@ -13,6 +15,7 @@ import type { CallNodeTable, CallNodePath, IndexIntoCallNodeTable, + SuffixOrderIndex, } from 'firefox-profiler/types'; /** @@ -222,6 +225,32 @@ export class CallNodeInfoInvertedImpl extends CallNodeInfoImpl implements CallNodeInfoInverted { + // 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; + + constructor( + callNodeTable: CallNodeTable, + nonInvertedCallNodeTable: CallNodeTable, + stackIndexToCallNodeIndex: Int32Array, + stackIndexToNonInvertedCallNodeIndex: Int32Array, + suffixOrderedCallNodes: Uint32Array, + suffixOrderIndexes: Uint32Array + ) { + super( + callNodeTable, + nonInvertedCallNodeTable, + stackIndexToCallNodeIndex, + stackIndexToNonInvertedCallNodeIndex + ); + this._suffixOrderedCallNodes = suffixOrderedCallNodes; + this._suffixOrderIndexes = suffixOrderIndexes; + } + isInverted(): boolean { return true; } @@ -229,4 +258,30 @@ export class CallNodeInfoInvertedImpl asInverted(): CallNodeInfoInverted | null { return this; } + + getSuffixOrderedCallNodes(): Uint32Array { + return this._suffixOrderedCallNodes; + } + + getSuffixOrderIndexes(): Uint32Array { + return this._suffixOrderIndexes; + } + + getSuffixOrderIndexRangeForCallNode( + callNodeIndex: IndexIntoCallNodeTable + ): [SuffixOrderIndex, SuffixOrderIndex] { + // `callNodeIndex` is an inverted call node. Translate it to a call path. + const callPath = this.getCallNodePathFromIndex(callNodeIndex); + return bisectEqualRange( + this._suffixOrderedCallNodes, + // comparedCallNodeIndex is a non-inverted call node. Compare it to the + // call path for our inverted call node. + (comparedCallNodeIndex) => + compareNonInvertedCallNodesInSuffixOrderWithPath( + comparedCallNodeIndex, + callPath, + this._nonInvertedCallNodeTable + ) + ); + } } diff --git a/src/profile-logic/profile-data.js b/src/profile-logic/profile-data.js index eb5a8263a7..c71a76f695 100644 --- a/src/profile-logic/profile-data.js +++ b/src/profile-logic/profile-data.js @@ -486,14 +486,99 @@ export function getInvertedCallNodeInfo( invertedStackIndexToCallNodeIndex[invertedStackIndex]; } } + + // TEMPORARY: Compute a suffix order for the entire non-inverted call node table. + // See the CallNodeInfoInverted interface for more details about the suffix order. + // By the end of this commit stack, the suffix order will be computed incrementally + // as inverted nodes are created; we won't compute the entire order upfront. + const nonInvertedCallNodeCount = nonInvertedCallNodeTable.length; + const suffixOrderedCallNodes = new Uint32Array(nonInvertedCallNodeCount); + const suffixOrderIndexes = new Uint32Array(nonInvertedCallNodeCount); + for (let i = 0; i < nonInvertedCallNodeCount; i++) { + suffixOrderedCallNodes[i] = i; + } + suffixOrderedCallNodes.sort((a, b) => + _compareNonInvertedCallNodesInSuffixOrder(a, b, nonInvertedCallNodeTable) + ); + for (let i = 0; i < suffixOrderedCallNodes.length; i++) { + suffixOrderIndexes[suffixOrderedCallNodes[i]] = i; + } + return new CallNodeInfoInvertedImpl( callNodeTable, nonInvertedCallNodeTable, nonInvertedStackIndexToCallNodeIndex, - stackIndexToNonInvertedCallNodeIndex + stackIndexToNonInvertedCallNodeIndex, + suffixOrderedCallNodes, + suffixOrderIndexes ); } +// 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 0; +} + +// Same as _compareNonInvertedCallNodesInSuffixOrder, but takes a call path for +// callNodeB. This is used in the getSuffixOrderIndexRangeForCallNode implementation +// of CallNodeInfoInvertedImpl, which doesn't have easy access to the non-inverted +// call node index for callPathB. +export function compareNonInvertedCallNodesInSuffixOrderWithPath( + callNodeA: IndexIntoCallNodeTable, + callPathB: CallNodePath, + nonInvertedCallNodeTable: CallNodeTable +): number { + for (let i = 0; i < callPathB.length - 1; i++) { + const funcA = nonInvertedCallNodeTable.func[callNodeA]; + const funcB = callPathB[i]; + if (funcA !== funcB) { + return funcA - funcB; + } + callNodeA = nonInvertedCallNodeTable.prefix[callNodeA]; + if (callNodeA === -1) { + return -1; + } + } + const funcA = nonInvertedCallNodeTable.func[callNodeA]; + const funcB = callPathB[callPathB.length - 1]; + return funcA - funcB; +} + // Given a stack index `needleStack` and a call node in the inverted tree // `invertedCallTreeNode`, find an ancestor stack of `needleStack` which // corresponds to the given call node in the inverted call tree. Returns null if diff --git a/src/test/unit/profile-data.test.js b/src/test/unit/profile-data.test.js index acf112cf31..81f9a1ba2b 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( + thread, + nonInvertedCallNodeInfo.getNonInvertedCallNodeTable(), + nonInvertedCallNodeInfo.getStackIndexToNonInvertedCallNodeIndex(), + defaultCategory + ); + + // 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 = [ diff --git a/src/types/profile-derived.js b/src/types/profile-derived.js index 32c611e60f..cbef0c5645 100644 --- a/src/types/profile-derived.js +++ b/src/types/profile-derived.js @@ -370,8 +370,147 @@ export type SuffixOrderIndex = number; /** * A sub-interface of CallNodeInfo 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 + * 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) + * - [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) + * ``` + * + * 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) + * */ export interface CallNodeInfoInverted extends CallNodeInfo { + // 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. + getSuffixOrderedCallNodes(): Uint32Array; + + // Returns the inverse of getSuffixOrderedCallNodes(), i.e. a mapping + // IndexIntoNonInvertedCallNodeTable -> SuffixOrderIndex. + getSuffixOrderIndexes(): Uint32Array; + + // 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( + callNodeIndex: IndexIntoCallNodeTable + ): [SuffixOrderIndex, SuffixOrderIndex]; } export type LineNumber = number; diff --git a/src/utils/bisect.js b/src/utils/bisect.js index 16dcc4e724..05fe86adbd 100644 --- a/src/utils/bisect.js +++ b/src/utils/bisect.js @@ -208,3 +208,132 @@ export function bisectionLeft( return low; } + +/* + * TEMPORARY: The functions below implement bisectEqualRange(). The implementation + * is copied from https://searchfox.org/mozilla-central/rev/8b0666aff1197e1dd8017de366343de9c21ee437/mfbt/BinarySearch.h#132-243 + * The only code calling bisectEqualRange will be removed by the end of this + * commit stack, so all the code added here will be removed again, too. + * + * bisectLowerBound(), bisectUpperBound(), and bisectEqualRange() are equivalent to + * std::lower_bound(), std::upper_bound(), and std::equal_range() respectively. + * + * bisectLowerBound() returns an index pointing to the first element in the range + * in which each element is considered *not less than* the given value passed + * via |aCompare|, or the length of |aContainer| if no such element is found. + * + * bisectUpperBound() returns an index pointing to the first element in the range + * in which each element is considered *greater than* the given value passed + * via |aCompare|, or the length of |aContainer| if no such element is found. + * + * bisectEqualRange() returns a range [first, second) containing all elements are + * considered equivalent to the given value via |aCompare|. If you need + * either the first or last index of the range, bisectLowerBound() or bisectUpperBound(), + * which is slightly faster than bisectEqualRange(), should suffice. + * + * Example (another example is given in TestBinarySearch.cpp): + * + * Vector sortedStrings = ... + * + * struct Comparator { + * const nsACString& mStr; + * explicit Comparator(const nsACString& aStr) : mStr(aStr) {} + * int32_t operator()(const char* aVal) const { + * return Compare(mStr, nsDependentCString(aVal)); + * } + * }; + * + * auto bounds = bisectEqualRange(sortedStrings, 0, sortedStrings.length(), + * Comparator("needle I'm looking for"_ns)); + * printf("Found the range [%zd %zd)\n", bounds.first(), bounds.second()); + * + */ +export function bisectLowerBound( + array: number[] | $TypedArray, + f: (number) => number, // < 0 if arg is before needle, > 0 if after, === 0 if same + low?: number, + high?: number +): number { + low = low || 0; + high = high || array.length; + + if (low < 0 || low > array.length || high < 0 || high > array.length) { + throw new TypeError("low and high must lie within the array's range"); + } + + while (high !== low) { + const middle = (low + high) >> 1; + const result = f(array[middle]); + + // The range returning from bisectLowerBound does include elements + // equivalent to the given value i.e. f(element) == 0 + if (result >= 0) { + high = middle; + } else { + low = middle + 1; + } + } + + return low; +} + +export function bisectUpperBound( + array: number[] | $TypedArray, + f: (number) => number, // < 0 if arg is before needle, > 0 if after, === 0 if same + low?: number, + high?: number +): number { + low = low || 0; + high = high || array.length; + + if (low < 0 || low > array.length || high < 0 || high > array.length) { + throw new TypeError("low and high must lie within the array's range"); + } + + while (high !== low) { + const middle = (low + high) >> 1; + const result = f(array[middle]); + + // The range returning from bisectUpperBound does NOT include elements + // equivalent to the given value i.e. f(element) == 0 + if (result > 0) { + high = middle; + } else { + low = middle + 1; + } + } + + return high; +} + +export function bisectEqualRange( + array: number[] | $TypedArray, + f: (number) => number, // < 0 if arg is before needle, > 0 if after, === 0 if same + low?: number, + high?: number +): [number, number] { + low = low || 0; + high = high || array.length; + + if (low < 0 || low > array.length || high < 0 || high > array.length) { + throw new TypeError("low and high must lie within the array's range"); + } + + while (high !== low) { + const middle = (low + high) >> 1; + const result = f(array[middle]); + + if (result > 0) { + high = middle; + } else if (result < 0) { + low = middle + 1; + } else { + return [ + bisectLowerBound(array, f, low, middle), + bisectUpperBound(array, f, middle + 1, high), + ]; + } + } + + return [low, high]; +} From 2a3c8889251b2ce6c18e8145e21fe517992a4109 Mon Sep 17 00:00:00 2001 From: Markus Stange Date: Fri, 19 Jan 2024 17:44:00 -0500 Subject: [PATCH 03/31] Use the suffix order in getMatchingAncestorStackForInvertedCallNode. This function is used by getNativeSymbolsForCallNodeInverted, getStackAddressInfoForCallNodeInverted, and getStackLineInfoForCallNodeInverted. This replaces a call to getStackIndexToCallNodeIndex() with a call to getStackIndexToNonInvertedCallNodeIndex(). It also mostly removes the use of the inverted call node table for this code. (There's still a place that accesses callNodeInfo.getCallNodeTable().depth, but this will be fixed in a later commit.) We want to eliminate all callers to getStackIndexToCallNodeIndex() because we don't want to compute a mapping from non-inverted stack index to inverted call node index upfront. --- src/profile-logic/address-timings.js | 26 ++++---- src/profile-logic/call-node-info.js | 4 ++ src/profile-logic/line-timings.js | 26 ++++---- src/profile-logic/profile-data.js | 94 ++++++++++++++++------------ src/types/profile-derived.js | 3 + 5 files changed, 91 insertions(+), 62 deletions(-) diff --git a/src/profile-logic/address-timings.js b/src/profile-logic/address-timings.js index 00080a3f3d..2e94663a2a 100644 --- a/src/profile-logic/address-timings.js +++ b/src/profile-logic/address-timings.js @@ -75,6 +75,7 @@ import type { StackTable, SamplesLikeTable, CallNodeInfo, + CallNodeInfoInverted, IndexIntoCallNodeTable, IndexIntoNativeSymbolTable, StackAddressInfo, @@ -202,12 +203,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 +428,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.getCallNodeTable().depth[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 +452,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 b1ba5634ec..a10dc44fb4 100644 --- a/src/profile-logic/call-node-info.js +++ b/src/profile-logic/call-node-info.js @@ -210,6 +210,10 @@ export class CallNodeInfoImpl implements CallNodeInfo { return null; } + + isRoot(callNodeIndex: IndexIntoCallNodeTable): boolean { + return this._callNodeTable.prefix[callNodeIndex] === -1; + } } /** diff --git a/src/profile-logic/line-timings.js b/src/profile-logic/line-timings.js index 0d0065f25c..3d0fd3443b 100644 --- a/src/profile-logic/line-timings.js +++ b/src/profile-logic/line-timings.js @@ -10,6 +10,7 @@ import type { StackTable, SamplesLikeTable, CallNodeInfo, + CallNodeInfoInverted, IndexIntoCallNodeTable, IndexIntoStringTable, StackLineInfo, @@ -125,12 +126,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 +284,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.getCallNodeTable().depth[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 +307,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 c71a76f695..b1da4c2a4c 100644 --- a/src/profile-logic/profile-data.js +++ b/src/profile-logic/profile-data.js @@ -96,6 +96,7 @@ import type { Bytes, ThreadWithReservedFunctions, TabID, + SuffixOrderIndex, } from 'firefox-profiler/types'; /** @@ -586,6 +587,17 @@ export function compareNonInvertedCallNodesInSuffixOrderWithPath( // // 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: @@ -613,33 +625,32 @@ export function compareNonInvertedCallNodesInSuffixOrderWithPath( // 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, @@ -647,7 +658,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; } @@ -3970,20 +3981,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( @@ -4009,21 +4020,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.getCallNodeTable().depth[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 diff --git a/src/types/profile-derived.js b/src/types/profile-derived.js index cbef0c5645..b34d107540 100644 --- a/src/types/profile-derived.js +++ b/src/types/profile-derived.js @@ -362,6 +362,9 @@ export interface CallNodeInfo { parent: IndexIntoCallNodeTable | -1, func: IndexIntoFuncTable ): IndexIntoCallNodeTable | null; + + // Returns whether the given node is a root node. + isRoot(callNodeIndex: IndexIntoCallNodeTable): boolean; } // An index into SuffixOrderedCallNodes. From f5e3ad188e7b01813095bbf38f6307a6706771c6 Mon Sep 17 00:00:00 2001 From: Markus Stange Date: Fri, 19 Jan 2024 18:42:29 -0500 Subject: [PATCH 04/31] Implement getSamplesSelectedStates for inverted trees with the suffix order. This replaces a call to getStackIndexToCallNodeIndex() with a call to getStackIndexToNonInvertedCallNodeIndex(). It also removes a call to getCallNodeTable(). And it replaces a SampleIndexToCallNodeIndex mapping with a SampleIndexToNonInvertedCallNodeIndex mapping. --- src/profile-logic/profile-data.js | 84 +++++++++++++++++++----- src/selectors/per-thread/stack-sample.js | 21 +++--- src/test/unit/profile-data.test.js | 13 ++-- 3 files changed, 85 insertions(+), 33 deletions(-) diff --git a/src/profile-logic/profile-data.js b/src/profile-logic/profile-data.js index b1da4c2a4c..af9c7e238e 100644 --- a/src/profile-logic/profile-data.js +++ b/src/profile-logic/profile-data.js @@ -698,7 +698,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[] { @@ -731,7 +731,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 @@ -777,12 +777,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++) { @@ -810,6 +813,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. @@ -819,24 +864,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 + ); } /** diff --git a/src/selectors/per-thread/stack-sample.js b/src/selectors/per-thread/stack-sample.js index 7240710930..5201e90471 100644 --- a/src/selectors/per-thread/stack-sample.js +++ b/src/selectors/per-thread/stack-sample.js @@ -262,35 +262,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 ); } @@ -454,7 +454,6 @@ export function getStackAndSampleSelectorsPerThread( getExpandedCallNodeIndexes, getSampleIndexToCallNodeIndexForFilteredThread, getSampleIndexToNonInvertedCallNodeIndexForFilteredThread, - getSampleIndexToCallNodeIndexForTabFilteredThread, getSamplesSelectedStatesInFilteredThread, getTreeOrderComparatorInFilteredThread, getCallTree, diff --git a/src/test/unit/profile-data.test.js b/src/test/unit/profile-data.test.js index 81f9a1ba2b..c63d70355a 100644 --- a/src/test/unit/profile-data.test.js +++ b/src/test/unit/profile-data.test.js @@ -1108,6 +1108,7 @@ describe('getSamplesSelectedStates', function () { * */ const { callNodeInfoInverted, + sampleCallNodes, sampleInvertedCallNodes, funcNamesDict: { A, B, C }, } = setup(` @@ -1131,8 +1132,8 @@ describe('getSamplesSelectedStates', function () { expect( getSamplesSelectedStates( callNodeInfoInverted, - sampleInvertedCallNodes, - sampleInvertedCallNodes, + sampleCallNodes, + sampleCallNodes, inBA ) ).toEqual([ @@ -1149,8 +1150,8 @@ describe('getSamplesSelectedStates', function () { expect( getSamplesSelectedStates( callNodeInfoInverted, - sampleInvertedCallNodes, - sampleInvertedCallNodes, + sampleCallNodes, + sampleCallNodes, inCBA ) ).toEqual([ @@ -1167,8 +1168,8 @@ describe('getSamplesSelectedStates', function () { expect( getSamplesSelectedStates( callNodeInfoInverted, - sampleInvertedCallNodes, - sampleInvertedCallNodes, + sampleCallNodes, + sampleCallNodes, inB ) ).toEqual([ From 3ac08b9bd40a56dc150c28f2e4ec49e4302b2454 Mon Sep 17 00:00:00 2001 From: Markus Stange Date: Fri, 19 Jan 2024 19:11:41 -0500 Subject: [PATCH 05/31] Use suffix order in getTimingsForCallNodeIndex. This replaces a call to getStackIndexToCallNodeIndex() with a call to getStackIndexToNonInvertedCallNodeIndex(). It also removes a call to getCallNodeTable(). --- src/profile-logic/profile-data.js | 103 +++++++++++++++++++----------- 1 file changed, 65 insertions(+), 38 deletions(-) diff --git a/src/profile-logic/profile-data.js b/src/profile-logic/profile-data.js index af9c7e238e..ba204b85db 100644 --- a/src/profile-logic/profile-data.js +++ b/src/profile-logic/profile-data.js @@ -1164,51 +1164,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); } } } From 9f1c8f311d03b4c451080b75e6a96a7a805bafe0 Mon Sep 17 00:00:00 2001 From: Markus Stange Date: Sat, 20 Jan 2024 00:00:33 -0500 Subject: [PATCH 06/31] Implement getTreeOrderComparator for the inverted tree with non-inverted call nodes. This function is used when hovering or clicking the activity graph. This commit replaces a SampleIndexToCallNodeIndex mapping with a SampleIndexToNonInvertedCallNodeIndex mapping. --- src/profile-logic/profile-data.js | 45 +++++++++++++++++++ src/selectors/per-thread/stack-sample.js | 3 +- src/test/unit/profile-data.test.js | 57 +++++++++++------------- 3 files changed, 73 insertions(+), 32 deletions(-) diff --git a/src/profile-logic/profile-data.js b/src/profile-logic/profile-data.js index ba204b85db..d3d6fd4942 100644 --- a/src/profile-logic/profile-data.js +++ b/src/profile-logic/profile-data.js @@ -3230,6 +3230,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 { /** @@ -3263,6 +3276,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 { diff --git a/src/selectors/per-thread/stack-sample.js b/src/selectors/per-thread/stack-sample.js index 5201e90471..3ec25ae88a 100644 --- a/src/selectors/per-thread/stack-sample.js +++ b/src/selectors/per-thread/stack-sample.js @@ -299,7 +299,8 @@ export function getStackAndSampleSelectorsPerThread( const getTreeOrderComparatorInFilteredThread: Selector< (IndexIntoSamplesTable, IndexIntoSamplesTable) => number, > = createSelector( - getSampleIndexToCallNodeIndexForFilteredThread, + getSampleIndexToNonInvertedCallNodeIndexForFilteredThread, + getCallNodeInfo, ProfileData.getTreeOrderComparator ); diff --git a/src/test/unit/profile-data.test.js b/src/test/unit/profile-data.test.js index c63d70355a..fae1c78ad2 100644 --- a/src/test/unit/profile-data.test.js +++ b/src/test/unit/profile-data.test.js @@ -979,17 +979,10 @@ describe('getSamplesSelectedStates', function () { stackIndexToCallNodeIndex, defaultCategory ); - const stackIndexToInvertedCallNodeIndex = - callNodeInfoInverted.getStackIndexToCallNodeIndex(); - const sampleInvertedCallNodes = getSampleIndexToCallNodeIndex( - thread.samples.stack, - stackIndexToInvertedCallNodeIndex - ); return { callNodeInfo, callNodeInfoInverted, - sampleInvertedCallNodes, sampleCallNodes, funcNamesDict, }; @@ -1071,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]); @@ -1085,31 +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, sampleCallNodes, - sampleInvertedCallNodes, funcNamesDict: { A, B, C }, } = setup(` A A A A A A A @@ -1184,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 From cb7a9ce52970e7ce3986d77a827d1d5e92ba5ae4 Mon Sep 17 00:00:00 2001 From: Markus Stange Date: Sat, 20 Jan 2024 00:04:38 -0500 Subject: [PATCH 07/31] Use SampleIndexToNonInvertedCallNodeIndex for the stack chart. The stack chart is always non-inverted, so this commit is functionally neutral. This lets us remove the now-unused function getSampleIndexToCallNodeIndexForFilteredThread. --- src/selectors/per-thread/stack-sample.js | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/src/selectors/per-thread/stack-sample.js b/src/selectors/per-thread/stack-sample.js index 3ec25ae88a..2b90dc89a3 100644 --- a/src/selectors/per-thread/stack-sample.js +++ b/src/selectors/per-thread/stack-sample.js @@ -230,18 +230,6 @@ 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< Array, > = createSelector( @@ -402,7 +390,7 @@ export function getStackAndSampleSelectorsPerThread( const getStackTimingByDepth: Selector = createSelector( threadSelectors.getFilteredCtssSamples, - getSampleIndexToCallNodeIndexForFilteredThread, // Bug! #5327 + getSampleIndexToNonInvertedCallNodeIndexForFilteredThread, // Bug! #5327 getCallNodeInfo, getFilteredCallNodeMaxDepthPlusOne, ProfileSelectors.getProfileInterval, @@ -453,7 +441,6 @@ export function getStackAndSampleSelectorsPerThread( getSelectedCallNodeIndex, getExpandedCallNodePaths, getExpandedCallNodeIndexes, - getSampleIndexToCallNodeIndexForFilteredThread, getSampleIndexToNonInvertedCallNodeIndexForFilteredThread, getSamplesSelectedStatesInFilteredThread, getTreeOrderComparatorInFilteredThread, From 2259f6099abbd641658b0ea5eae4730ba975ebfc Mon Sep 17 00:00:00 2001 From: Markus Stange Date: Sat, 20 Jan 2024 00:09:27 -0500 Subject: [PATCH 08/31] Remove now-unused stack-to-inverted-call-node mapping. --- src/profile-logic/call-node-info.js | 13 ------ src/profile-logic/profile-data.js | 40 +++---------------- .../__snapshots__/profile-view.test.js.snap | 33 --------------- src/types/profile-derived.js | 18 +-------- 4 files changed, 7 insertions(+), 97 deletions(-) diff --git a/src/profile-logic/call-node-info.js b/src/profile-logic/call-node-info.js index a10dc44fb4..97ec9eefdc 100644 --- a/src/profile-logic/call-node-info.js +++ b/src/profile-logic/call-node-info.js @@ -33,11 +33,6 @@ export class CallNodeInfoImpl implements CallNodeInfo { // The non-inverted call node table, regardless of isInverted(). _nonInvertedCallNodeTable: CallNodeTable; - // 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; - // 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(). @@ -51,12 +46,10 @@ export class CallNodeInfoImpl implements CallNodeInfo { constructor( callNodeTable: CallNodeTable, nonInvertedCallNodeTable: CallNodeTable, - stackIndexToCallNodeIndex: Int32Array, stackIndexToNonInvertedCallNodeIndex: Int32Array ) { this._callNodeTable = callNodeTable; this._nonInvertedCallNodeTable = nonInvertedCallNodeTable; - this._stackIndexToCallNodeIndex = stackIndexToCallNodeIndex; this._stackIndexToNonInvertedCallNodeIndex = stackIndexToNonInvertedCallNodeIndex; } @@ -75,10 +68,6 @@ export class CallNodeInfoImpl implements CallNodeInfo { return this._callNodeTable; } - getStackIndexToCallNodeIndex(): Int32Array { - return this._stackIndexToCallNodeIndex; - } - getNonInvertedCallNodeTable(): CallNodeTable { return this._nonInvertedCallNodeTable; } @@ -240,7 +229,6 @@ export class CallNodeInfoInvertedImpl constructor( callNodeTable: CallNodeTable, nonInvertedCallNodeTable: CallNodeTable, - stackIndexToCallNodeIndex: Int32Array, stackIndexToNonInvertedCallNodeIndex: Int32Array, suffixOrderedCallNodes: Uint32Array, suffixOrderIndexes: Uint32Array @@ -248,7 +236,6 @@ export class CallNodeInfoInvertedImpl super( callNodeTable, nonInvertedCallNodeTable, - stackIndexToCallNodeIndex, stackIndexToNonInvertedCallNodeIndex ); this._suffixOrderedCallNodes = suffixOrderedCallNodes; diff --git a/src/profile-logic/profile-data.js b/src/profile-logic/profile-data.js index d3d6fd4942..e421a71c95 100644 --- a/src/profile-logic/profile-data.js +++ b/src/profile-logic/profile-data.js @@ -122,7 +122,6 @@ export function getCallNodeInfo( return new CallNodeInfoImpl( callNodeTable, callNodeTable, - stackIndexToCallNodeIndex, stackIndexToCallNodeIndex ); } @@ -447,47 +446,19 @@ export function getInvertedCallNodeInfo( defaultCategory: IndexIntoCategoryList ): CallNodeInfoInverted { // We compute an inverted stack table, but we don't let it escape this function. - const { - invertedThread, - oldStackToNewStack: nonInvertedStackToInvertedStack, - } = _computeThreadWithInvertedStackTable(thread, defaultCategory); + const { invertedThread } = _computeThreadWithInvertedStackTable( + thread, + defaultCategory + ); // Create an inverted call node table based on the inverted stack table. - const { - callNodeTable, - stackIndexToCallNodeIndex: invertedStackIndexToCallNodeIndex, - } = computeCallNodeTable( + const { callNodeTable } = computeCallNodeTable( invertedThread.stackTable, invertedThread.frameTable, invertedThread.funcTable, defaultCategory ); - // 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]; - } - } - // TEMPORARY: Compute a suffix order for the entire non-inverted call node table. // See the CallNodeInfoInverted interface for more details about the suffix order. // By the end of this commit stack, the suffix order will be computed incrementally @@ -508,7 +479,6 @@ export function getInvertedCallNodeInfo( return new CallNodeInfoInvertedImpl( callNodeTable, nonInvertedCallNodeTable, - nonInvertedStackIndexToCallNodeIndex, stackIndexToNonInvertedCallNodeIndex, suffixOrderedCallNodes, suffixOrderIndexes diff --git a/src/test/store/__snapshots__/profile-view.test.js.snap b/src/test/store/__snapshots__/profile-view.test.js.snap index d7da837f60..ad87b1da57 100644 --- a/src/test/store/__snapshots__/profile-view.test.js.snap +++ b/src/test/store/__snapshots__/profile-view.test.js.snap @@ -2338,17 +2338,6 @@ CallNodeInfoImpl { 9, ], }, - "_stackIndexToCallNodeIndex": Int32Array [ - 0, - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 8, - ], "_stackIndexToNonInvertedCallNodeIndex": Int32Array [ 0, 1, @@ -2573,17 +2562,6 @@ CallTree { 9, ], }, - "_stackIndexToCallNodeIndex": Int32Array [ - 0, - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 8, - ], "_stackIndexToNonInvertedCallNodeIndex": Int32Array [ 0, 1, @@ -2979,17 +2957,6 @@ CallTree { 9, ], }, - "_stackIndexToCallNodeIndex": Int32Array [ - 0, - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 8, - ], "_stackIndexToNonInvertedCallNodeIndex": Int32Array [ 0, 1, diff --git a/src/types/profile-derived.js b/src/types/profile-derived.js index b34d107540..f64c1a28d6 100644 --- a/src/types/profile-derived.js +++ b/src/types/profile-derived.js @@ -322,25 +322,11 @@ export interface CallNodeInfo { // 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. + // 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. // - // 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. + // All entries are >= 0. // This always maps to the non-inverted call node table, regardless of isInverted(). getStackIndexToNonInvertedCallNodeIndex(): Int32Array; From 166342c1dfa54f4b6b5e590363952d379a7f8f7e Mon Sep 17 00:00:00 2001 From: Markus Stange Date: Sat, 20 Jan 2024 00:30:17 -0500 Subject: [PATCH 09/31] Call getNonInvertedCallNodeTable() in more stack-chart-related places. This removes a few more uses of getCallNodeTable(). --- src/components/stack-chart/Canvas.js | 2 +- src/profile-logic/stack-timing.js | 2 +- src/test/unit/profile-data.test.js | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/stack-chart/Canvas.js b/src/components/stack-chart/Canvas.js index ce865cc563..df25e28f73 100644 --- a/src/components/stack-chart/Canvas.js +++ b/src/components/stack-chart/Canvas.js @@ -262,7 +262,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/profile-logic/stack-timing.js b/src/profile-logic/stack-timing.js index fc8c728a69..c1aff9f5ce 100644 --- a/src/profile-logic/stack-timing.js +++ b/src/profile-logic/stack-timing.js @@ -66,7 +66,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/test/unit/profile-data.test.js b/src/test/unit/profile-data.test.js index fae1c78ad2..aedfb74897 100644 --- a/src/test/unit/profile-data.test.js +++ b/src/test/unit/profile-data.test.js @@ -884,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 }; } From f2a32cf1579a5d2fdf65d25ae32922f13a28ee67 Mon Sep 17 00:00:00 2001 From: Markus Stange Date: Tue, 6 Aug 2024 15:07:28 -0400 Subject: [PATCH 10/31] Compute inverted call tree timings differently. This replaces lots of uses of getCallNodeTable() with uses of getNonInvertedCallNodeTable(). It also replaces lots of uses of getStackIndexToCallNodeIndex() with uses of getStackIndexToNonInvertedCallNodeIndex(). We now compute the call tree timings quite differently for inverted mode compared to non-inverted mode. There's one part of the work that's shared: The getCallNodeLeafAndSummary computes the self time for each non-inverted node, and the result is used for both the inverted and the non-inverted call tree timings. The CallTreeTimings Flow type is turned into an enum, with a different type for CallTreeTimingsNonInverted and for CallTreeTimingsInverted. A new implementation for the CallTreeInternal interface is added. --- src/components/flame-graph/Canvas.js | 4 +- src/components/flame-graph/FlameGraph.js | 12 +- src/profile-logic/call-node-info.js | 37 ++ src/profile-logic/call-tree.js | 409 +++++++++++++++--- src/profile-logic/flame-graph.js | 4 +- src/selectors/per-thread/stack-sample.js | 58 ++- src/test/fixtures/utils.js | 4 +- .../__snapshots__/profile-view.test.js.snap | 2 +- src/test/store/profile-view.test.js | 9 +- src/test/unit/profile-tree.test.js | 31 +- src/types/profile-derived.js | 6 + 11 files changed, 468 insertions(+), 108 deletions(-) diff --git a/src/components/flame-graph/Canvas.js b/src/components/flame-graph/Canvas.js index f6cc1e23c7..0f9e2a9038 100644 --- a/src/components/flame-graph/Canvas.js +++ b/src/components/flame-graph/Canvas.js @@ -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..8c2934b4e6 100644 --- a/src/components/flame-graph/FlameGraph.js +++ b/src/components/flame-graph/FlameGraph.js @@ -344,6 +344,16 @@ 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 && tracedTiming.type === 'NON_INVERTED' + ? tracedTiming.timings + : null; + const maxViewportHeight = maxStackDepthPlusOne * STACK_FRAME_HEIGHT; return ( @@ -394,7 +404,7 @@ class FlameGraphImpl extends React.PureComponent { isInverted, ctssSamples, unfilteredCtssSamples, - tracedTiming, + tracedTiming: tracedTimingNonInverted, displayImplementation, displayStackType, }} diff --git a/src/profile-logic/call-node-info.js b/src/profile-logic/call-node-info.js index 97ec9eefdc..b50fa5ce9b 100644 --- a/src/profile-logic/call-node-info.js +++ b/src/profile-logic/call-node-info.js @@ -200,9 +200,46 @@ export class CallNodeInfoImpl implements CallNodeInfo { return null; } + getRoots(): IndexIntoCallNodeTable[] { + const roots = []; + if (this._callNodeTable.length !== 0) { + // The call node with index 0 is guaruanteed to be a root, by construction + // of the call node table. + // Start with node 0 and add its siblings. + for ( + let root = 0; + root !== -1; + root = this._callNodeTable.nextSibling[root] + ) { + roots.push(root); + } + } + return roots; + } + isRoot(callNodeIndex: IndexIntoCallNodeTable): boolean { return this._callNodeTable.prefix[callNodeIndex] === -1; } + + getChildren(callNodeIndex: IndexIntoCallNodeTable): IndexIntoCallNodeTable[] { + if ( + this._callNodeTable.subtreeRangeEnd[callNodeIndex] === + callNodeIndex + 1 + ) { + return []; + } + + const children = []; + const firstChild = callNodeIndex + 1; + for ( + let childCallNodeIndex = firstChild; + childCallNodeIndex !== -1; + childCallNodeIndex = this._callNodeTable.nextSibling[childCallNodeIndex] + ) { + children.push(childCallNodeIndex); + } + return children; + } } /** diff --git a/src/profile-logic/call-tree.js b/src/profile-logic/call-tree.js index f4e10833eb..5d1366f336 100644 --- a/src/profile-logic/call-tree.js +++ b/src/profile-logic/call-tree.js @@ -22,6 +22,7 @@ import type { CallNodePath, IndexIntoCallNodeTable, CallNodeInfo, + CallNodeInfoInverted, CallNodeData, CallNodeDisplayData, Milliseconds, @@ -39,13 +40,32 @@ import type { CallTreeSummaryStrategy } from '../types/actions'; 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 = {| + callNodeLeaf: Float32Array, rootTotalSummary: number, -}; + sortedRoots: IndexIntoFuncTable[], + totalPerRootNode: Map, + rootNodesWithChildren: Set, +|}; + +export type CallTreeTimings = + | {| type: 'NON_INVERTED', timings: CallTreeTimingsNonInverted |} + | {| type: 'INVERTED', timings: CallTreeTimingsInverted |}; function extractFaviconFromLibname(libname: string): string | null { try { @@ -74,15 +94,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; } @@ -157,6 +180,127 @@ export class CallTreeInternalImpl implements CallTreeInternal { } } +class CallTreeInternalInverted implements CallTreeInternal { + _callNodeInfo: CallNodeInfoInverted; + _nonInvertedCallNodeTable: CallNodeTable; + _callNodeLeaf: Float32Array; + _rootNodes: IndexIntoCallNodeTable[]; + _funcCount: number; + _totalPerRootNode: Map; + _rootNodesWithChildren: Set; + _totalAndHasChildrenPerNonRootNode: Map< + IndexIntoCallNodeTable, + TotalAndHasChildren, + > = new Map(); + + constructor( + callNodeInfo: CallNodeInfoInverted, + callTreeTimingsInverted: CallTreeTimingsInverted + ) { + this._callNodeInfo = callNodeInfo; + this._nonInvertedCallNodeTable = callNodeInfo.getNonInvertedCallNodeTable(); + this._callNodeLeaf = callTreeTimingsInverted.callNodeLeaf; + const { sortedRoots, totalPerRootNode, rootNodesWithChildren } = + callTreeTimingsInverted; + this._totalPerRootNode = totalPerRootNode; + this._rootNodesWithChildren = rootNodesWithChildren; + this._rootNodes = sortedRoots; + } + + createRoots(): IndexIntoCallNodeTable[] { + return this._rootNodes; + } + + hasChildren(callNodeIndex: IndexIntoCallNodeTable): boolean { + if (this._callNodeInfo.isRoot(callNodeIndex)) { + return this._rootNodesWithChildren.has(callNodeIndex); + } + 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 = ensureExists(this._totalPerRootNode.get(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._callNodeLeaf + ); + 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._callNodeLeaf[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; @@ -424,7 +568,7 @@ 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 @@ -453,43 +597,6 @@ export class CallTree { } } -// 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 * values. @@ -523,22 +630,175 @@ export function computeCallNodeLeafAndSummary( return { callNodeLeaf, 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 { callNodeLeaf, totalPerRootNode } = timings; + if (callNodeInfoInverted.isRoot(callNodeIndex)) { + const total = totalPerRootNode.get(callNodeIndex) ?? 0; + return { self: total, total }; + } + const { total } = _getInvertedTreeNodeTotalAndHasChildren( + callNodeIndex, + callNodeInfoInverted, + callNodeLeaf + ); + return { self: 0, total }; + } + default: + throw assertExhaustiveCheck(callTreeTimings.type); + } +} + +function _getInvertedTreeNodeTotalAndHasChildren( + callNodeIndex: IndexIntoCallNodeTable, + callNodeInfo: CallNodeInfoInverted, + callNodeLeaf: Float32Array +): TotalAndHasChildren { + const nodeDepth = callNodeInfo.getCallNodeTable().depth[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 leafNode = suffixOrderedCallNodes[i]; + const leaf = callNodeLeaf[leafNode]; + total += leaf; + + // 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 `leafNode` 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 || + (leaf !== 0 && callNodeTableDepthCol[leafNode] > nodeDepth); + } + return { total, hasChildren }; +} + +export function computeCallTreeTimingsInverted( + callNodeInfo: CallNodeInfoInverted, + { callNodeLeaf, rootTotalSummary }: CallNodeLeafAndSummary +): CallTreeTimingsInverted { + const roots = callNodeInfo.getRoots(); + const invertedCallNodeTable = callNodeInfo.getCallNodeTable(); + const callNodeTable = callNodeInfo.getNonInvertedCallNodeTable(); + const callNodeTableFuncCol = callNodeTable.func; + const callNodeTableDepthCol = callNodeTable.depth; + const totalPerRootNode = new Map(); + const rootNodesWithChildren = new Set(); + const seenRoots = new Set(); + for (let i = 0; i < callNodeLeaf.length; i++) { + const leaf = callNodeLeaf[i]; + if (leaf === 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 leaf function of the non-inverted call node. + const func = callNodeTableFuncCol[i]; + const rootNode = roots.find( + (invertedCallNode) => + invertedCallNodeTable.func[invertedCallNode] === func + ); + if (rootNode === undefined) { + throw new Error( + "Couldn't find the inverted root for a function with non-zero self time." + ); + } + + totalPerRootNode.set( + rootNode, + (totalPerRootNode.get(rootNode) ?? 0) + leaf + ); + seenRoots.add(rootNode); + if (callNodeTableDepthCol[i] !== 0) { + rootNodesWithChildren.add(rootNode); + } + } + const sortedRoots = [...seenRoots]; + sortedRoots.sort( + (a, b) => + Math.abs(totalPerRootNode.get(b) ?? 0) - + Math.abs(totalPerRootNode.get(a) ?? 0) + ); + return { + callNodeLeaf, + rootTotalSummary, + sortedRoots, + totalPerRootNode, + rootNodesWithChildren, + }; +} + +export function computeCallTreeTimings( + callNodeInfo: CallNodeInfo, + callNodeLeafAndSummary: CallNodeLeafAndSummary +): CallTreeTimings { + const callNodeInfoInverted = callNodeInfo.asInverted(); + if (callNodeInfoInverted !== null) { + return { + type: 'INVERTED', + timings: computeCallTreeTimingsInverted( + callNodeInfoInverted, + callNodeLeafAndSummary + ), + }; + } + return { + type: 'NON_INVERTED', + timings: computeCallTreeTimingsNonInverted( + callNodeInfo, + callNodeLeafAndSummary + ), + }; +} + /** * 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(); +): CallTreeTimingsNonInverted { + const callNodeTable = callNodeInfo.getNonInvertedCallNodeTable(); 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; + const callNodeSelf = callNodeLeaf; // Compute the following variables: const callNodeTotalSummary = new Float32Array(callNodeTable.length); @@ -588,15 +848,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.' + ); + } }); } 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/selectors/per-thread/stack-sample.js b/src/selectors/per-thread/stack-sample.js index 2b90dc89a3..3eef7706b6 100644 --- a/src/selectors/per-thread/stack-sample.js +++ b/src/selectors/per-thread/stack-sample.js @@ -44,6 +44,7 @@ import type { $ReturnType, ThreadsKey, SelfAndTotal, + CallNodeLeafAndSummary, } from 'firefox-profiler/types'; import type { ThreadSelectorsPerThread } from './thread'; @@ -230,11 +231,11 @@ export function getStackAndSampleSelectorsPerThread( ) ); - const _getPreviewFilteredCtssSampleIndexToCallNodeIndex: Selector< + const _getPreviewFilteredCtssSampleIndexToNonInvertedCallNodeIndex: Selector< Array, > = createSelector( (state) => threadSelectors.getPreviewFilteredCtssSamples(state).stack, - (state) => getCallNodeInfo(state).getStackIndexToCallNodeIndex(), + (state) => getCallNodeInfo(state).getStackIndexToNonInvertedCallNodeIndex(), ProfileData.getSampleIndexToCallNodeIndex ); @@ -310,23 +311,33 @@ export function getStackAndSampleSelectorsPerThread( (samples) => samples.weightType || 'samples' ); + const getCallNodeLeafAndSummary: Selector = + createSelector( + threadSelectors.getPreviewFilteredCtssSamples, + _getPreviewFilteredCtssSampleIndexToNonInvertedCallNodeIndex, + getCallNodeInfo, + (samples, sampleIndexToCallNodeIndex, callNodeInfo) => { + return CallTree.computeCallNodeLeafAndSummary( + 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 - ); - } + getCallNodeLeafAndSummary, + CallTree.computeCallTreeTimings ); + const getCallTreeTimingsNonInverted: Selector = + createSelector( + getCallNodeInfo, + getCallNodeLeafAndSummary, + CallTree.computeCallTreeTimingsNonInverted + ); + const getCallTree: Selector = createSelector( threadSelectors.getFilteredThread, getCallNodeInfo, @@ -352,7 +363,7 @@ export function getStackAndSampleSelectorsPerThread( const getTracedTiming: Selector = createSelector( threadSelectors.getPreviewFilteredCtssSamples, - _getPreviewFilteredCtssSampleIndexToCallNodeIndex, + _getPreviewFilteredCtssSampleIndexToNonInvertedCallNodeIndex, getCallNodeInfo, ProfileSelectors.getProfileInterval, (samples, sampleIndexToCallNodeIndex, callNodeInfo, interval) => { @@ -360,7 +371,7 @@ export function getStackAndSampleSelectorsPerThread( CallTree.computeCallNodeTracedLeafAndSummary( samples, sampleIndexToCallNodeIndex, - callNodeInfo.getCallNodeTable().length, + callNodeInfo.getNonInvertedCallNodeTable().length, interval ); if (callNodeLeafAndSummary === null) { @@ -376,14 +387,17 @@ 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 + ); } ); @@ -408,7 +422,7 @@ export function getStackAndSampleSelectorsPerThread( createSelector( getFlameGraphRows, (state) => getCallNodeInfo(state).getNonInvertedCallNodeTable(), - getCallTreeTimings, + getCallTreeTimingsNonInverted, FlameGraph.getFlameGraphTiming ); diff --git a/src/test/fixtures/utils.js b/src/test/fixtures/utils.js index 7dbe88843f..82837cdc88 100644 --- a/src/test/fixtures/utils.js +++ b/src/test/fixtures/utils.js @@ -169,9 +169,9 @@ export function callTreeFromProfile( 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 ad87b1da57..cd1b68bea0 100644 --- a/src/test/store/__snapshots__/profile-view.test.js.snap +++ b/src/test/store/__snapshots__/profile-view.test.js.snap @@ -2737,7 +2737,7 @@ CallTree { ], "_children": Array [], "_displayDataByIndex": Map {}, - "_internal": CallTreeInternalImpl { + "_internal": CallTreeInternalNonInverted { "_callNodeHasChildren": Uint8Array [ 1, 1, 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/unit/profile-tree.test.js b/src/test/unit/profile-tree.test.js index bf16e39c28..2b5c4f1288 100644 --- a/src/test/unit/profile-tree.test.js +++ b/src/test/unit/profile-tree.test.js @@ -73,17 +73,20 @@ describe('unfiltered call tree', function () { 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]), + leaf: new Float32Array([0, 0, 0, 0, 1, 0, 1, 0, 1]), + total: new Float32Array([3, 3, 2, 1, 1, 1, 1, 1, 1]), + }, }); }); }); @@ -424,9 +427,9 @@ describe('inverted call tree', function () { thread.samples, getSampleIndexToCallNodeIndex( thread.samples.stack, - callNodeInfo.getStackIndexToCallNodeIndex() + callNodeInfo.getStackIndexToNonInvertedCallNodeIndex() ), - callNodeInfo.getCallNodeTable().length + callNodeInfo.getNonInvertedCallNodeTable().length ) ); const callTree = getCallTree( @@ -466,9 +469,9 @@ describe('inverted call tree', function () { thread.samples, getSampleIndexToCallNodeIndex( thread.samples.stack, - invertedCallNodeInfo.getStackIndexToCallNodeIndex() + invertedCallNodeInfo.getStackIndexToNonInvertedCallNodeIndex() ), - invertedCallNodeInfo.getCallNodeTable().length + invertedCallNodeInfo.getNonInvertedCallNodeTable().length ) ); const invertedCallTree = getCallTree( @@ -616,12 +619,12 @@ describe('diffing trees', function () { 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/profile-derived.js b/src/types/profile-derived.js index f64c1a28d6..efdad7512d 100644 --- a/src/types/profile-derived.js +++ b/src/types/profile-derived.js @@ -349,8 +349,14 @@ export interface CallNodeInfo { func: IndexIntoFuncTable ): IndexIntoCallNodeTable | null; + // Returns the list of root nodes. + getRoots(): IndexIntoCallNodeTable[]; + // Returns whether the given node is a root node. isRoot(callNodeIndex: IndexIntoCallNodeTable): boolean; + + // Returns the list of children of a node. + getChildren(callNodeIndex: IndexIntoCallNodeTable): IndexIntoCallNodeTable[]; } // An index into SuffixOrderedCallNodes. From 724ed13cafe1567d256a1812884a2ab8e8548f26 Mon Sep 17 00:00:00 2001 From: Markus Stange Date: Fri, 19 Jan 2024 15:21:06 -0500 Subject: [PATCH 11/31] Rename leaf to self in many places. All these places now deal with non-inverted call nodes, and for those, what we meant by "leaf" and by "self" was always the same thing. And I prefer the word "self" because "leaf" usually means "has no children" and that's not the case here. We still use the word "leaf" in many parts of the documentation. --- src/profile-logic/call-tree.js | 95 +++++++++---------- src/selectors/per-thread/stack-sample.js | 18 ++-- src/test/fixtures/utils.js | 4 +- .../__snapshots__/profile-view.test.js.snap | 11 --- src/test/unit/profile-tree.test.js | 11 +-- src/types/profile-derived.js | 10 +- 6 files changed, 67 insertions(+), 82 deletions(-) diff --git a/src/profile-logic/call-tree.js b/src/profile-logic/call-tree.js index 5d1366f336..7f7706b0ad 100644 --- a/src/profile-logic/call-tree.js +++ b/src/profile-logic/call-tree.js @@ -28,7 +28,7 @@ import type { Milliseconds, ExtraBadgeInfo, BottomBoxInfo, - CallNodeLeafAndSummary, + CallNodeSelfAndSummary, SelfAndTotal, } from 'firefox-profiler/types'; @@ -43,7 +43,6 @@ type CallNodeChildren = IndexIntoCallNodeTable[]; export type CallTreeTimingsNonInverted = {| callNodeHasChildren: Uint8Array, self: Float32Array, - leaf: Float32Array, total: Float32Array, rootTotalSummary: number, // sum of absolute values, this is used for computing percentages |}; @@ -56,7 +55,7 @@ export type InvertedCallTreeRoot = {| |}; export type CallTreeTimingsInverted = {| - callNodeLeaf: Float32Array, + callNodeSelf: Float32Array, rootTotalSummary: number, sortedRoots: IndexIntoFuncTable[], totalPerRootNode: Map, @@ -165,14 +164,14 @@ export class CallTreeInternalNonInverted 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; } } @@ -183,7 +182,7 @@ export class CallTreeInternalNonInverted implements CallTreeInternal { class CallTreeInternalInverted implements CallTreeInternal { _callNodeInfo: CallNodeInfoInverted; _nonInvertedCallNodeTable: CallNodeTable; - _callNodeLeaf: Float32Array; + _callNodeSelf: Float32Array; _rootNodes: IndexIntoCallNodeTable[]; _funcCount: number; _totalPerRootNode: Map; @@ -199,7 +198,7 @@ class CallTreeInternalInverted implements CallTreeInternal { ) { this._callNodeInfo = callNodeInfo; this._nonInvertedCallNodeTable = callNodeInfo.getNonInvertedCallNodeTable(); - this._callNodeLeaf = callTreeTimingsInverted.callNodeLeaf; + this._callNodeSelf = callTreeTimingsInverted.callNodeSelf; const { sortedRoots, totalPerRootNode, rootNodesWithChildren } = callTreeTimingsInverted; this._totalPerRootNode = totalPerRootNode; @@ -261,7 +260,7 @@ class CallTreeInternalInverted implements CallTreeInternal { const totalAndHasChildren = _getInvertedTreeNodeTotalAndHasChildren( callNodeIndex, this._callNodeInfo, - this._callNodeLeaf + this._callNodeSelf ); this._totalAndHasChildrenPerNonRootNode.set( callNodeIndex, @@ -282,7 +281,7 @@ class CallTreeInternalInverted implements CallTreeInternal { let maxAbs = 0; for (let i = rangeStart; i < rangeEnd; i++) { const nodeIndex = orderedCallNodes[i]; - const nodeSelf = Math.abs(this._callNodeLeaf[nodeIndex]); + const nodeSelf = Math.abs(this._callNodeSelf[nodeIndex]); if (maxNode === -1 || nodeSelf > maxAbs) { maxNode = nodeIndex; maxAbs = nodeSelf; @@ -575,7 +574,7 @@ export class CallTree { * (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 @@ -598,15 +597,15 @@ export class CallTree { } /** - * 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; @@ -615,7 +614,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; } } @@ -624,10 +623,10 @@ 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( @@ -645,7 +644,7 @@ export function getSelfAndTotalForCallNode( case 'INVERTED': { const callNodeInfoInverted = ensureExists(callNodeInfo.asInverted()); const { timings } = callTreeTimings; - const { callNodeLeaf, totalPerRootNode } = timings; + const { callNodeSelf, totalPerRootNode } = timings; if (callNodeInfoInverted.isRoot(callNodeIndex)) { const total = totalPerRootNode.get(callNodeIndex) ?? 0; return { self: total, total }; @@ -653,7 +652,7 @@ export function getSelfAndTotalForCallNode( const { total } = _getInvertedTreeNodeTotalAndHasChildren( callNodeIndex, callNodeInfoInverted, - callNodeLeaf + callNodeSelf ); return { self: 0, total }; } @@ -665,7 +664,7 @@ export function getSelfAndTotalForCallNode( function _getInvertedTreeNodeTotalAndHasChildren( callNodeIndex: IndexIntoCallNodeTable, callNodeInfo: CallNodeInfoInverted, - callNodeLeaf: Float32Array + callNodeSelf: Float32Array ): TotalAndHasChildren { const nodeDepth = callNodeInfo.getCallNodeTable().depth[callNodeIndex]; const [rangeStart, rangeEnd] = @@ -685,14 +684,14 @@ function _getInvertedTreeNodeTotalAndHasChildren( let total = 0; let hasChildren = false; for (let i = rangeStart; i < rangeEnd; i++) { - const leafNode = suffixOrderedCallNodes[i]; - const leaf = callNodeLeaf[leafNode]; - total += leaf; + 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 `leafNode` contribute to one of our children? + // 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. @@ -704,14 +703,14 @@ function _getInvertedTreeNodeTotalAndHasChildren( // self time contribute to it. hasChildren = hasChildren || - (leaf !== 0 && callNodeTableDepthCol[leafNode] > nodeDepth); + (self !== 0 && callNodeTableDepthCol[selfNode] > nodeDepth); } return { total, hasChildren }; } export function computeCallTreeTimingsInverted( callNodeInfo: CallNodeInfoInverted, - { callNodeLeaf, rootTotalSummary }: CallNodeLeafAndSummary + { callNodeSelf, rootTotalSummary }: CallNodeSelfAndSummary ): CallTreeTimingsInverted { const roots = callNodeInfo.getRoots(); const invertedCallNodeTable = callNodeInfo.getCallNodeTable(); @@ -721,15 +720,15 @@ export function computeCallTreeTimingsInverted( const totalPerRootNode = new Map(); const rootNodesWithChildren = new Set(); const seenRoots = new Set(); - for (let i = 0; i < callNodeLeaf.length; i++) { - const leaf = callNodeLeaf[i]; - if (leaf === 0) { + 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 leaf function of the non-inverted call node. + // the self function of the non-inverted call node. const func = callNodeTableFuncCol[i]; const rootNode = roots.find( (invertedCallNode) => @@ -743,7 +742,7 @@ export function computeCallTreeTimingsInverted( totalPerRootNode.set( rootNode, - (totalPerRootNode.get(rootNode) ?? 0) + leaf + (totalPerRootNode.get(rootNode) ?? 0) + self ); seenRoots.add(rootNode); if (callNodeTableDepthCol[i] !== 0) { @@ -757,7 +756,7 @@ export function computeCallTreeTimingsInverted( Math.abs(totalPerRootNode.get(a) ?? 0) ); return { - callNodeLeaf, + callNodeSelf, rootTotalSummary, sortedRoots, totalPerRootNode, @@ -767,7 +766,7 @@ export function computeCallTreeTimingsInverted( export function computeCallTreeTimings( callNodeInfo: CallNodeInfo, - callNodeLeafAndSummary: CallNodeLeafAndSummary + CallNodeSelfAndSummary: CallNodeSelfAndSummary ): CallTreeTimings { const callNodeInfoInverted = callNodeInfo.asInverted(); if (callNodeInfoInverted !== null) { @@ -775,7 +774,7 @@ export function computeCallTreeTimings( type: 'INVERTED', timings: computeCallTreeTimingsInverted( callNodeInfoInverted, - callNodeLeafAndSummary + CallNodeSelfAndSummary ), }; } @@ -783,7 +782,7 @@ export function computeCallTreeTimings( type: 'NON_INVERTED', timings: computeCallTreeTimingsNonInverted( callNodeInfo, - callNodeLeafAndSummary + CallNodeSelfAndSummary ), }; } @@ -794,11 +793,10 @@ export function computeCallTreeTimings( */ export function computeCallTreeTimingsNonInverted( callNodeInfo: CallNodeInfo, - callNodeLeafAndSummary: CallNodeLeafAndSummary + CallNodeSelfAndSummary: CallNodeSelfAndSummary ): CallTreeTimingsNonInverted { const callNodeTable = callNodeInfo.getNonInvertedCallNodeTable(); - const { callNodeLeaf, rootTotalSummary } = callNodeLeafAndSummary; - const callNodeSelf = callNodeLeaf; + const { callNodeSelf, rootTotalSummary } = CallNodeSelfAndSummary; // Compute the following variables: const callNodeTotalSummary = new Float32Array(callNodeTable.length); @@ -812,7 +810,7 @@ export function computeCallTreeTimingsNonInverted( callNodeIndex >= 0; callNodeIndex-- ) { - callNodeTotalSummary[callNodeIndex] += callNodeLeaf[callNodeIndex]; + callNodeTotalSummary[callNodeIndex] += callNodeSelf[callNodeIndex]; const hasChildren = callNodeHasChildren[callNodeIndex] !== 0; const hasTotalValue = callNodeTotalSummary[callNodeIndex] !== 0; @@ -830,7 +828,6 @@ export function computeCallTreeTimingsNonInverted( return { self: callNodeSelf, - leaf: callNodeLeaf, total: callNodeTotalSummary, callNodeHasChildren, rootTotalSummary, @@ -998,7 +995,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 @@ -1008,12 +1005,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 @@ -1025,7 +1022,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++) { @@ -1033,7 +1030,7 @@ export function computeCallNodeTracedLeafAndSummary( if (callNodeIndex !== null) { const sampleTracedTime = samples.time[sampleIndex + 1] - samples.time[sampleIndex]; - callNodeLeaf[callNodeIndex] += sampleTracedTime; + callNodeSelf[callNodeIndex] += sampleTracedTime; rootTotalSummary += sampleTracedTime; } } @@ -1043,10 +1040,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/selectors/per-thread/stack-sample.js b/src/selectors/per-thread/stack-sample.js index 3eef7706b6..b2ac8a0d3d 100644 --- a/src/selectors/per-thread/stack-sample.js +++ b/src/selectors/per-thread/stack-sample.js @@ -44,7 +44,7 @@ import type { $ReturnType, ThreadsKey, SelfAndTotal, - CallNodeLeafAndSummary, + CallNodeSelfAndSummary, } from 'firefox-profiler/types'; import type { ThreadSelectorsPerThread } from './thread'; @@ -311,13 +311,13 @@ export function getStackAndSampleSelectorsPerThread( (samples) => samples.weightType || 'samples' ); - const getCallNodeLeafAndSummary: Selector = + const getCallNodeSelfAndSummary: Selector = createSelector( threadSelectors.getPreviewFilteredCtssSamples, _getPreviewFilteredCtssSampleIndexToNonInvertedCallNodeIndex, getCallNodeInfo, (samples, sampleIndexToCallNodeIndex, callNodeInfo) => { - return CallTree.computeCallNodeLeafAndSummary( + return CallTree.computeCallNodeSelfAndSummary( samples, sampleIndexToCallNodeIndex, callNodeInfo.getNonInvertedCallNodeTable().length @@ -327,14 +327,14 @@ export function getStackAndSampleSelectorsPerThread( const getCallTreeTimings: Selector = createSelector( getCallNodeInfo, - getCallNodeLeafAndSummary, + getCallNodeSelfAndSummary, CallTree.computeCallTreeTimings ); const getCallTreeTimingsNonInverted: Selector = createSelector( getCallNodeInfo, - getCallNodeLeafAndSummary, + getCallNodeSelfAndSummary, CallTree.computeCallTreeTimingsNonInverted ); @@ -367,19 +367,19 @@ export function getStackAndSampleSelectorsPerThread( getCallNodeInfo, ProfileSelectors.getProfileInterval, (samples, sampleIndexToCallNodeIndex, callNodeInfo, interval) => { - const callNodeLeafAndSummary = - CallTree.computeCallNodeTracedLeafAndSummary( + const CallNodeSelfAndSummary = + CallTree.computeCallNodeTracedSelfAndSummary( samples, sampleIndexToCallNodeIndex, callNodeInfo.getNonInvertedCallNodeTable().length, interval ); - if (callNodeLeafAndSummary === null) { + if (CallNodeSelfAndSummary === null) { return null; } return CallTree.computeCallTreeTimings( callNodeInfo, - callNodeLeafAndSummary + CallNodeSelfAndSummary ); } ); diff --git a/src/test/fixtures/utils.js b/src/test/fixtures/utils.js index 82837cdc88..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,7 +165,7 @@ export function callTreeFromProfile( ); const callTreeTimings = computeCallTreeTimings( callNodeInfo, - computeCallNodeLeafAndSummary( + computeCallNodeSelfAndSummary( thread.samples, getSampleIndexToCallNodeIndex( thread.samples.stack, diff --git a/src/test/store/__snapshots__/profile-view.test.js.snap b/src/test/store/__snapshots__/profile-view.test.js.snap index cd1b68bea0..fcd323f4c2 100644 --- a/src/test/store/__snapshots__/profile-view.test.js.snap +++ b/src/test/store/__snapshots__/profile-view.test.js.snap @@ -3084,17 +3084,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/unit/profile-tree.test.js b/src/test/unit/profile-tree.test.js index 2b5c4f1288..7a2b6fb9f3 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,7 +69,7 @@ describe('unfiltered call tree', function () { it('yields expected results', function () { const callTreeTimings = computeCallTreeTimings( callNodeInfo, - computeCallNodeLeafAndSummary( + computeCallNodeSelfAndSummary( thread.samples, getSampleIndexToCallNodeIndex( thread.samples.stack, @@ -84,7 +84,6 @@ describe('unfiltered call tree', function () { 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]), }, }); @@ -423,7 +422,7 @@ describe('inverted call tree', function () { ); const callTreeTimings = computeCallTreeTimings( callNodeInfo, - computeCallNodeLeafAndSummary( + computeCallNodeSelfAndSummary( thread.samples, getSampleIndexToCallNodeIndex( thread.samples.stack, @@ -465,7 +464,7 @@ describe('inverted call tree', function () { ); const invertedCallTreeTimings = computeCallTreeTimings( invertedCallNodeInfo, - computeCallNodeLeafAndSummary( + computeCallNodeSelfAndSummary( thread.samples, getSampleIndexToCallNodeIndex( thread.samples.stack, @@ -615,7 +614,7 @@ describe('diffing trees', function () { ); const callTreeTimings = computeCallTreeTimings( callNodeInfo, - computeCallNodeLeafAndSummary( + computeCallNodeSelfAndSummary( thread.samples, getSampleIndexToCallNodeIndex( thread.samples.stack, diff --git a/src/types/profile-derived.js b/src/types/profile-derived.js index efdad7512d..66e9c03183 100644 --- a/src/types/profile-derived.js +++ b/src/types/profile-derived.js @@ -1003,11 +1003,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, |}; From 2da3e88a81458da9d6f96024117809700f5bdf7d Mon Sep 17 00:00:00 2001 From: Markus Stange Date: Wed, 7 Aug 2024 18:14:12 -0400 Subject: [PATCH 12/31] Use the non-inverted call node table to check for recursion. Whether a function recurses (directly or indirectly) is the same in the inverted call node table and in the non-inverted call node table. --- src/actions/profile-view.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/actions/profile-view.js b/src/actions/profile-view.js index 6fe354ece7..3c1eb3268c 100644 --- a/src/actions/profile-view.js +++ b/src/actions/profile-view.js @@ -2041,6 +2041,7 @@ export function handleCallNodeTransformShortcut( const callNodePath = callNodeInfo.getCallNodePathFromIndex(callNodeIndex); const funcIndex = callNodeTable.func[callNodeIndex]; const category = callNodeTable.category[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', From 3fc5886b2f86ef9fc77a65d51f6e122f6d9d3b66 Mon Sep 17 00:00:00 2001 From: Markus Stange Date: Mon, 13 Nov 2023 14:20:47 -0500 Subject: [PATCH 13/31] Replace all remaining callers of getCallNodeTable() with xyzForNode() calls. --- src/actions/profile-view.js | 6 +- src/components/calltree/CallTree.js | 6 +- src/components/shared/CallNodeContextMenu.js | 17 ++- src/components/stack-chart/Canvas.js | 3 +- src/components/stack-chart/index.js | 3 +- src/components/tooltip/CallNode.js | 9 +- src/profile-logic/address-timings.js | 2 +- src/profile-logic/call-node-info.js | 40 +++++++ src/profile-logic/call-tree.js | 25 ++--- src/profile-logic/line-timings.js | 2 +- src/profile-logic/profile-data.js | 5 +- src/profile-logic/transforms.js | 4 +- src/selectors/per-thread/index.js | 3 +- .../__snapshots__/profile-view.test.js.snap | 103 ------------------ src/types/profile-derived.js | 23 ++++ 15 files changed, 100 insertions(+), 151 deletions(-) diff --git a/src/actions/profile-view.js b/src/actions/profile-view.js index 3c1eb3268c..223cadbd71 100644 --- a/src/actions/profile-view.js +++ b/src/actions/profile-view.js @@ -2035,12 +2035,12 @@ 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) { diff --git a/src/components/calltree/CallTree.js b/src/components/calltree/CallTree.js index 4ad5c7110d..206f2fe70d 100644 --- a/src/components/calltree/CallTree.js +++ b/src/components/calltree/CallTree.js @@ -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/shared/CallNodeContextMenu.js b/src/components/shared/CallNodeContextMenu.js index 2a9dbbc1bb..0f9a662c40 100644 --- a/src/components/shared/CallNodeContextMenu.js +++ b/src/components/shared/CallNodeContextMenu.js @@ -138,8 +138,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); @@ -176,8 +175,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; @@ -303,8 +301,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, { @@ -488,9 +485,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. @@ -504,6 +500,9 @@ class CallNodeContextMenuImpl extends React.PureComponent { const fileName = filePath && parseFileNameFromSymbolication(filePath).path.match(/[^\\/]+$/)?.[0]; + + const callNodeTable = callNodeInfo.getNonInvertedCallNodeTable(); + return ( <> {fileName ? ( diff --git a/src/components/stack-chart/Canvas.js b/src/components/stack-chart/Canvas.js index df25e28f73..90ab920f3f 100644 --- a/src/components/stack-chart/Canvas.js +++ b/src/components/stack-chart/Canvas.js @@ -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) { diff --git a/src/components/stack-chart/index.js b/src/components/stack-chart/index.js index 9b9b65cf11..94297e39e5 100644 --- a/src/components/stack-chart/index.js +++ b/src/components/stack-chart/index.js @@ -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/tooltip/CallNode.js b/src/components/tooltip/CallNode.js index 9bc4568b68..6eed705c81 100644 --- a/src/components/tooltip/CallNode.js +++ b/src/components/tooltip/CallNode.js @@ -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 2e94663a2a..e026ef5775 100644 --- a/src/profile-logic/address-timings.js +++ b/src/profile-logic/address-timings.js @@ -431,7 +431,7 @@ export function getStackAddressInfoForCallNodeInverted( callNodeInfo: CallNodeInfoInverted, nativeSymbol: IndexIntoNativeSymbolTable ): StackAddressInfo { - const depth = callNodeInfo.getCallNodeTable().depth[callNodeIndex]; + const depth = callNodeInfo.depthForNode(callNodeIndex); const [rangeStart, rangeEnd] = callNodeInfo.getSuffixOrderIndexRangeForCallNode(callNodeIndex); const callNodeIsRootOfInvertedTree = callNodeInfo.isRoot(callNodeIndex); diff --git a/src/profile-logic/call-node-info.js b/src/profile-logic/call-node-info.js index b50fa5ce9b..96945f9108 100644 --- a/src/profile-logic/call-node-info.js +++ b/src/profile-logic/call-node-info.js @@ -16,6 +16,8 @@ import type { CallNodePath, IndexIntoCallNodeTable, SuffixOrderIndex, + IndexIntoCategoryList, + IndexIntoNativeSymbolTable, } from 'firefox-profiler/types'; /** @@ -240,6 +242,44 @@ export class CallNodeInfoImpl implements CallNodeInfo { } return children; } + + 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 | null { + return this._callNodeTable.sourceFramesInlinedIntoSymbol[callNodeIndex]; + } } /** diff --git a/src/profile-logic/call-tree.js b/src/profile-logic/call-tree.js index 7f7706b0ad..cc9ae6cbd1 100644 --- a/src/profile-logic/call-tree.js +++ b/src/profile-logic/call-tree.js @@ -304,7 +304,6 @@ export class CallTree { _categories: CategoryList; _internal: CallTreeInternal; _callNodeInfo: CallNodeInfo; - _callNodeTable: CallNodeTable; _thread: Thread; _rootTotalSummary: number; _displayDataByIndex: Map; @@ -327,7 +326,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(); @@ -375,15 +373,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] ); @@ -407,7 +405,7 @@ export class CallTree { ): ExtraBadgeInfo | void { const calledFunction = getFunctionName(funcName); const inlinedIntoNativeSymbol = - this._callNodeTable.sourceFramesInlinedIntoSymbol[callNodeIndex]; + this._callNodeInfo.sourceFramesInlinedIntoSymbolForNode(callNodeIndex); if (inlinedIntoNativeSymbol === null) { return undefined; } @@ -442,9 +440,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]; @@ -590,7 +589,7 @@ 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(); } @@ -666,7 +665,7 @@ function _getInvertedTreeNodeTotalAndHasChildren( callNodeInfo: CallNodeInfoInverted, callNodeSelf: Float32Array ): TotalAndHasChildren { - const nodeDepth = callNodeInfo.getCallNodeTable().depth[callNodeIndex]; + const nodeDepth = callNodeInfo.depthForNode(callNodeIndex); const [rangeStart, rangeEnd] = callNodeInfo.getSuffixOrderIndexRangeForCallNode(callNodeIndex); const suffixOrderedCallNodes = callNodeInfo.getSuffixOrderedCallNodes(); @@ -713,7 +712,6 @@ export function computeCallTreeTimingsInverted( { callNodeSelf, rootTotalSummary }: CallNodeSelfAndSummary ): CallTreeTimingsInverted { const roots = callNodeInfo.getRoots(); - const invertedCallNodeTable = callNodeInfo.getCallNodeTable(); const callNodeTable = callNodeInfo.getNonInvertedCallNodeTable(); const callNodeTableFuncCol = callNodeTable.func; const callNodeTableDepthCol = callNodeTable.depth; @@ -731,8 +729,7 @@ export function computeCallTreeTimingsInverted( // the self function of the non-inverted call node. const func = callNodeTableFuncCol[i]; const rootNode = roots.find( - (invertedCallNode) => - invertedCallNodeTable.func[invertedCallNode] === func + (invertedCallNode) => callNodeInfo.funcForNode(invertedCallNode) === func ); if (rootNode === undefined) { throw new Error( diff --git a/src/profile-logic/line-timings.js b/src/profile-logic/line-timings.js index 3d0fd3443b..9907eb7359 100644 --- a/src/profile-logic/line-timings.js +++ b/src/profile-logic/line-timings.js @@ -286,7 +286,7 @@ export function getStackLineInfoForCallNodeInverted( callNodeIndex: IndexIntoCallNodeTable, callNodeInfo: CallNodeInfoInverted ): StackLineInfo { - const depth = callNodeInfo.getCallNodeTable().depth[callNodeIndex]; + const depth = callNodeInfo.depthForNode(callNodeIndex); const [rangeStart, rangeEnd] = callNodeInfo.getSuffixOrderIndexRangeForCallNode(callNodeIndex); const callNodeIsRootOfInvertedTree = callNodeInfo.isRoot(callNodeIndex); diff --git a/src/profile-logic/profile-data.js b/src/profile-logic/profile-data.js index e421a71c95..c16362c5a9 100644 --- a/src/profile-logic/profile-data.js +++ b/src/profile-logic/profile-data.js @@ -4118,7 +4118,7 @@ export function getNativeSymbolsForCallNodeInverted( stackTable: StackTable, frameTable: FrameTable ): IndexIntoNativeSymbolTable[] { - const depth = callNodeInfo.getCallNodeTable().depth[callNodeIndex]; + const depth = callNodeInfo.depthForNode(callNodeIndex); const [rangeStart, rangeEnd] = callNodeInfo.getSuffixOrderIndexRangeForCallNode(callNodeIndex); const stackTablePrefixCol = stackTable.prefix; @@ -4196,8 +4196,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/transforms.js b/src/profile-logic/transforms.js index 0a661c2566..34c2c59d42 100644 --- a/src/profile-logic/transforms.js +++ b/src/profile-logic/transforms.js @@ -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/test/store/__snapshots__/profile-view.test.js.snap b/src/test/store/__snapshots__/profile-view.test.js.snap index fcd323f4c2..9bf1dad97b 100644 --- a/src/test/store/__snapshots__/profile-view.test.js.snap +++ b/src/test/store/__snapshots__/profile-view.test.js.snap @@ -2574,109 +2574,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", diff --git a/src/types/profile-derived.js b/src/types/profile-derived.js index 66e9c03183..49c491a5c6 100644 --- a/src/types/profile-derived.js +++ b/src/types/profile-derived.js @@ -357,6 +357,29 @@ export interface CallNodeInfo { // Returns the list of children of a node. getChildren(callNodeIndex: IndexIntoCallNodeTable): IndexIntoCallNodeTable[]; + + // 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 | null; } // An index into SuffixOrderedCallNodes. From 1e9c8baf66559261f8af2406caa3c5e887c1c48a Mon Sep 17 00:00:00 2001 From: Markus Stange Date: Sat, 20 Jan 2024 00:32:48 -0500 Subject: [PATCH 14/31] Remove now-unused getCallNodeTable(). This just stops exposing it from the interface. The way we compute it will change in the next commit. --- src/profile-logic/call-node-info.js | 4 ---- src/types/profile-derived.js | 4 ---- 2 files changed, 8 deletions(-) diff --git a/src/profile-logic/call-node-info.js b/src/profile-logic/call-node-info.js index 96945f9108..2b605cc70b 100644 --- a/src/profile-logic/call-node-info.js +++ b/src/profile-logic/call-node-info.js @@ -66,10 +66,6 @@ export class CallNodeInfoImpl implements CallNodeInfo { return null; } - getCallNodeTable(): CallNodeTable { - return this._callNodeTable; - } - getNonInvertedCallNodeTable(): CallNodeTable { return this._nonInvertedCallNodeTable; } diff --git a/src/types/profile-derived.js b/src/types/profile-derived.js index 49c491a5c6..b10a9a818f 100644 --- a/src/types/profile-derived.js +++ b/src/types/profile-derived.js @@ -314,10 +314,6 @@ export interface CallNodeInfo { // Returns this object as CallNodeInfoInverted if isInverted(), otherwise null. asInverted(): CallNodeInfoInverted | null; - // 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; From a0db0ec8f7783c7838b6a0536470c18ffa795b25 Mon Sep 17 00:00:00 2001 From: Markus Stange Date: Sat, 20 Jan 2024 11:47:47 -0500 Subject: [PATCH 15/31] Create inverted call nodes lazily. This is the main commit of this PR. Now that nothing is relying on having an inverted call node for each sample, or on having a fully-computed inverted call node table, we can make it so that we only add entries to the inverted call node table when we actually need a node, for example because it was revealed in the call tree. This makes it a lot faster to click the "Invert call stack" checkbox - before this commit, we were computing a lot of inverted call nodes that were never shown to the user. After this commit, CallNodeInfoInvertedImpl no longer inherits from CallNodeInfoImpl - it is now a fully separate implementation. This commit reduces the time spent in `getInvertedCallNodeInfo` on an example profile (https://share.firefox.dev/411Vg2T) from 11 seconds to 718 ms. Before: https://share.firefox.dev/3CTNApp After: https://share.firefox.dev/492F7wl (15x faster) --- src/profile-logic/call-node-info.js | 1053 ++++++++++++++++- src/profile-logic/profile-data.js | 155 +-- src/selectors/per-thread/stack-sample.js | 8 +- .../ProfileCallTreeView.test.js.snap | 16 +- .../__snapshots__/profile-view.test.js.snap | 315 +---- src/test/unit/address-timings.test.js | 4 +- src/test/unit/line-timings.test.js | 4 +- src/test/unit/profile-data.test.js | 12 +- src/test/unit/profile-tree.test.js | 4 +- src/types/profile-derived.js | 19 +- src/utils/bisect.js | 129 -- src/utils/path.js | 13 +- 12 files changed, 1068 insertions(+), 664 deletions(-) diff --git a/src/profile-logic/call-node-info.js b/src/profile-logic/call-node-info.js index 2b605cc70b..72b122f376 100644 --- a/src/profile-logic/call-node-info.js +++ b/src/profile-logic/call-node-info.js @@ -4,9 +4,13 @@ // @flow -import { hashPath } from 'firefox-profiler/utils/path'; -import { bisectEqualRange } from 'firefox-profiler/utils/bisect'; -import { compareNonInvertedCallNodesInSuffixOrderWithPath } from 'firefox-profiler/profile-logic/profile-data'; +import { + hashPath, + concatHash, + hashPathSingleFunc, +} from 'firefox-profiler/utils/path'; +import { ensureExists } from '../utils/flow'; +import { bisectionRightByKey } from '../utils/bisect'; import type { IndexIntoFuncTable, @@ -18,6 +22,8 @@ import type { SuffixOrderIndex, IndexIntoCategoryList, IndexIntoNativeSymbolTable, + IndexIntoSubcategoryListForCategory, + InnerWindowID, } from 'firefox-profiler/types'; /** @@ -27,17 +33,11 @@ import type { * By the end of this commit stack, it will no longer inherit from this class and * will have its own implementation. */ -export class CallNodeInfoImpl implements CallNodeInfo { - // The call node table. This is either the inverted or the non-inverted call - // node table, depending on isInverted(). +export class CallNodeInfoNonInvertedImpl implements CallNodeInfo { + // The call node table. (always non-inverted) _callNodeTable: CallNodeTable; - // The non-inverted call node table, regardless of isInverted(). - _nonInvertedCallNodeTable: 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 @@ -47,27 +47,23 @@ export class CallNodeInfoImpl implements CallNodeInfo { constructor( callNodeTable: CallNodeTable, - nonInvertedCallNodeTable: CallNodeTable, stackIndexToNonInvertedCallNodeIndex: Int32Array ) { this._callNodeTable = callNodeTable; - this._nonInvertedCallNodeTable = nonInvertedCallNodeTable; this._stackIndexToNonInvertedCallNodeIndex = stackIndexToNonInvertedCallNodeIndex; } isInverted(): boolean { - // Overridden in subclass return false; } asInverted(): CallNodeInfoInverted | null { - // Overridden in subclass return null; } getNonInvertedCallNodeTable(): CallNodeTable { - return this._nonInvertedCallNodeTable; + return this._callNodeTable; } getStackIndexToNonInvertedCallNodeIndex(): Int32Array { @@ -278,51 +274,507 @@ export class CallNodeInfoImpl implements CallNodeInfo { } } +// 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 + // null: no inlining + sourceFramesInlinedIntoSymbol: Array, + // 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 + // null: 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. + // For a non-root node x of the inverted tree, let k = depth[x] its depth in the inverted tree, + // and deepNodes = deepNodes[x] be its non-null deep nodes. + // Then, for every index i in suffixOrderIndexRangeStart[x]..suffixOrderIndexRangeEnd[x], + // the k'th prefix node of suffixOrderedCallNodes[i] is stored at deepNodes[x][i - suffixOrderIndexRangeStart[x]]. + deepNodes: Array, // IndexIntoInvertedNonRootCallNodeTable -> (Uint32Array | null) + + depth: number[], // IndexIntoInvertedNonRootCallNodeTable -> number + length: number, +|}; + +// Compute the "suffix order index range" for each root of the inverted call +// node info, i.e. the range of suffix order indexes so that all non-inverted +// call nodes in that range have a call path which ends with the root's func. +// The returned array `rangeEnd` has just the (exclusive) end of those ranges; +// the start of each range is the end of the previous range, or zero. +// +// More explicitly, the suffix order index range for the inverted root for func X is: +// (X == 0 ? 0 : rangeEnd[X - 1]) .. rangeEnd[X] +function _computeInvertedRootSuffixOrderIndexRanges( + callNodeTable: CallNodeTable, + suffixOrderedCallNodes: Uint32Array, + funcCount: number +): Uint32Array { + const rootSuffixOrderIndexRangeEndCol = new Uint32Array(funcCount); + const callNodeCount = suffixOrderedCallNodes.length; + + // suffixOrderedCallNodes is ordered by callNodeTable.func[callNodeIndex]. + // Walk it from front to back and terminate the index ranges whenever the + // func changes. + let currentFunc = 0; + for (let i = 0; i < callNodeCount; i++) { + const callNodeIndex = suffixOrderedCallNodes[i]; + const callNodeFunc = callNodeTable.func[callNodeIndex]; + // assert(currentFunc <= callNodeFunc, "guaranteed by suffix order") + // If the current node has a different func from currentFunc, this means + // that the range for currentFunc ends at i. + // There may also be funcs with empty ranges between currentFunc and callNodeFunc. + for (; currentFunc < callNodeFunc; currentFunc++) { + rootSuffixOrderIndexRangeEndCol[currentFunc] = i; + } + } + // Terminate the current func, and any remaining funcs in the funcTable for + // which there is no non-inverted call node whose call path ends in that func. + for (; currentFunc < funcCount; currentFunc++) { + rootSuffixOrderIndexRangeEndCol[currentFunc] = callNodeCount; + } + + return rootSuffixOrderIndexRangeEndCol; +} + +// 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 Array(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 all columns to zero / null for this root. + sourceFramesInlinedIntoSymbol[funcIndex] = null; + // (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, + }; +} + +// 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: IndexIntoFuncTable[], + // The number of deep nodes for each child. Every entry is non-zero. + deepNodeCountPerChild: number[], + // The deep nodes of all children, concatenated into a single array. + // The length of this array is the sum of the values in deepNodeCountPerChild. + childrenDeepNodes: Uint32Array, + // The suffixOrderIndexRangeStart of the first child. + childrenSuffixOrderIndexRangeStart: number, +|}; + /** - * A subclass of CallNodeInfoImpl for "invert call stack" mode. + * This is the implementation of the CallNodeInfoInverted interface. + * + * The most interesting part of this class is the _createChildren method. This is + * the place where inverted nodes are "materialized" on demand. + * + * ## On-demand node creation + * + * 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. + * + * For any inverted tree node inX, _invertedNonRootCallNodeTable either contains + * none or all of inX's children. + * For any inverted non-root node inQ whose parent node is 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 + * + * To know what the children of a node in the inverted tree are, we need to look + * at the parents in the non-inverted tree. + * + * ``` + * 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 roots: + * Represents call paths ending in + * - [in0] A (so:0..3) = A = ... A (cn0, cn4, cn2) + * - [in1] B (so:3..5) = B = ... B (cn1, cn5) + * - [in2] C (so:5..7) = C = ... C (cn6, cn3) + * ``` + * + * First, let's create the children for in0, which is the root for func A. + * in0 has three "self nodes": cn0, cn4, and cn2. + * + * in0's func is A. + * cn0, cn4, and cn2 also have func A. Of course; that's what makes them in0's self funcs. + * + * 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, whose func is A. + * cn2's parent is cn1, whose func is 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, self nodes [cn4] + * - in4: func B, parent in0, self nodes [cn2] + * + * Now we're done! + * + * --- + * + * Now let's create the children of a non-root node in the inverted tree. + * We want to create the children for in4. + * in4 describes the call path suffix "... -> B -> A". + * + * in4 has one self node: cn2. This is the only non-inverted node whose call path + * ends in "... -> B -> A". + * + * in4 has depth 1. + * + * in4's func is B. + * cn2's func is A. (!) + * + * cn2's func still corresponds to the inverted root, i.e. in0's func. + * But cn2's parent, cn1, has func B. + * + * And cn1's parent, cn0, has func A. + * + * So in4 has one child, with func A. Let's create it: + * - in5: func A, parent in4, self nodes [cn2] + * + * What this example shows is that we need to look not at a self node's immediate + * parent, but rather at its (k + 1)'th parent, where k is the depth of the + * inverted node whose children we're creating. * - * This currently shares its implementation with CallNodeInfoImpl; - * this._callNodeTable is the inverted call node table. + * --- * - * By the end of this commit stack, we will no longer have an inverted call node - * table and this class will stop inheriting from CallNodeInfoImpl. + * What are the children of in5? + * + * in5 has one self node: cn2. + * in5 has depth 2. + * + * cn2's 0th parent is cn2. + * cn2's 1st parent is cn1. + * cn2's 2nd parent (i.e. its grandparent) is cn0. + * cn2's 3rd parent is ... it does not have one! + * + * So in5 has no children. + * + * --- + * + * Now let's say we want to create the children of an inverted node with depth 20, + * and it has 500 self nodes. We would need to look at each self node, find its + * 21st parent node, and then check that node's func. + * + * Climbing up the parent chain 20 steps, for each of the 500 self nodes, would + * be quite expensive. It would be better if we had stored the 20th parent for + * each of the self nodes, so that we would only need to go up to the immediate + * parent. + * + * So that's what we do. 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. + * 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 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 | + * |-----------|-------------------------| + * + * in5 (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 CallNodeInfoInvertedImpl - extends CallNodeInfoImpl - implements CallNodeInfoInverted -{ +export class CallNodeInfoInvertedImpl implements CallNodeInfoInverted { + // 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, i.e. this._roots.length. + _rootCount: number; + + // All inverted call tree roots. The roots of the inverted call tree are the + // "self" functions of the non-inverted call paths. + _roots: InvertedCallNodeHandle[]; + // 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, - nonInvertedCallNodeTable: CallNodeTable, stackIndexToNonInvertedCallNodeIndex: Int32Array, - suffixOrderedCallNodes: Uint32Array, - suffixOrderIndexes: Uint32Array + suffixOrderedCallNodes: Uint32Array, // IndexIntoCallNodeTable[], + suffixOrderIndexes: Uint32Array, // Map, + defaultCategory: IndexIntoCategoryList, + funcCount: number ) { - super( - callNodeTable, - nonInvertedCallNodeTable, - stackIndexToNonInvertedCallNodeIndex - ); + this._callNodeTable = callNodeTable; + this._stackIndexToNonInvertedCallNodeIndex = + stackIndexToNonInvertedCallNodeIndex; this._suffixOrderedCallNodes = suffixOrderedCallNodes; this._suffixOrderIndexes = suffixOrderIndexes; + this._defaultCategory = defaultCategory; + + const rootCount = funcCount; + this._rootCount = rootCount; + + const roots = new Array(rootCount); + for (let i = 0; i < rootCount; i++) { + roots[i] = i; + } + this._roots = roots; + + const rootSuffixOrderIndexRangeEndCol = + _computeInvertedRootSuffixOrderIndexRanges( + callNodeTable, + suffixOrderedCallNodes, + funcCount + ); + const invertedRootCallNodeTable = _createInvertedRootCallNodeTable( + callNodeTable, + rootSuffixOrderIndexRangeEndCol, + suffixOrderedCallNodes, + defaultCategory + ); + this._invertedRootCallNodeTable = invertedRootCallNodeTable; + this._invertedNonRootCallNodeTable = + _createEmptyInvertedNonRootCallNodeTable(); } isInverted(): boolean { return true; } - asInverted(): CallNodeInfoInverted | null { + asInverted(): CallNodeInfoInvertedImpl | null { return this; } + getNonInvertedCallNodeTable(): CallNodeTable { + return this._callNodeTable; + } + + getStackIndexToNonInvertedCallNodeIndex(): Int32Array { + return this._stackIndexToNonInvertedCallNodeIndex; + } + getSuffixOrderedCallNodes(): Uint32Array { return this._suffixOrderedCallNodes; } @@ -331,21 +783,528 @@ export class CallNodeInfoInvertedImpl return this._suffixOrderIndexes; } + getRoots(): Array { + return this._roots; + } + + isRoot(nodeHandle: InvertedCallNodeHandle): boolean { + return nodeHandle < this._rootCount; + } + getSuffixOrderIndexRangeForCallNode( - callNodeIndex: IndexIntoCallNodeTable + nodeHandle: InvertedCallNodeHandle ): [SuffixOrderIndex, SuffixOrderIndex] { - // `callNodeIndex` is an inverted call node. Translate it to a call path. - const callPath = this.getCallNodePathFromIndex(callNodeIndex); - return bisectEqualRange( - this._suffixOrderedCallNodes, - // comparedCallNodeIndex is a non-inverted call node. Compare it to the - // call path for our inverted call node. - (comparedCallNodeIndex) => - compareNonInvertedCallNodesInSuffixOrderWithPath( - comparedCallNodeIndex, - callPath, - this._nonInvertedCallNodeTable - ) + 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 []; + } + + return this._createChildrenForInfo(childrenInfo, parentNodeHandle); + } + + /** + * Compute the information needed to create the children of parentNodeHandle. + * + * 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); + if (parentIndexRangeStart + parentDeepNodeCount !== parentIndexRangeEnd) { + throw new Error('indexes out of sync'); + } + + const callNodeTable = this._callNodeTable; + + // Count how many of the parent's deep nodes end at the parent. If there + // are any, they will all be at the start of the parentDeepNodes array by + // construction of the suffix order. + let nodesWhichEndHereCount = 0; + while (nodesWhichEndHereCount < parentDeepNodeCount) { + const deepNode = parentDeepNodes[nodesWhichEndHereCount]; + if (callNodeTable.prefix[deepNode] !== -1) { + break; + } + nodesWhichEndHereCount++; + } + + if (nodesWhichEndHereCount === parentDeepNodeCount) { + // All deep nodes ended at the parent's depth. The parent has no children. + return null; + } + + const childrenDeepNodeCount = parentDeepNodeCount - nodesWhichEndHereCount; + const childrenDeepNodes = new Uint32Array(childrenDeepNodeCount); + // assert(childrenDeepNodeCount > 0); + + // Iterate over the remaining deep nodes, get each deep node's prefix, + // and build up our list of children. For each child, compute its func and + // its number of deep nodes. + + const firstChildFirstParentDeepNode = + parentDeepNodes[nodesWhichEndHereCount]; + const firstChildFirstDeepNode = + callNodeTable.prefix[firstChildFirstParentDeepNode]; + childrenDeepNodes[0] = firstChildFirstDeepNode; + const firstChildFunc = callNodeTable.func[firstChildFirstDeepNode]; + + const deepNodeCountPerChild = []; + const funcPerChild = []; + + let currentChildFunc = firstChildFunc; + let currentChildDeepNodeCount = 1; + for (let j = 1; j < childrenDeepNodeCount; j++) { + const parentDeepNode = parentDeepNodes[nodesWhichEndHereCount + j]; + const deepNode = callNodeTable.prefix[parentDeepNode]; + childrenDeepNodes[j] = deepNode; + // assert(deepNode !== -1, "parentDeepNodes is sorted so that all call paths which end at this depth come first (by definition of the suffix order), and we already skipped those"); + const deepNodeFunc = callNodeTable.func[deepNode]; + // assert(currentChildFunc <= deepNodeFunc, "parentDeepNodes is sorted by prefix func, by definition of the suffix order (at least in this range, because the rest of the call path is identical for all nodes in parentDeepNodes)"); + + if (deepNodeFunc !== currentChildFunc) { + funcPerChild.push(currentChildFunc); + deepNodeCountPerChild.push(currentChildDeepNodeCount); + currentChildFunc = deepNodeFunc; + currentChildDeepNodeCount = 0; + } + currentChildDeepNodeCount++; + } + funcPerChild.push(currentChildFunc); + deepNodeCountPerChild.push(currentChildDeepNodeCount); + + const childrenSuffixOrderIndexRangeStart = + parentIndexRangeStart + nodesWhichEndHereCount; + + return { + funcPerChild, + deepNodeCountPerChild, + childrenSuffixOrderIndexRangeStart, + childrenDeepNodes, + }; + } + + /** + * 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], + * 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; + } + + _getChildWithFunc( + childrenSortedByFunc: InvertedCallNodeHandle[], + func: IndexIntoFuncTable + ): InvertedCallNodeHandle | null { + const index = bisectionRightByKey(childrenSortedByFunc, func, (node) => + this.funcForNode(node) + ); + if (index === 0) { + return null; + } + const childNodeHandle = childrenSortedByFunc[index - 1]; + if (this.funcForNode(childNodeHandle) !== func) { + return null; + } + return childNodeHandle; + } + + _findDeepestKnownAncestor(callPath: CallNodePath): InvertedCallNodeHandle { + const completePathNode = this._cache.get(hashPath(callPath)); + if (completePathNode !== undefined) { + return completePathNode; + } + + 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; + } + + /** + * 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. + this._invertedNonRootCallNodeTable.deepNodes[nonRootIndex] = null; + return deepNodes; + } + + // This function returns a 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 a CallNodePath. + 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; + let deepestKnownAncestor = this._findDeepestKnownAncestor(callNodePath); + let deepestKnownAncestorDepth = this.depthForNode(deepestKnownAncestor); + + while (deepestKnownAncestorDepth < pathDepth) { + const currentChildFunc = callNodePath[deepestKnownAncestorDepth + 1]; + const children = this.getChildren(deepestKnownAncestor); + 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; + } + deepestKnownAncestor = childMatchingFunc; + deepestKnownAncestorDepth++; + } + return deepestKnownAncestor; + } + + // 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 | null { + 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/profile-data.js b/src/profile-logic/profile-data.js index c16362c5a9..75fb8f1bf3 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, CallNodeInfoInvertedImpl } from './call-node-info'; +import { + CallNodeInfoNonInvertedImpl, + CallNodeInfoInvertedImpl, +} from './call-node-info'; import { computeThreadCPURatio } from './cpu'; import { INSTANT, @@ -119,8 +122,7 @@ export function getCallNodeInfo( funcTable, defaultCategory ); - return new CallNodeInfoImpl( - callNodeTable, + return new CallNodeInfoNonInvertedImpl( callNodeTable, stackIndexToCallNodeIndex ); @@ -440,33 +442,20 @@ function _createCallNodeTableFromUnorderedComponents( * Generate the inverted CallNodeInfo for a thread. */ export function getInvertedCallNodeInfo( - thread: Thread, nonInvertedCallNodeTable: CallNodeTable, stackIndexToNonInvertedCallNodeIndex: Int32Array, - defaultCategory: IndexIntoCategoryList + defaultCategory: IndexIntoCategoryList, + funcCount: number ): CallNodeInfoInverted { - // We compute an inverted stack table, but we don't let it escape this function. - const { invertedThread } = _computeThreadWithInvertedStackTable( - thread, - defaultCategory - ); - - // Create an inverted call node table based on the inverted stack table. - const { callNodeTable } = computeCallNodeTable( - invertedThread.stackTable, - invertedThread.frameTable, - invertedThread.funcTable, - defaultCategory - ); + const callNodeCount = nonInvertedCallNodeTable.length; + const suffixOrderedCallNodes = new Uint32Array(callNodeCount); + const suffixOrderIndexes = new Uint32Array(callNodeCount); // TEMPORARY: Compute a suffix order for the entire non-inverted call node table. // See the CallNodeInfoInverted interface for more details about the suffix order. // By the end of this commit stack, the suffix order will be computed incrementally // as inverted nodes are created; we won't compute the entire order upfront. - const nonInvertedCallNodeCount = nonInvertedCallNodeTable.length; - const suffixOrderedCallNodes = new Uint32Array(nonInvertedCallNodeCount); - const suffixOrderIndexes = new Uint32Array(nonInvertedCallNodeCount); - for (let i = 0; i < nonInvertedCallNodeCount; i++) { + for (let i = 0; i < callNodeCount; i++) { suffixOrderedCallNodes[i] = i; } suffixOrderedCallNodes.sort((a, b) => @@ -477,11 +466,12 @@ export function getInvertedCallNodeInfo( } return new CallNodeInfoInvertedImpl( - callNodeTable, nonInvertedCallNodeTable, stackIndexToNonInvertedCallNodeIndex, suffixOrderedCallNodes, - suffixOrderIndexes + suffixOrderIndexes, + defaultCategory, + funcCount ); } @@ -525,31 +515,6 @@ function _compareNonInvertedCallNodesInSuffixOrder( return 0; } -// Same as _compareNonInvertedCallNodesInSuffixOrder, but takes a call path for -// callNodeB. This is used in the getSuffixOrderIndexRangeForCallNode implementation -// of CallNodeInfoInvertedImpl, which doesn't have easy access to the non-inverted -// call node index for callPathB. -export function compareNonInvertedCallNodesInSuffixOrderWithPath( - callNodeA: IndexIntoCallNodeTable, - callPathB: CallNodePath, - nonInvertedCallNodeTable: CallNodeTable -): number { - for (let i = 0; i < callPathB.length - 1; i++) { - const funcA = nonInvertedCallNodeTable.func[callNodeA]; - const funcB = callPathB[i]; - if (funcA !== funcB) { - return funcA - funcB; - } - callNodeA = nonInvertedCallNodeTable.prefix[callNodeA]; - if (callNodeA === -1) { - return -1; - } - } - const funcA = nonInvertedCallNodeTable.func[callNodeA]; - const funcB = callPathB[callPathB.length - 1]; - return funcA - funcB; -} - // Given a stack index `needleStack` and a call node in the inverted tree // `invertedCallTreeNode`, find an ancestor stack of `needleStack` which // corresponds to the given call node in the inverted call tree. Returns null if @@ -2386,98 +2351,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. */ diff --git a/src/selectors/per-thread/stack-sample.js b/src/selectors/per-thread/stack-sample.js index b2ac8a0d3d..0799188b80 100644 --- a/src/selectors/per-thread/stack-sample.js +++ b/src/selectors/per-thread/stack-sample.js @@ -116,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 ); } ); 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/store/__snapshots__/profile-view.test.js.snap b/src/test/store/__snapshots__/profile-view.test.js.snap index 9bf1dad97b..d48be3b90e 100644 --- a/src/test/store/__snapshots__/profile-view.test.js.snap +++ b/src/test/store/__snapshots__/profile-view.test.js.snap @@ -2130,7 +2130,7 @@ Object { `; exports[`snapshots of selectors/profile matches the last stored run of selectedThreadSelector.getCallNodeInfo 1`] = ` -CallNodeInfoImpl { +CallNodeInfoNonInvertedImpl { "_cache": Map {}, "_callNodeTable": Object { "category": Int32Array [ @@ -2235,109 +2235,6 @@ CallNodeInfoImpl { 9, ], }, - "_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, - ], - }, "_stackIndexToNonInvertedCallNodeIndex": Int32Array [ 0, 1, @@ -2354,7 +2251,7 @@ CallNodeInfoImpl { exports[`snapshots of selectors/profile matches the last stored run of selectedThreadSelector.getCallTree 1`] = ` CallTree { - "_callNodeInfo": CallNodeInfoImpl { + "_callNodeInfo": CallNodeInfoNonInvertedImpl { "_cache": Map {}, "_callNodeTable": Object { "category": Int32Array [ @@ -2459,109 +2356,6 @@ CallTree { 9, ], }, - "_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, - ], - }, "_stackIndexToNonInvertedCallNodeIndex": Int32Array [ 0, 1, @@ -2646,7 +2440,7 @@ CallTree { 0, 0, ], - "_callNodeInfo": CallNodeInfoImpl { + "_callNodeInfo": CallNodeInfoNonInvertedImpl { "_cache": Map {}, "_callNodeTable": Object { "category": Int32Array [ @@ -2751,109 +2545,6 @@ CallTree { 9, ], }, - "_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, - ], - }, "_stackIndexToNonInvertedCallNodeIndex": Int32Array [ 0, 1, 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 aedfb74897..373e2a10fa 100644 --- a/src/test/unit/profile-data.test.js +++ b/src/test/unit/profile-data.test.js @@ -586,10 +586,10 @@ describe('getInvertedCallNodeInfo', function () { ); const invertedCallNodeInfo = getInvertedCallNodeInfo( - thread, nonInvertedCallNodeInfo.getNonInvertedCallNodeTable(), nonInvertedCallNodeInfo.getStackIndexToNonInvertedCallNodeIndex(), - defaultCategory + defaultCategory, + thread.funcTable.length ); // This function is used to test `getSuffixOrderIndexRangeForCallNode` and @@ -974,10 +974,10 @@ describe('getSamplesSelectedStates', function () { ); const callNodeInfoInverted = getInvertedCallNodeInfo( - thread, callNodeInfo.getNonInvertedCallNodeTable(), stackIndexToCallNodeIndex, - defaultCategory + defaultCategory, + thread.funcTable.length ); return { @@ -1520,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 7a2b6fb9f3..908a9c2f0a 100644 --- a/src/test/unit/profile-tree.test.js +++ b/src/test/unit/profile-tree.test.js @@ -457,10 +457,10 @@ 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, diff --git a/src/types/profile-derived.js b/src/types/profile-derived.js index b10a9a818f..79a923bbd0 100644 --- a/src/types/profile-derived.js +++ b/src/types/profile-derived.js @@ -477,16 +477,16 @@ export type SuffixOrderIndex = number; * ``` * 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) + * - [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) - * - [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) + * - [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 @@ -502,7 +502,6 @@ export type SuffixOrderIndex = number; * * 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) - * */ export interface CallNodeInfoInverted extends CallNodeInfo { // Get a mapping SuffixOrderIndex -> IndexIntoNonInvertedCallNodeTable. diff --git a/src/utils/bisect.js b/src/utils/bisect.js index 05fe86adbd..16dcc4e724 100644 --- a/src/utils/bisect.js +++ b/src/utils/bisect.js @@ -208,132 +208,3 @@ export function bisectionLeft( return low; } - -/* - * TEMPORARY: The functions below implement bisectEqualRange(). The implementation - * is copied from https://searchfox.org/mozilla-central/rev/8b0666aff1197e1dd8017de366343de9c21ee437/mfbt/BinarySearch.h#132-243 - * The only code calling bisectEqualRange will be removed by the end of this - * commit stack, so all the code added here will be removed again, too. - * - * bisectLowerBound(), bisectUpperBound(), and bisectEqualRange() are equivalent to - * std::lower_bound(), std::upper_bound(), and std::equal_range() respectively. - * - * bisectLowerBound() returns an index pointing to the first element in the range - * in which each element is considered *not less than* the given value passed - * via |aCompare|, or the length of |aContainer| if no such element is found. - * - * bisectUpperBound() returns an index pointing to the first element in the range - * in which each element is considered *greater than* the given value passed - * via |aCompare|, or the length of |aContainer| if no such element is found. - * - * bisectEqualRange() returns a range [first, second) containing all elements are - * considered equivalent to the given value via |aCompare|. If you need - * either the first or last index of the range, bisectLowerBound() or bisectUpperBound(), - * which is slightly faster than bisectEqualRange(), should suffice. - * - * Example (another example is given in TestBinarySearch.cpp): - * - * Vector sortedStrings = ... - * - * struct Comparator { - * const nsACString& mStr; - * explicit Comparator(const nsACString& aStr) : mStr(aStr) {} - * int32_t operator()(const char* aVal) const { - * return Compare(mStr, nsDependentCString(aVal)); - * } - * }; - * - * auto bounds = bisectEqualRange(sortedStrings, 0, sortedStrings.length(), - * Comparator("needle I'm looking for"_ns)); - * printf("Found the range [%zd %zd)\n", bounds.first(), bounds.second()); - * - */ -export function bisectLowerBound( - array: number[] | $TypedArray, - f: (number) => number, // < 0 if arg is before needle, > 0 if after, === 0 if same - low?: number, - high?: number -): number { - low = low || 0; - high = high || array.length; - - if (low < 0 || low > array.length || high < 0 || high > array.length) { - throw new TypeError("low and high must lie within the array's range"); - } - - while (high !== low) { - const middle = (low + high) >> 1; - const result = f(array[middle]); - - // The range returning from bisectLowerBound does include elements - // equivalent to the given value i.e. f(element) == 0 - if (result >= 0) { - high = middle; - } else { - low = middle + 1; - } - } - - return low; -} - -export function bisectUpperBound( - array: number[] | $TypedArray, - f: (number) => number, // < 0 if arg is before needle, > 0 if after, === 0 if same - low?: number, - high?: number -): number { - low = low || 0; - high = high || array.length; - - if (low < 0 || low > array.length || high < 0 || high > array.length) { - throw new TypeError("low and high must lie within the array's range"); - } - - while (high !== low) { - const middle = (low + high) >> 1; - const result = f(array[middle]); - - // The range returning from bisectUpperBound does NOT include elements - // equivalent to the given value i.e. f(element) == 0 - if (result > 0) { - high = middle; - } else { - low = middle + 1; - } - } - - return high; -} - -export function bisectEqualRange( - array: number[] | $TypedArray, - f: (number) => number, // < 0 if arg is before needle, > 0 if after, === 0 if same - low?: number, - high?: number -): [number, number] { - low = low || 0; - high = high || array.length; - - if (low < 0 || low > array.length || high < 0 || high > array.length) { - throw new TypeError("low and high must lie within the array's range"); - } - - while (high !== low) { - const middle = (low + high) >> 1; - const result = f(array[middle]); - - if (result > 0) { - high = middle; - } else if (result < 0) { - low = middle + 1; - } else { - return [ - bisectLowerBound(array, f, low, middle), - bisectUpperBound(array, f, middle + 1, high), - ]; - } - } - - return [low, high]; -} 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. From e44ec8ff4e74bbcdf1eaa4f1104c7393df976fd9 Mon Sep 17 00:00:00 2001 From: Markus Stange Date: Sat, 20 Jan 2024 12:17:25 -0500 Subject: [PATCH 16/31] Optimize handling of roots. The new structure gives us a nice guarantee about roots of the inverted tree: There is an inverted root for every func, and their indexes are identical. This makes it really cheap to translate between the call node index and the func index (no conversion or lookup is necessary) and also makes it cheap to check if a node is a root. This commit also replaces a few maps and sets with typed arrays for performance. This is easier now that the root indexes are all contiguous. --- src/profile-logic/call-node-info.js | 44 ++++++--------------- src/profile-logic/call-tree.js | 60 ++++++++++++----------------- src/types/profile-derived.js | 8 ++-- 3 files changed, 41 insertions(+), 71 deletions(-) diff --git a/src/profile-logic/call-node-info.js b/src/profile-logic/call-node-info.js index 72b122f376..8ba67e5834 100644 --- a/src/profile-logic/call-node-info.js +++ b/src/profile-logic/call-node-info.js @@ -194,23 +194,6 @@ export class CallNodeInfoNonInvertedImpl implements CallNodeInfo { return null; } - getRoots(): IndexIntoCallNodeTable[] { - const roots = []; - if (this._callNodeTable.length !== 0) { - // The call node with index 0 is guaruanteed to be a root, by construction - // of the call node table. - // Start with node 0 and add its siblings. - for ( - let root = 0; - root !== -1; - root = this._callNodeTable.nextSibling[root] - ) { - roots.push(root); - } - } - return roots; - } - isRoot(callNodeIndex: IndexIntoCallNodeTable): boolean { return this._callNodeTable.prefix[callNodeIndex] === -1; } @@ -689,13 +672,16 @@ export class CallNodeInfoInvertedImpl implements CallNodeInfoInverted { // The mapping of non-inverted stack index to non-inverted call node index. _stackIndexToNonInvertedCallNodeIndex: Int32Array; - // The number of roots, i.e. this._roots.length. + // 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; - // All inverted call tree roots. The roots of the inverted call tree are the - // "self" functions of the non-inverted call paths. - _roots: InvertedCallNodeHandle[]; - // 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. @@ -732,15 +718,7 @@ export class CallNodeInfoInvertedImpl implements CallNodeInfoInverted { this._suffixOrderedCallNodes = suffixOrderedCallNodes; this._suffixOrderIndexes = suffixOrderIndexes; this._defaultCategory = defaultCategory; - - const rootCount = funcCount; - this._rootCount = rootCount; - - const roots = new Array(rootCount); - for (let i = 0; i < rootCount; i++) { - roots[i] = i; - } - this._roots = roots; + this._rootCount = funcCount; const rootSuffixOrderIndexRangeEndCol = _computeInvertedRootSuffixOrderIndexRanges( @@ -783,8 +761,8 @@ export class CallNodeInfoInvertedImpl implements CallNodeInfoInverted { return this._suffixOrderIndexes; } - getRoots(): Array { - return this._roots; + getFuncCount(): number { + return this._rootCount; } isRoot(nodeHandle: InvertedCallNodeHandle): boolean { diff --git a/src/profile-logic/call-tree.js b/src/profile-logic/call-tree.js index cc9ae6cbd1..412d8ece12 100644 --- a/src/profile-logic/call-tree.js +++ b/src/profile-logic/call-tree.js @@ -58,8 +58,8 @@ export type CallTreeTimingsInverted = {| callNodeSelf: Float32Array, rootTotalSummary: number, sortedRoots: IndexIntoFuncTable[], - totalPerRootNode: Map, - rootNodesWithChildren: Set, + totalPerRootFunc: Float32Array, + hasChildrenPerRootFunc: Uint8Array, |}; export type CallTreeTimings = @@ -185,8 +185,8 @@ class CallTreeInternalInverted implements CallTreeInternal { _callNodeSelf: Float32Array; _rootNodes: IndexIntoCallNodeTable[]; _funcCount: number; - _totalPerRootNode: Map; - _rootNodesWithChildren: Set; + _totalPerRootFunc: Float32Array; + _hasChildrenPerRootFunc: Uint8Array; _totalAndHasChildrenPerNonRootNode: Map< IndexIntoCallNodeTable, TotalAndHasChildren, @@ -199,10 +199,10 @@ class CallTreeInternalInverted implements CallTreeInternal { this._callNodeInfo = callNodeInfo; this._nonInvertedCallNodeTable = callNodeInfo.getNonInvertedCallNodeTable(); this._callNodeSelf = callTreeTimingsInverted.callNodeSelf; - const { sortedRoots, totalPerRootNode, rootNodesWithChildren } = + const { sortedRoots, totalPerRootFunc, hasChildrenPerRootFunc } = callTreeTimingsInverted; - this._totalPerRootNode = totalPerRootNode; - this._rootNodesWithChildren = rootNodesWithChildren; + this._totalPerRootFunc = totalPerRootFunc; + this._hasChildrenPerRootFunc = hasChildrenPerRootFunc; this._rootNodes = sortedRoots; } @@ -212,7 +212,7 @@ class CallTreeInternalInverted implements CallTreeInternal { hasChildren(callNodeIndex: IndexIntoCallNodeTable): boolean { if (this._callNodeInfo.isRoot(callNodeIndex)) { - return this._rootNodesWithChildren.has(callNodeIndex); + return this._hasChildrenPerRootFunc[callNodeIndex] !== 0; } return this._getTotalAndHasChildren(callNodeIndex).hasChildren; } @@ -238,7 +238,7 @@ class CallTreeInternalInverted implements CallTreeInternal { getSelfAndTotal(callNodeIndex: IndexIntoCallNodeTable): SelfAndTotal { if (this._callNodeInfo.isRoot(callNodeIndex)) { - const total = ensureExists(this._totalPerRootNode.get(callNodeIndex)); + const total = this._totalPerRootFunc[callNodeIndex]; return { self: total, total }; } const { total } = this._getTotalAndHasChildren(callNodeIndex); @@ -643,9 +643,9 @@ export function getSelfAndTotalForCallNode( case 'INVERTED': { const callNodeInfoInverted = ensureExists(callNodeInfo.asInverted()); const { timings } = callTreeTimings; - const { callNodeSelf, totalPerRootNode } = timings; + const { callNodeSelf, totalPerRootFunc } = timings; if (callNodeInfoInverted.isRoot(callNodeIndex)) { - const total = totalPerRootNode.get(callNodeIndex) ?? 0; + const total = totalPerRootFunc[callNodeIndex]; return { self: total, total }; } const { total } = _getInvertedTreeNodeTotalAndHasChildren( @@ -711,13 +711,14 @@ export function computeCallTreeTimingsInverted( callNodeInfo: CallNodeInfoInverted, { callNodeSelf, rootTotalSummary }: CallNodeSelfAndSummary ): CallTreeTimingsInverted { - const roots = callNodeInfo.getRoots(); + const funcCount = callNodeInfo.getFuncCount(); const callNodeTable = callNodeInfo.getNonInvertedCallNodeTable(); const callNodeTableFuncCol = callNodeTable.func; const callNodeTableDepthCol = callNodeTable.depth; - const totalPerRootNode = new Map(); - const rootNodesWithChildren = new Set(); - const seenRoots = new Set(); + 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) { @@ -728,36 +729,25 @@ export function computeCallTreeTimingsInverted( // 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]; - const rootNode = roots.find( - (invertedCallNode) => callNodeInfo.funcForNode(invertedCallNode) === func - ); - if (rootNode === undefined) { - throw new Error( - "Couldn't find the inverted root for a function with non-zero self time." - ); - } - totalPerRootNode.set( - rootNode, - (totalPerRootNode.get(rootNode) ?? 0) + self - ); - seenRoots.add(rootNode); + totalPerRootFunc[func] += self; + if (seenPerRootFunc[func] === 0) { + seenPerRootFunc[func] = 1; + sortedRoots.push(func); + } if (callNodeTableDepthCol[i] !== 0) { - rootNodesWithChildren.add(rootNode); + hasChildrenPerRootFunc[func] = 1; } } - const sortedRoots = [...seenRoots]; sortedRoots.sort( - (a, b) => - Math.abs(totalPerRootNode.get(b) ?? 0) - - Math.abs(totalPerRootNode.get(a) ?? 0) + (a, b) => Math.abs(totalPerRootFunc[b]) - Math.abs(totalPerRootFunc[a]) ); return { callNodeSelf, rootTotalSummary, sortedRoots, - totalPerRootNode, - rootNodesWithChildren, + totalPerRootFunc, + hasChildrenPerRootFunc, }; } diff --git a/src/types/profile-derived.js b/src/types/profile-derived.js index 79a923bbd0..863b09dc0a 100644 --- a/src/types/profile-derived.js +++ b/src/types/profile-derived.js @@ -345,9 +345,6 @@ export interface CallNodeInfo { func: IndexIntoFuncTable ): IndexIntoCallNodeTable | null; - // Returns the list of root nodes. - getRoots(): IndexIntoCallNodeTable[]; - // Returns whether the given node is a root node. isRoot(callNodeIndex: IndexIntoCallNodeTable): boolean; @@ -504,6 +501,11 @@ export type SuffixOrderIndex = number; * Suffix order indexes: [0, 3, 2, 6, 1, 4, 5] (cnX -> soY) */ export interface CallNodeInfoInverted extends CallNodeInfo { + // 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; + // 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. From a891ee2d4cad15a3ba6d276e823c301a33143e1c Mon Sep 17 00:00:00 2001 From: Markus Stange Date: Sat, 20 Jan 2024 12:23:08 -0500 Subject: [PATCH 17/31] Make sourceFramesInlinedIntoSymbol an Int32Array. This avoids a CompareIC when comparing to null in _createInvertedRootCallNodeTable, because we'll now only be comparing integers. This speeds up _createInvertedRootCallNodeTable by almost 2x. --- src/profile-logic/call-node-info.js | 18 ++--- src/profile-logic/call-tree.js | 2 +- src/profile-logic/data-structures.js | 2 +- src/profile-logic/profile-data.js | 12 +-- .../__snapshots__/profile-view.test.js.snap | 80 +++++++++---------- src/types/profile-derived.js | 6 +- 6 files changed, 60 insertions(+), 60 deletions(-) diff --git a/src/profile-logic/call-node-info.js b/src/profile-logic/call-node-info.js index 8ba67e5834..f3083cda18 100644 --- a/src/profile-logic/call-node-info.js +++ b/src/profile-logic/call-node-info.js @@ -252,7 +252,7 @@ export class CallNodeInfoNonInvertedImpl implements CallNodeInfo { sourceFramesInlinedIntoSymbolForNode( callNodeIndex: IndexIntoCallNodeTable - ): IndexIntoNativeSymbolTable | -1 | null { + ): IndexIntoNativeSymbolTable | -1 | -2 { return this._callNodeTable.sourceFramesInlinedIntoSymbol[callNodeIndex]; } } @@ -277,8 +277,8 @@ type InvertedRootCallNodeTable = {| 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 - // null: no inlining - sourceFramesInlinedIntoSymbol: Array, + // -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 @@ -298,8 +298,8 @@ type InvertedNonRootCallNodeTable = {| 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 - // null: no inlining - sourceFramesInlinedIntoSymbol: Array, + // -2: no inlining + sourceFramesInlinedIntoSymbol: Array, suffixOrderIndexRangeStart: SuffixOrderIndex[], // IndexIntoInvertedNonRootCallNodeTable -> SuffixOrderIndex suffixOrderIndexRangeEnd: SuffixOrderIndex[], // IndexIntoInvertedNonRootCallNodeTable -> SuffixOrderIndex @@ -367,7 +367,7 @@ function _createInvertedRootCallNodeTable( const category = new Int32Array(funcCount); const subcategory = new Int32Array(funcCount); const innerWindowID = new Float64Array(funcCount); - const sourceFramesInlinedIntoSymbol = new Array(funcCount); + const sourceFramesInlinedIntoSymbol = new Int32Array(funcCount); let previousRootSuffixOrderIndexRangeEnd = 0; for (let funcIndex = 0; funcIndex < funcCount; funcIndex++) { const callNodeSuffixOrderIndexRangeStart = @@ -383,8 +383,8 @@ function _createInvertedRootCallNodeTable( // this func as its self func. This root only exists for simplicity, so // that there is one root per func. - // Set all columns to zero / null for this root. - sourceFramesInlinedIntoSymbol[funcIndex] = null; + // 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; @@ -1273,7 +1273,7 @@ export class CallNodeInfoInvertedImpl implements CallNodeInfoInverted { sourceFramesInlinedIntoSymbolForNode( callNodeHandle: InvertedCallNodeHandle - ): IndexIntoNativeSymbolTable | -1 | null { + ): IndexIntoNativeSymbolTable | -1 | -2 { if (callNodeHandle < this._rootCount) { const rootFunc = callNodeHandle; return this._invertedRootCallNodeTable.sourceFramesInlinedIntoSymbol[ diff --git a/src/profile-logic/call-tree.js b/src/profile-logic/call-tree.js index 412d8ece12..662ec4bc11 100644 --- a/src/profile-logic/call-tree.js +++ b/src/profile-logic/call-tree.js @@ -406,7 +406,7 @@ export class CallTree { const calledFunction = getFunctionName(funcName); const inlinedIntoNativeSymbol = this._callNodeInfo.sourceFramesInlinedIntoSymbolForNode(callNodeIndex); - if (inlinedIntoNativeSymbol === null) { + if (inlinedIntoNativeSymbol === -2) { return undefined; } 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/profile-data.js b/src/profile-logic/profile-data.js index 75fb8f1bf3..a264e73698 100644 --- a/src/profile-logic/profile-data.js +++ b/src/profile-logic/profile-data.js @@ -160,7 +160,7 @@ export function computeCallNodeTable( const subcategory: Array = []; const innerWindowID: Array = []; const sourceFramesInlinedIntoSymbol: Array< - IndexIntoNativeSymbolTable | -1 | null, + IndexIntoNativeSymbolTable | -1 | -2, > = []; let length = 0; @@ -180,7 +180,7 @@ export function computeCallNodeTable( categoryIndex: IndexIntoCategoryList, subcategoryIndex: IndexIntoSubcategoryListForCategory, windowID: InnerWindowID, - inlinedIntoSymbol: IndexIntoNativeSymbolTable | null + inlinedIntoSymbol: IndexIntoNativeSymbolTable | -1 | -2 ) { const index = length++; prefix[index] = prefixIndex; @@ -233,8 +233,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. @@ -330,7 +330,7 @@ function _createCallNodeTableFromUnorderedComponents( category: Array, subcategory: Array, innerWindowID: Array, - sourceFramesInlinedIntoSymbol: Array, + sourceFramesInlinedIntoSymbol: Array, length: number, stackIndexToCallNodeIndex: Int32Array ): CallNodeTableAndStackMap { @@ -349,7 +349,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; diff --git a/src/test/store/__snapshots__/profile-view.test.js.snap b/src/test/store/__snapshots__/profile-view.test.js.snap index d48be3b90e..896e2a3599 100644 --- a/src/test/store/__snapshots__/profile-view.test.js.snap +++ b/src/test/store/__snapshots__/profile-view.test.js.snap @@ -2201,16 +2201,16 @@ CallNodeInfoNonInvertedImpl { 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, @@ -2322,16 +2322,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, @@ -2511,16 +2511,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, @@ -2626,16 +2626,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, diff --git a/src/types/profile-derived.js b/src/types/profile-derived.js index 863b09dc0a..4b84f9f00c 100644 --- a/src/types/profile-derived.js +++ b/src/types/profile-derived.js @@ -292,10 +292,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. @@ -372,7 +372,7 @@ export interface CallNodeInfo { depthForNode(callNodeIndex: IndexIntoCallNodeTable): number; sourceFramesInlinedIntoSymbolForNode( callNodeIndex: IndexIntoCallNodeTable - ): IndexIntoNativeSymbolTable | -1 | null; + ): IndexIntoNativeSymbolTable | -1 | -2; } // An index into SuffixOrderedCallNodes. From b25d6b5351acb16bc5bcd31252ad512d1578a3f1 Mon Sep 17 00:00:00 2001 From: Markus Stange Date: Sun, 24 Nov 2024 13:05:16 -0500 Subject: [PATCH 18/31] Refine the suffix order incrementally, as new inverted nodes are created. This saves a lot of work upfront that's not needed. At any given time, we just need the suffix order to be accurate enough so that the "suffix order index range" for every existing inverted call node is correct. This commit reduces the time spent in `getInvertedCallNodeInfo` + `getChildren` on an example profile (https://share.firefox.dev/411Vg2T) from 721 ms to 20 ms. Before: https://share.firefox.dev/40Wdi6S After: https://share.firefox.dev/3AZjbpg (35x faster) --- src/profile-logic/call-node-info.js | 367 ++++++++++++++++++++-------- src/profile-logic/profile-data.js | 20 -- src/types/profile-derived.js | 26 ++ 3 files changed, 292 insertions(+), 121 deletions(-) diff --git a/src/profile-logic/call-node-info.js b/src/profile-logic/call-node-info.js index f3083cda18..00701b797d 100644 --- a/src/profile-logic/call-node-info.js +++ b/src/profile-logic/call-node-info.js @@ -314,46 +314,6 @@ type InvertedNonRootCallNodeTable = {| length: number, |}; -// Compute the "suffix order index range" for each root of the inverted call -// node info, i.e. the range of suffix order indexes so that all non-inverted -// call nodes in that range have a call path which ends with the root's func. -// The returned array `rangeEnd` has just the (exclusive) end of those ranges; -// the start of each range is the end of the previous range, or zero. -// -// More explicitly, the suffix order index range for the inverted root for func X is: -// (X == 0 ? 0 : rangeEnd[X - 1]) .. rangeEnd[X] -function _computeInvertedRootSuffixOrderIndexRanges( - callNodeTable: CallNodeTable, - suffixOrderedCallNodes: Uint32Array, - funcCount: number -): Uint32Array { - const rootSuffixOrderIndexRangeEndCol = new Uint32Array(funcCount); - const callNodeCount = suffixOrderedCallNodes.length; - - // suffixOrderedCallNodes is ordered by callNodeTable.func[callNodeIndex]. - // Walk it from front to back and terminate the index ranges whenever the - // func changes. - let currentFunc = 0; - for (let i = 0; i < callNodeCount; i++) { - const callNodeIndex = suffixOrderedCallNodes[i]; - const callNodeFunc = callNodeTable.func[callNodeIndex]; - // assert(currentFunc <= callNodeFunc, "guaranteed by suffix order") - // If the current node has a different func from currentFunc, this means - // that the range for currentFunc ends at i. - // There may also be funcs with empty ranges between currentFunc and callNodeFunc. - for (; currentFunc < callNodeFunc; currentFunc++) { - rootSuffixOrderIndexRangeEndCol[currentFunc] = i; - } - } - // Terminate the current func, and any remaining funcs in the funcTable for - // which there is no non-inverted call node whose call path ends in that func. - for (; currentFunc < funcCount; currentFunc++) { - rootSuffixOrderIndexRangeEndCol[currentFunc] = callNodeCount; - } - - return rootSuffixOrderIndexRangeEndCol; -} - // 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. @@ -482,14 +442,93 @@ function _createEmptyInvertedNonRootCallNodeTable(): InvertedNonRootCallNodeTabl }; } +// 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: IndexIntoFuncTable[], + funcPerChild: Uint32Array, // IndexIntoFuncTable[] // The number of deep nodes for each child. Every entry is non-zero. - deepNodeCountPerChild: number[], - // The deep nodes of all children, concatenated into a single array. - // The length of this array is the sum of the values in deepNodeCountPerChild. + 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, @@ -707,25 +746,24 @@ export class CallNodeInfoInvertedImpl implements CallNodeInfoInverted { constructor( callNodeTable: CallNodeTable, stackIndexToNonInvertedCallNodeIndex: Int32Array, - suffixOrderedCallNodes: Uint32Array, // IndexIntoCallNodeTable[], - suffixOrderIndexes: Uint32Array, // Map, 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 rootSuffixOrderIndexRangeEndCol = - _computeInvertedRootSuffixOrderIndexRanges( - callNodeTable, - suffixOrderedCallNodes, - funcCount - ); const invertedRootCallNodeTable = _createInvertedRootCallNodeTable( callNodeTable, rootSuffixOrderIndexRangeEndCol, @@ -816,11 +854,19 @@ export class CallNodeInfoInvertedImpl implements CallNodeInfoInverted { return []; } + this._applyRefinedSuffixOrderForNode( + parentNodeHandle, + childrenInfo.selfNodesWhichEndAtParent, + childrenInfo.childrenSelfNodes + ); + return this._createChildrenForInfo(childrenInfo, parentNodeHandle); } /** - * Compute the information needed to create the children of 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 @@ -835,67 +881,132 @@ export class CallNodeInfoInvertedImpl implements CallNodeInfoInverted { const parentDeepNodeCount = parentDeepNodes.length; const [parentIndexRangeStart, parentIndexRangeEnd] = this.getSuffixOrderIndexRangeForCallNode(parentNodeHandle); - if (parentIndexRangeStart + parentDeepNodeCount !== parentIndexRangeEnd) { + 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 parentDoopNode, 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; - - // Count how many of the parent's deep nodes end at the parent. If there - // are any, they will all be at the start of the parentDeepNodes array by - // construction of the suffix order. - let nodesWhichEndHereCount = 0; - while (nodesWhichEndHereCount < parentDeepNodeCount) { - const deepNode = parentDeepNodes[nodesWhichEndHereCount]; - if (callNodeTable.prefix[deepNode] !== -1) { - break; + 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); } - nodesWhichEndHereCount++; + } + + 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; } - const childrenDeepNodeCount = parentDeepNodeCount - nodesWhichEndHereCount; + // We create one child for each distinct func we found. The children need to + // be ordered by func. + 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); - // assert(childrenDeepNodeCount > 0); - - // Iterate over the remaining deep nodes, get each deep node's prefix, - // and build up our list of children. For each child, compute its func and - // its number of deep nodes. - - const firstChildFirstParentDeepNode = - parentDeepNodes[nodesWhichEndHereCount]; - const firstChildFirstDeepNode = - callNodeTable.prefix[firstChildFirstParentDeepNode]; - childrenDeepNodes[0] = firstChildFirstDeepNode; - const firstChildFunc = callNodeTable.func[firstChildFirstDeepNode]; - - const deepNodeCountPerChild = []; - const funcPerChild = []; - - let currentChildFunc = firstChildFunc; - let currentChildDeepNodeCount = 1; - for (let j = 1; j < childrenDeepNodeCount; j++) { - const parentDeepNode = parentDeepNodes[nodesWhichEndHereCount + j]; - const deepNode = callNodeTable.prefix[parentDeepNode]; - childrenDeepNodes[j] = deepNode; - // assert(deepNode !== -1, "parentDeepNodes is sorted so that all call paths which end at this depth come first (by definition of the suffix order), and we already skipped those"); - const deepNodeFunc = callNodeTable.func[deepNode]; - // assert(currentChildFunc <= deepNodeFunc, "parentDeepNodes is sorted by prefix func, by definition of the suffix order (at least in this range, because the rest of the call path is identical for all nodes in parentDeepNodes)"); - - if (deepNodeFunc !== currentChildFunc) { - funcPerChild.push(currentChildFunc); - deepNodeCountPerChild.push(currentChildDeepNodeCount); - currentChildFunc = deepNodeFunc; - currentChildDeepNodeCount = 0; - } - currentChildDeepNodeCount++; + 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; } - funcPerChild.push(currentChildFunc); - deepNodeCountPerChild.push(currentChildDeepNodeCount); const childrenSuffixOrderIndexRangeStart = parentIndexRangeStart + nodesWhichEndHereCount; @@ -904,10 +1015,63 @@ export class CallNodeInfoInvertedImpl implements CallNodeInfoInverted { 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. @@ -1125,7 +1289,8 @@ export class CallNodeInfoInvertedImpl implements CallNodeInfoInverted { 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. + // 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; } diff --git a/src/profile-logic/profile-data.js b/src/profile-logic/profile-data.js index a264e73698..de5c1379e5 100644 --- a/src/profile-logic/profile-data.js +++ b/src/profile-logic/profile-data.js @@ -447,29 +447,9 @@ export function getInvertedCallNodeInfo( defaultCategory: IndexIntoCategoryList, funcCount: number ): CallNodeInfoInverted { - const callNodeCount = nonInvertedCallNodeTable.length; - const suffixOrderedCallNodes = new Uint32Array(callNodeCount); - const suffixOrderIndexes = new Uint32Array(callNodeCount); - - // TEMPORARY: Compute a suffix order for the entire non-inverted call node table. - // See the CallNodeInfoInverted interface for more details about the suffix order. - // By the end of this commit stack, the suffix order will be computed incrementally - // as inverted nodes are created; we won't compute the entire order upfront. - for (let i = 0; i < callNodeCount; i++) { - suffixOrderedCallNodes[i] = i; - } - suffixOrderedCallNodes.sort((a, b) => - _compareNonInvertedCallNodesInSuffixOrder(a, b, nonInvertedCallNodeTable) - ); - for (let i = 0; i < suffixOrderedCallNodes.length; i++) { - suffixOrderIndexes[suffixOrderedCallNodes[i]] = i; - } - return new CallNodeInfoInvertedImpl( nonInvertedCallNodeTable, stackIndexToNonInvertedCallNodeIndex, - suffixOrderedCallNodes, - suffixOrderIndexes, defaultCategory, funcCount ); diff --git a/src/types/profile-derived.js b/src/types/profile-derived.js index 4b84f9f00c..2fabf42afc 100644 --- a/src/types/profile-derived.js +++ b/src/types/profile-derived.js @@ -499,6 +499,24 @@ export type SuffixOrderIndex = number; * * 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. */ export interface CallNodeInfoInverted extends CallNodeInfo { // Get the number of functions. There is one root per function. @@ -509,10 +527,18 @@ export interface CallNodeInfoInverted extends CallNodeInfo { // 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; // 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; // Get the [start, exclusiveEnd] range of suffix order indexes for this From 94407256d58409a96572613419b0075f27cd73d4 Mon Sep 17 00:00:00 2001 From: Markus Stange Date: Sun, 24 Nov 2024 18:54:05 -0500 Subject: [PATCH 19/31] Fold the CallNodeInfoInverted interface into the implementation class. --- src/actions/profile-view.js | 2 +- src/components/calltree/CallTree.js | 2 +- src/components/flame-graph/Canvas.js | 2 +- src/components/flame-graph/FlameGraph.js | 2 +- src/components/shared/CallNodeContextMenu.js | 2 +- src/components/shared/thread/CPUGraph.js | 2 +- src/components/shared/thread/StackGraph.js | 2 +- src/components/stack-chart/Canvas.js | 2 +- src/components/stack-chart/index.js | 2 +- src/components/timeline/TrackThread.js | 2 +- src/components/tooltip/CallNode.js | 2 +- src/profile-logic/address-timings.js | 3 +- src/profile-logic/call-node-info.js | 259 ++++++++++++++++-- src/profile-logic/call-tree.js | 3 +- src/profile-logic/line-timings.js | 3 +- src/profile-logic/profile-data.js | 15 +- src/profile-logic/stack-timing.js | 3 +- src/profile-logic/transforms.js | 2 +- src/selectors/per-thread/stack-sample.js | 2 +- .../__snapshots__/profile-view.test.js.snap | 6 +- src/types/actions.js | 2 +- src/types/profile-derived.js | 251 ----------------- 22 files changed, 270 insertions(+), 301 deletions(-) diff --git a/src/actions/profile-view.js b/src/actions/profile-view.js index 223cadbd71..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'; /** diff --git a/src/components/calltree/CallTree.js b/src/components/calltree/CallTree.js index 206f2fe70d..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, diff --git a/src/components/flame-graph/Canvas.js b/src/components/flame-graph/Canvas.js index 0f9e2a9038..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, diff --git a/src/components/flame-graph/FlameGraph.js b/src/components/flame-graph/FlameGraph.js index 8c2934b4e6..6b860d5823 100644 --- a/src/components/flame-graph/FlameGraph.js +++ b/src/components/flame-graph/FlameGraph.js @@ -40,7 +40,6 @@ import type { SamplesLikeTable, PreviewSelection, CallTreeSummaryStrategy, - CallNodeInfo, IndexIntoCallNodeTable, ThreadsKey, InnerWindowID, @@ -48,6 +47,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, diff --git a/src/components/shared/CallNodeContextMenu.js b/src/components/shared/CallNodeContextMenu.js index 0f9a662c40..2075ef94e0 100644 --- a/src/components/shared/CallNodeContextMenu.js +++ b/src/components/shared/CallNodeContextMenu.js @@ -49,7 +49,6 @@ import type { TransformType, ImplementationFilter, IndexIntoCallNodeTable, - CallNodeInfo, CallNodePath, Thread, ThreadsKey, @@ -58,6 +57,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'; type StateProps = {| +thread: Thread | null, 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 90ab920f3f..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, diff --git a/src/components/stack-chart/index.js b/src/components/stack-chart/index.js index 94297e39e5..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'; 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 6eed705c81..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'; diff --git a/src/profile-logic/address-timings.js b/src/profile-logic/address-timings.js index e026ef5775..657895bddc 100644 --- a/src/profile-logic/address-timings.js +++ b/src/profile-logic/address-timings.js @@ -74,8 +74,6 @@ import type { FuncTable, StackTable, SamplesLikeTable, - CallNodeInfo, - CallNodeInfoInverted, IndexIntoCallNodeTable, IndexIntoNativeSymbolTable, StackAddressInfo, @@ -84,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 diff --git a/src/profile-logic/call-node-info.js b/src/profile-logic/call-node-info.js index 00701b797d..ba0670647b 100644 --- a/src/profile-logic/call-node-info.js +++ b/src/profile-logic/call-node-info.js @@ -14,12 +14,9 @@ import { bisectionRightByKey } from '../utils/bisect'; import type { IndexIntoFuncTable, - CallNodeInfo, - CallNodeInfoInverted, CallNodeTable, CallNodePath, IndexIntoCallNodeTable, - SuffixOrderIndex, IndexIntoCategoryList, IndexIntoNativeSymbolTable, IndexIntoSubcategoryListForCategory, @@ -27,13 +24,82 @@ import type { } from 'firefox-profiler/types'; /** - * The implementation of the CallNodeInfo interface. - * - * CallNodeInfoInvertedImpl inherits from this class and shares this implementation. - * By the end of this commit stack, it will no longer inherit from this class and - * will have its own implementation. + * 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 interface CallNodeInfo { + // If true, call node indexes describe nodes in the inverted call tree. + isInverted(): boolean; + + // 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; + + // 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; + + // Returns whether the given node is a root node. + isRoot(callNodeIndex: IndexIntoCallNodeTable): boolean; + + // Returns the list of children of a node. + getChildren(callNodeIndex: IndexIntoCallNodeTable): IndexIntoCallNodeTable[]; + + // 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 CallNodeInfoNonInvertedImpl implements CallNodeInfo { +export class CallNodeInfoNonInverted implements CallNodeInfo { // The call node table. (always non-inverted) _callNodeTable: CallNodeTable; @@ -534,13 +600,150 @@ type ChildrenInfo = {| childrenSuffixOrderIndexRangeStart: number, |}; +// An index into SuffixOrderedCallNodes. +export type SuffixOrderIndex = number; + /** - * This is the implementation of the CallNodeInfoInverted interface. + * 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: * - * The most interesting part of this class is the _createChildren method. This is - * the place where inverted nodes are "materialized" on demand. + * - 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? * - * ## On-demand node creation + * 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 + * 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 * * 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 @@ -555,7 +758,7 @@ type ChildrenInfo = {| * 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 + * ## Computation of the children * * To know what the children of a node in the inverted tree are, we need to look * at the parents in the non-inverted tree. @@ -694,7 +897,7 @@ type ChildrenInfo = {| * deep nodes. They're not needed anymore. So _takeDeepNodesForInvertedNode * nulls out the stored deepNodes for an inverted node when it's called. */ -export class CallNodeInfoInvertedImpl implements CallNodeInfoInverted { +export class CallNodeInfoInverted implements CallNodeInfo { // The non-inverted call node table. _callNodeTable: CallNodeTable; @@ -779,7 +982,7 @@ export class CallNodeInfoInvertedImpl implements CallNodeInfoInverted { return true; } - asInverted(): CallNodeInfoInvertedImpl | null { + asInverted(): CallNodeInfoInverted | null { return this; } @@ -791,14 +994,30 @@ export class CallNodeInfoInvertedImpl implements CallNodeInfoInverted { 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; } @@ -807,6 +1026,14 @@ export class CallNodeInfoInvertedImpl implements CallNodeInfoInverted { 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] { diff --git a/src/profile-logic/call-tree.js b/src/profile-logic/call-tree.js index 662ec4bc11..69a2c75b84 100644 --- a/src/profile-logic/call-tree.js +++ b/src/profile-logic/call-tree.js @@ -21,8 +21,6 @@ import type { CallNodeTable, CallNodePath, IndexIntoCallNodeTable, - CallNodeInfo, - CallNodeInfoInverted, CallNodeData, CallNodeDisplayData, Milliseconds, @@ -37,6 +35,7 @@ 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[]; diff --git a/src/profile-logic/line-timings.js b/src/profile-logic/line-timings.js index 9907eb7359..eac106e0af 100644 --- a/src/profile-logic/line-timings.js +++ b/src/profile-logic/line-timings.js @@ -9,8 +9,6 @@ import type { FuncTable, StackTable, SamplesLikeTable, - CallNodeInfo, - CallNodeInfoInverted, IndexIntoCallNodeTable, IndexIntoStringTable, StackLineInfo, @@ -19,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 diff --git a/src/profile-logic/profile-data.js b/src/profile-logic/profile-data.js index de5c1379e5..2dbe0e794e 100644 --- a/src/profile-logic/profile-data.js +++ b/src/profile-logic/profile-data.js @@ -15,8 +15,8 @@ import { shallowCloneFuncTable, } from './data-structures'; import { - CallNodeInfoNonInvertedImpl, - CallNodeInfoInvertedImpl, + CallNodeInfoNonInverted, + CallNodeInfoInverted, } from './call-node-info'; import { computeThreadCPURatio } from './cpu'; import { @@ -73,8 +73,6 @@ import type { BalancedNativeAllocationsTable, IndexIntoFrameTable, PageList, - CallNodeInfo, - CallNodeInfoInverted, CallNodeTable, CallNodePath, CallNodeAndCategoryPath, @@ -99,8 +97,8 @@ import type { Bytes, ThreadWithReservedFunctions, TabID, - SuffixOrderIndex, } from 'firefox-profiler/types'; +import type { CallNodeInfo, SuffixOrderIndex } from './call-node-info'; /** * Various helpers for dealing with the profile as a data structure. @@ -122,10 +120,7 @@ export function getCallNodeInfo( funcTable, defaultCategory ); - return new CallNodeInfoNonInvertedImpl( - callNodeTable, - stackIndexToCallNodeIndex - ); + return new CallNodeInfoNonInverted(callNodeTable, stackIndexToCallNodeIndex); } type CallNodeTableAndStackMap = { @@ -447,7 +442,7 @@ export function getInvertedCallNodeInfo( defaultCategory: IndexIntoCategoryList, funcCount: number ): CallNodeInfoInverted { - return new CallNodeInfoInvertedImpl( + return new CallNodeInfoInverted( nonInvertedCallNodeTable, stackIndexToNonInvertedCallNodeIndex, defaultCategory, diff --git a/src/profile-logic/stack-timing.js b/src/profile-logic/stack-timing.js index c1aff9f5ce..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 diff --git a/src/profile-logic/transforms.js b/src/profile-logic/transforms.js index 34c2c59d42..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'; /** diff --git a/src/selectors/per-thread/stack-sample.js b/src/selectors/per-thread/stack-sample.js index 0799188b80..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, @@ -46,6 +45,7 @@ import type { 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'; diff --git a/src/test/store/__snapshots__/profile-view.test.js.snap b/src/test/store/__snapshots__/profile-view.test.js.snap index 896e2a3599..affb02513f 100644 --- a/src/test/store/__snapshots__/profile-view.test.js.snap +++ b/src/test/store/__snapshots__/profile-view.test.js.snap @@ -2130,7 +2130,7 @@ Object { `; exports[`snapshots of selectors/profile matches the last stored run of selectedThreadSelector.getCallNodeInfo 1`] = ` -CallNodeInfoNonInvertedImpl { +CallNodeInfoNonInverted { "_cache": Map {}, "_callNodeTable": Object { "category": Int32Array [ @@ -2251,7 +2251,7 @@ CallNodeInfoNonInvertedImpl { exports[`snapshots of selectors/profile matches the last stored run of selectedThreadSelector.getCallTree 1`] = ` CallTree { - "_callNodeInfo": CallNodeInfoNonInvertedImpl { + "_callNodeInfo": CallNodeInfoNonInverted { "_cache": Map {}, "_callNodeTable": Object { "category": Int32Array [ @@ -2440,7 +2440,7 @@ CallTree { 0, 0, ], - "_callNodeInfo": CallNodeInfoNonInvertedImpl { + "_callNodeInfo": CallNodeInfoNonInverted { "_cache": Map {}, "_callNodeTable": Object { "category": Int32Array [ 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 2fabf42afc..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, @@ -304,256 +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 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; - - // 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; - - // Returns whether the given node is a root node. - isRoot(callNodeIndex: IndexIntoCallNodeTable): boolean; - - // Returns the list of children of a node. - getChildren(callNodeIndex: IndexIntoCallNodeTable): IndexIntoCallNodeTable[]; - - // 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; -} - -// An index into SuffixOrderedCallNodes. -export type SuffixOrderIndex = number; - -/** - * A sub-interface of CallNodeInfo 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 - * 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. - */ -export interface CallNodeInfoInverted extends CallNodeInfo { - // 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; - - // 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; - - // 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; - - // 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( - callNodeIndex: IndexIntoCallNodeTable - ): [SuffixOrderIndex, SuffixOrderIndex]; -} - export type LineNumber = number; // Stores the line numbers which are hit by each stack, for one specific source From e1a726b8c6a1bd5685b5519955303a455f0df683 Mon Sep 17 00:00:00 2001 From: Markus Stange Date: Sun, 24 Nov 2024 19:11:53 -0500 Subject: [PATCH 20/31] Remove unused methods from CallNodeInfoNonInverted. --- src/profile-logic/call-node-info.js | 31 +---------------------------- 1 file changed, 1 insertion(+), 30 deletions(-) diff --git a/src/profile-logic/call-node-info.js b/src/profile-logic/call-node-info.js index ba0670647b..143a6f95c3 100644 --- a/src/profile-logic/call-node-info.js +++ b/src/profile-logic/call-node-info.js @@ -66,12 +66,6 @@ export interface CallNodeInfo { func: IndexIntoFuncTable ): IndexIntoCallNodeTable | null; - // Returns whether the given node is a root node. - isRoot(callNodeIndex: IndexIntoCallNodeTable): boolean; - - // Returns the list of children of a node. - getChildren(callNodeIndex: IndexIntoCallNodeTable): IndexIntoCallNodeTable[]; - // 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 @@ -260,30 +254,6 @@ export class CallNodeInfoNonInverted implements CallNodeInfo { return null; } - isRoot(callNodeIndex: IndexIntoCallNodeTable): boolean { - return this._callNodeTable.prefix[callNodeIndex] === -1; - } - - getChildren(callNodeIndex: IndexIntoCallNodeTable): IndexIntoCallNodeTable[] { - if ( - this._callNodeTable.subtreeRangeEnd[callNodeIndex] === - callNodeIndex + 1 - ) { - return []; - } - - const children = []; - const firstChild = callNodeIndex + 1; - for ( - let childCallNodeIndex = firstChild; - childCallNodeIndex !== -1; - childCallNodeIndex = this._callNodeTable.nextSibling[childCallNodeIndex] - ) { - children.push(childCallNodeIndex); - } - return children; - } - prefixForNode( callNodeIndex: IndexIntoCallNodeTable ): IndexIntoCallNodeTable | -1 { @@ -1022,6 +992,7 @@ export class CallNodeInfoInverted implements CallNodeInfo { return this._rootCount; } + // Returns whether the given node is a root node. isRoot(nodeHandle: InvertedCallNodeHandle): boolean { return nodeHandle < this._rootCount; } From 0cd70fae5a99e89c6751e74c05cacf368b71de7b Mon Sep 17 00:00:00 2001 From: Markus Stange Date: Sun, 24 Nov 2024 19:28:17 -0500 Subject: [PATCH 21/31] Add a test for focus-category on an inverted call tree. --- src/test/store/transforms.test.js | 138 ++++++++++++++++-------------- 1 file changed, 75 insertions(+), 63 deletions(-) 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]); }); }); }); From b8a472d7767ef790371c60da231842b11cd4d578 Mon Sep 17 00:00:00 2001 From: Markus Stange Date: Thu, 13 Feb 2025 15:24:22 -0500 Subject: [PATCH 22/31] Be more explicit about the expectation that, if we have traced timing in the flame graph, it'll be non-inverted traced timing. --- src/components/flame-graph/FlameGraph.js | 9 +++++++-- src/profile-logic/call-tree.js | 12 ++++++++++++ 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/src/components/flame-graph/FlameGraph.js b/src/components/flame-graph/FlameGraph.js index 6b860d5823..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, @@ -350,8 +352,11 @@ class FlameGraphImpl extends React.PureComponent { // (CallTreeTimingsNonInverted and CallTreeTimingsInverted are very // different, and the flame graph is only used with non-inverted timings.) const tracedTimingNonInverted = - tracedTiming !== null && tracedTiming.type === 'NON_INVERTED' - ? tracedTiming.timings + 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; diff --git a/src/profile-logic/call-tree.js b/src/profile-logic/call-tree.js index 69a2c75b84..e05103e5ed 100644 --- a/src/profile-logic/call-tree.js +++ b/src/profile-logic/call-tree.js @@ -65,6 +65,18 @@ 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 { const url = new URL('/favicon.ico', libname); From 3462954ba243daef9a2cabc04fedfd3c1b9bab75 Mon Sep 17 00:00:00 2001 From: Markus Stange Date: Thu, 13 Feb 2025 16:00:51 -0500 Subject: [PATCH 23/31] Expand comment about inverted call node indexes (mention that the order is somewhat arbitrary) as requested in julien's review --- src/profile-logic/call-node-info.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/profile-logic/call-node-info.js b/src/profile-logic/call-node-info.js index 143a6f95c3..2604559ae6 100644 --- a/src/profile-logic/call-node-info.js +++ b/src/profile-logic/call-node-info.js @@ -663,7 +663,9 @@ export type SuffixOrderIndex = number; * * Legend, continued: * - * inX: Inverted call node index X + * 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) * * ``` From 9ddbf1c5dbaccd137692c58045525cc67c62391b Mon Sep 17 00:00:00 2001 From: Markus Stange Date: Thu, 13 Feb 2025 16:14:10 -0500 Subject: [PATCH 24/31] Adjust CallNodeInfoInverted documentation based on julien's feedback. --- src/profile-logic/call-node-info.js | 161 +++++++++++++++++----------- 1 file changed, 98 insertions(+), 63 deletions(-) diff --git a/src/profile-logic/call-node-info.js b/src/profile-logic/call-node-info.js index 2604559ae6..ff9d251137 100644 --- a/src/profile-logic/call-node-info.js +++ b/src/profile-logic/call-node-info.js @@ -340,10 +340,19 @@ type InvertedNonRootCallNodeTable = {| suffixOrderIndexRangeEnd: SuffixOrderIndex[], // IndexIntoInvertedNonRootCallNodeTable -> SuffixOrderIndex // Non-null for non-root nodes whose children haven't been created yet. - // For a non-root node x of the inverted tree, let k = depth[x] its depth in the inverted tree, - // and deepNodes = deepNodes[x] be its non-null deep nodes. - // Then, for every index i in suffixOrderIndexRangeStart[x]..suffixOrderIndexRangeEnd[x], - // the k'th prefix node of suffixOrderedCallNodes[i] is stored at deepNodes[x][i - suffixOrderIndexRangeStart[x]]. + // 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 @@ -717,23 +726,15 @@ export type SuffixOrderIndex = number; * * # 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. * - * For any inverted tree node inX, _invertedNonRootCallNodeTable either contains - * none or all of inX's children. - * For any inverted non-root node inQ whose parent node is 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 - * - * To know what the children of a node in the inverted tree are, we need to look - * at the parents in the non-inverted tree. + * Example: * * ``` * Non-inverted tree: @@ -747,91 +748,125 @@ export type SuffixOrderIndex = number; * - [cn5] B = A -> A -> B = A -> A -> B [so4] * - [cn6] C = A -> C = A -> C [so5] * - * Inverted roots: + * 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) * ``` * - * First, let's create the children for in0, which is the root for func A. - * in0 has three "self nodes": cn0, cn4, and cn2. + * This inverted tree was built up as follows: * - * in0's func is A. - * cn0, cn4, and cn2 also have func A. Of course; that's what makes them in0's self funcs. + * 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, whose func is A. - * cn2's parent is cn1, whose func is B. + * 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, self nodes [cn4] - * - in4: func B, parent in0, self nodes [cn2] - * - * Now we're done! - * - * --- - * - * Now let's create the children of a non-root node in the inverted tree. - * We want to create the children for in4. - * in4 describes the call path suffix "... -> B -> A". + * - in3: func A, parent in0, depth 1, self nodes [cn4] + * - in4: func B, parent in0, depth 1, self nodes [cn2] * - * in4 has one self node: cn2. This is the only non-inverted node whose call path - * ends in "... -> B -> A". + * ### Why do we keep a "deepNodes" property in the inverted table? * - * in4 has depth 1. + * 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! * - * in4's func is B. - * cn2's func is A. (!) + * Let's create the children of the non-root node in4 (func: B, depth 1). + * in4 represents the call path suffix "... -> B -> A". * - * cn2's func still corresponds to the inverted root, i.e. in0's func. - * But cn2's parent, cn1, has func B. + * in4 has one self node: cn2 (func: A). cn2 is the only non-inverted node + * whose call path ends in "... -> B -> A". * - * And cn1's parent, cn0, has func 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: - * - in5: func A, parent in4, self nodes [cn2] + * - in6: func A, parent in4, depth 2, self nodes [cn2] * - * What this example shows is that we need to look not at a self node's immediate - * parent, but rather at its (k + 1)'th parent, where k is the depth of the - * inverted node whose children we're creating. + * 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. * * --- * - * What are the children of in5? + * Let's try to go one level deeper and create the children of in6 (func A, depth 2): * - * in5 has one self node: cn2. - * in5 has depth 2. + * in6 has one self node: cn2. + * in6 has depth 2, its children would have depth 3. * - * cn2's 0th parent is cn2. - * cn2's 1st parent is cn1. - * cn2's 2nd parent (i.e. its grandparent) is cn0. + * 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 in5 has no children. + * So in6 has no children. * * --- * - * Now let's say we want to create the children of an inverted node with depth 20, - * and it has 500 self nodes. We would need to look at each self node, find its - * 21st parent node, and then check that node's func. + * 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! * - * Climbing up the parent chain 20 steps, for each of the 500 self nodes, would - * be quite expensive. It would be better if we had stored the 20th parent for - * each of the self nodes, so that we would only need to go up to the immediate - * parent. + * 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 we do. 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. + * 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 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.) + * 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: * |-----------|-------------------------| @@ -852,7 +887,7 @@ export type SuffixOrderIndex = number; * | cn2 | cn1 | * |-----------|-------------------------| * - * in5 (depth 2): + * in6 (depth 2): * |-----------|-------------------------| * | self node | corresponding deep node | * |-----------|-------------------------| From 3c79aff99d4f870d422bd45144be47f6e63e87ba Mon Sep 17 00:00:00 2001 From: Markus Stange Date: Thu, 13 Feb 2025 17:43:46 -0500 Subject: [PATCH 25/31] Clarify that the CallNodePaths used with CallNodeInfoInverted are inverted call node paths. --- src/profile-logic/call-node-info.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/profile-logic/call-node-info.js b/src/profile-logic/call-node-info.js index ff9d251137..fe242e1254 100644 --- a/src/profile-logic/call-node-info.js +++ b/src/profile-logic/call-node-info.js @@ -1530,7 +1530,7 @@ export class CallNodeInfoInverted implements CallNodeInfo { return deepNodes; } - // This function returns a CallNodePath from a InvertedCallNodeHandle. + // This function returns an inverted CallNodePath from a InvertedCallNodeHandle. getCallNodePathFromIndex( callNodeHandle: InvertedCallNodeHandle | null ): CallNodePath { @@ -1552,7 +1552,7 @@ export class CallNodeInfoInverted implements CallNodeInfo { return callNodePath; } - // Returns a CallNodeIndex from a CallNodePath. + // Returns a CallNodeIndex from an inverted CallNodePath. getCallNodeIndexFromPath( callNodePath: CallNodePath ): InvertedCallNodeHandle | null { From e5f0b4cd27d1c8b0182fa1360e52f477c419264e Mon Sep 17 00:00:00 2001 From: Markus Stange Date: Thu, 13 Feb 2025 17:45:51 -0500 Subject: [PATCH 26/31] Rename _findDeepestKnownAncestor to _findDeepestExistingInvertedAncestorNode and improve its comments. --- src/profile-logic/call-node-info.js | 85 ++++++++++++++++++----------- 1 file changed, 52 insertions(+), 33 deletions(-) diff --git a/src/profile-logic/call-node-info.js b/src/profile-logic/call-node-info.js index fe242e1254..6b1eb5ca0e 100644 --- a/src/profile-logic/call-node-info.js +++ b/src/profile-logic/call-node-info.js @@ -1459,31 +1459,6 @@ export class CallNodeInfoInverted implements CallNodeInfo { return childNodeHandle; } - _findDeepestKnownAncestor(callPath: CallNodePath): InvertedCallNodeHandle { - const completePathNode = this._cache.get(hashPath(callPath)); - if (completePathNode !== undefined) { - return completePathNode; - } - - 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; - } - /** * Returns the array of child node handles for the given inverted call node. * The returned array of call node handles is sorted by func. @@ -1565,12 +1540,18 @@ export class CallNodeInfoInverted implements CallNodeInfo { } const pathDepth = callNodePath.length - 1; - let deepestKnownAncestor = this._findDeepestKnownAncestor(callNodePath); - let deepestKnownAncestorDepth = this.depthForNode(deepestKnownAncestor); - while (deepestKnownAncestorDepth < pathDepth) { - const currentChildFunc = callNodePath[deepestKnownAncestorDepth + 1]; - const children = this.getChildren(deepestKnownAncestor); + // Get the deepest ancestor already present in the inverted table. + let deepestExistingInvertedAncestorNode = + this._findDeepestExistingInvertedAncestorNode(callNodePath); + let deepestExistingInvertedAncestorNodeDepth = this.depthForNode( + deepestExistingInvertedAncestorNode + ); + + while (deepestExistingInvertedAncestorNodeDepth < pathDepth) { + const currentChildFunc = + callNodePath[deepestExistingInvertedAncestorNodeDepth + 1]; + const children = this.getChildren(deepestExistingInvertedAncestorNode); const childMatchingFunc = this._getChildWithFunc( children, currentChildFunc @@ -1581,10 +1562,48 @@ export class CallNodeInfoInverted implements CallNodeInfo { // we return null. return null; } - deepestKnownAncestor = childMatchingFunc; - deepestKnownAncestorDepth++; + deepestExistingInvertedAncestorNode = childMatchingFunc; + deepestExistingInvertedAncestorNodeDepth++; } - return deepestKnownAncestor; + 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; } // Returns the CallNodeIndex that matches the function `func` and whose parent's From 92529187ca27fd94248d7417e86013277da5f5da Mon Sep 17 00:00:00 2001 From: Markus Stange Date: Thu, 13 Feb 2025 18:00:33 -0500 Subject: [PATCH 27/31] Improve comments in getCallNodeIndexFromPath. --- src/profile-logic/call-node-info.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/profile-logic/call-node-info.js b/src/profile-logic/call-node-info.js index 6b1eb5ca0e..7b1a5946eb 100644 --- a/src/profile-logic/call-node-info.js +++ b/src/profile-logic/call-node-info.js @@ -1528,6 +1528,9 @@ export class CallNodeInfoInverted implements CallNodeInfo { } // 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 { @@ -1548,6 +1551,7 @@ export class CallNodeInfoInverted implements CallNodeInfo { deepestExistingInvertedAncestorNode ); + // Now create the necessary children until the end of the requested call node path. while (deepestExistingInvertedAncestorNodeDepth < pathDepth) { const currentChildFunc = callNodePath[deepestExistingInvertedAncestorNodeDepth + 1]; From 034244c23f2e74578f40abeb1b9b286b48428780 Mon Sep 17 00:00:00 2001 From: Markus Stange Date: Thu, 13 Feb 2025 18:03:14 -0500 Subject: [PATCH 28/31] Move _getChildWithFunc and improve its comments. --- src/profile-logic/call-node-info.js | 43 +++++++++++++++++------------ 1 file changed, 26 insertions(+), 17 deletions(-) diff --git a/src/profile-logic/call-node-info.js b/src/profile-logic/call-node-info.js index 7b1a5946eb..e8a9c681bd 100644 --- a/src/profile-logic/call-node-info.js +++ b/src/profile-logic/call-node-info.js @@ -1442,23 +1442,6 @@ export class CallNodeInfoInverted implements CallNodeInfo { return newHandle; } - _getChildWithFunc( - childrenSortedByFunc: InvertedCallNodeHandle[], - func: IndexIntoFuncTable - ): InvertedCallNodeHandle | null { - const index = bisectionRightByKey(childrenSortedByFunc, func, (node) => - this.funcForNode(node) - ); - if (index === 0) { - return null; - } - const childNodeHandle = childrenSortedByFunc[index - 1]; - if (this.funcForNode(childNodeHandle) !== func) { - return null; - } - return childNodeHandle; - } - /** * Returns the array of child node handles for the given inverted call node. * The returned array of call node handles is sorted by func. @@ -1610,6 +1593,32 @@ export class CallNodeInfoInverted implements CallNodeInfo { 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( From 0331c6b9365225cbaacb6b3b77ff036e90d689ed Mon Sep 17 00:00:00 2001 From: Markus Stange Date: Thu, 13 Feb 2025 18:08:07 -0500 Subject: [PATCH 29/31] Adjust the comment above _createNonRootNode as requested by julien. --- src/profile-logic/call-node-info.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/profile-logic/call-node-info.js b/src/profile-logic/call-node-info.js index e8a9c681bd..8f6b121df0 100644 --- a/src/profile-logic/call-node-info.js +++ b/src/profile-logic/call-node-info.js @@ -1365,7 +1365,7 @@ export class CallNodeInfoInverted implements CallNodeInfo { * 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], + * of suffixOrderedCallNodes[suffixOrderIndexRangeStart + i] in the non-inverted tree, * with k being the depth of the new inverted node. */ _createNonRootNode( From 3f4e927632acb06aab0147b7d68eb3fb6dd9e96c Mon Sep 17 00:00:00 2001 From: Markus Stange Date: Thu, 13 Feb 2025 18:10:16 -0500 Subject: [PATCH 30/31] Clarify the comment about ordering the inverted children by func. --- src/profile-logic/call-node-info.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/profile-logic/call-node-info.js b/src/profile-logic/call-node-info.js index 8f6b121df0..27e5c187a8 100644 --- a/src/profile-logic/call-node-info.js +++ b/src/profile-logic/call-node-info.js @@ -1201,8 +1201,8 @@ export class CallNodeInfoInverted implements CallNodeInfo { return null; } - // We create one child for each distinct func we found. The children need to - // be ordered by func. + // 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; From c80f34a5eef643ea541d9db5d6906a8b2f1e7cf2 Mon Sep 17 00:00:00 2001 From: Markus Stange Date: Thu, 13 Feb 2025 18:11:32 -0500 Subject: [PATCH 31/31] Fix parentDoopNode typo. --- src/profile-logic/call-node-info.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/profile-logic/call-node-info.js b/src/profile-logic/call-node-info.js index 27e5c187a8..f5203318fc 100644 --- a/src/profile-logic/call-node-info.js +++ b/src/profile-logic/call-node-info.js @@ -1128,7 +1128,7 @@ export class CallNodeInfoInverted implements CallNodeInfo { // 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 parentDoopNode, we'll encounter + // 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