From db716a591e0e33332fdb4b126fe24d8a426db359 Mon Sep 17 00:00:00 2001 From: Markus Stange Date: Tue, 23 Jan 2024 11:45:42 -0500 Subject: [PATCH 01/25] 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/25] 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/25] 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/25] 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/25] 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/25] 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/25] 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/25] 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/25] 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/25] 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/25] 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/25] 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/25] 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/25] 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/25] 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/25] 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/25] 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/25] 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/25] 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/25] 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/25] 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 638a8008c3748a807535849854af1a6f89023e0d Mon Sep 17 00:00:00 2001 From: Markus Stange Date: Wed, 27 Nov 2024 13:20:00 -0500 Subject: [PATCH 22/25] Implement a function list. --- locales/en-US/app.ftl | 1 + res/css/style.css | 3 +- src/actions/profile-view.js | 28 ++ src/app-logic/tabs-handling.js | 2 + src/app-logic/url-handling.js | 1 + src/components/app/BottomBox.js | 7 +- src/components/app/Details.js | 2 + src/components/calltree/CallTree.js | 100 +---- src/components/calltree/FunctionList.js | 250 ++++++++++++ .../calltree/ProfileFunctionListView.js | 23 ++ src/components/calltree/columns.js | 100 +++++ src/components/sidebar/index.js | 1 + src/profile-logic/call-tree.js | 375 +++++++++++++++++- src/profile-logic/profile-data.js | 157 ++++++++ src/reducers/profile-view.js | 74 ++++ src/selectors/per-thread/index.js | 106 +++++ src/selectors/per-thread/stack-sample.js | 72 +++- src/test/components/Details.test.js | 10 +- src/test/components/DetailsContainer.test.js | 1 + src/test/components/TooltipCallnode.test.js | 2 +- src/test/store/useful-tabs.test.js | 2 + src/test/unit/bitset.test.js | 64 +++ src/types/actions.js | 7 + src/types/profile-derived.js | 5 + src/types/state.js | 8 + src/utils/bitset.js | 32 ++ src/utils/flow.js | 1 + 27 files changed, 1331 insertions(+), 103 deletions(-) create mode 100644 src/components/calltree/FunctionList.js create mode 100644 src/components/calltree/ProfileFunctionListView.js create mode 100644 src/components/calltree/columns.js create mode 100644 src/test/unit/bitset.test.js create mode 100644 src/utils/bitset.js diff --git a/locales/en-US/app.ftl b/locales/en-US/app.ftl index be8a5ceaea..1ed6ae6156 100644 --- a/locales/en-US/app.ftl +++ b/locales/en-US/app.ftl @@ -818,6 +818,7 @@ StackSettings--panel-search = ## Tab Bar for the bottom half of the analysis UI. TabBar--calltree-tab = Call Tree +TabBar--function-list-tab = Function Table TabBar--flame-graph-tab = Flame Graph TabBar--stack-chart-tab = Stack Chart TabBar--marker-chart-tab = Marker Chart diff --git a/res/css/style.css b/res/css/style.css index c8bd550a77..b1f5a3aaa1 100644 --- a/res/css/style.css +++ b/res/css/style.css @@ -57,7 +57,8 @@ body { flex-shrink: 1; } -.treeAndSidebarWrapper { +.treeAndSidebarWrapper, +.functionTableAndSidebarWrapper { display: flex; flex: 1; flex-flow: column nowrap; diff --git a/src/actions/profile-view.js b/src/actions/profile-view.js index 5b0063a5de..e669854a3a 100644 --- a/src/actions/profile-view.js +++ b/src/actions/profile-view.js @@ -67,6 +67,7 @@ import type { CallNodePath, IndexIntoCallNodeTable, IndexIntoResourceTable, + IndexIntoFuncTable, TrackIndex, MarkerIndex, Transform, @@ -133,6 +134,22 @@ export function changeSelectedCallNode( }; } +/** + * Select a function for a given thread in the function list. + */ +export function changeSelectedFunctionIndex( + threadsKey: ThreadsKey, + selectedFunctionIndex: IndexIntoFuncTable | null, + context: SelectionContext = { source: 'auto' } +): Action { + return { + type: 'CHANGE_SELECTED_FUNCTION', + selectedFunctionIndex, + threadsKey, + context, + }; +} + /** * This action is used when the user right clicks on a call node (in panels such * as the call tree, the flame chart, or the stack chart). It's especially used @@ -149,6 +166,17 @@ export function changeRightClickedCallNode( }; } +export function changeRightClickedFunctionIndex( + threadsKey: ThreadsKey, + functionIndex: IndexIntoFuncTable | null +) { + return { + type: 'CHANGE_RIGHT_CLICKED_FUNCTION', + threadsKey, + functionIndex, + }; +} + /** * Given a threadIndex and a sampleIndex, select the call node which carries the * sample's self time. In the inverted tree, this will be a root node. diff --git a/src/app-logic/tabs-handling.js b/src/app-logic/tabs-handling.js index 28850d0312..41e9f60796 100644 --- a/src/app-logic/tabs-handling.js +++ b/src/app-logic/tabs-handling.js @@ -11,6 +11,7 @@ */ export const tabsWithTitleL10nId = { calltree: 'TabBar--calltree-tab', + 'function-list': 'TabBar--function-list-tab', 'flame-graph': 'TabBar--flame-graph-tab', 'stack-chart': 'TabBar--stack-chart-tab', 'marker-chart': 'TabBar--marker-chart-tab', @@ -43,6 +44,7 @@ export const tabsWithTitleL10nIdArray: $ReadOnlyArray = export const tabsShowingSampleData: $ReadOnlyArray = [ 'calltree', + 'function-list', 'flame-graph', 'stack-chart', ]; diff --git a/src/app-logic/url-handling.js b/src/app-logic/url-handling.js index 392660d9f3..dfb5bbc28b 100644 --- a/src/app-logic/url-handling.js +++ b/src/app-logic/url-handling.js @@ -386,6 +386,7 @@ export function getQueryStringFromUrlState(urlState: UrlState): string { switch (selectedTab) { case 'stack-chart': case 'flame-graph': + case 'function-list': case 'calltree': { if (selectedTab === 'stack-chart') { // Stack chart uses all of the CallTree's query strings but also has an diff --git a/src/components/app/BottomBox.js b/src/components/app/BottomBox.js index 348ecd048c..6aa911f6b5 100644 --- a/src/components/app/BottomBox.js +++ b/src/components/app/BottomBox.js @@ -18,10 +18,12 @@ import { getAssemblyViewIsOpen, getAssemblyViewNativeSymbol, getAssemblyViewScrollGeneration, + // getSelectedTab, } from 'firefox-profiler/selectors/url-state'; import { selectedThreadSelectors, selectedNodeSelectors, + // selectedFunctionTableNodeSelectors, } from 'firefox-profiler/selectors/per-thread'; import { closeBottomBox } from 'firefox-profiler/actions/profile-view'; import { parseFileNameFromSymbolication } from 'firefox-profiler/utils/special-paths'; @@ -284,7 +286,10 @@ export const BottomBox = explicitConnect<{||}, StateProps, DispatchProps>({ mapStateToProps: (state) => ({ sourceViewFile: getSourceViewFile(state), sourceViewCode: getSourceViewCode(state), - globalLineTimings: selectedThreadSelectors.getSourceViewLineTimings(state), + globalLineTimings: + // getSelectedTab(state) === 'function-list' + // ? selectedFunctionTableNodeSelectors.getSourceViewLineTimings(state) : + selectedNodeSelectors.getSourceViewLineTimings(state), selectedCallNodeLineTimings: selectedNodeSelectors.getSourceViewLineTimings(state), sourceViewScrollGeneration: getSourceViewScrollGeneration(state), diff --git a/src/components/app/Details.js b/src/components/app/Details.js index 193a194d02..a639673005 100644 --- a/src/components/app/Details.js +++ b/src/components/app/Details.js @@ -12,6 +12,7 @@ import explicitConnect from 'firefox-profiler/utils/connect'; import { TabBar } from './TabBar'; import { LocalizedErrorBoundary } from './ErrorBoundary'; import { ProfileCallTreeView } from 'firefox-profiler/components/calltree/ProfileCallTreeView'; +import { ProfileFunctionListView } from 'firefox-profiler/components/calltree/ProfileFunctionListView'; import { MarkerTable } from 'firefox-profiler/components/marker-table'; import { StackChart } from 'firefox-profiler/components/stack-chart/'; import { MarkerChart } from 'firefox-profiler/components/marker-chart/'; @@ -113,6 +114,7 @@ class ProfileViewerImpl extends PureComponent { { { calltree: , + 'function-list': , 'flame-graph': , 'stack-chart': , 'marker-chart': , diff --git a/src/components/calltree/CallTree.js b/src/components/calltree/CallTree.js index 65bbf352b4..4f5bbb47b3 100644 --- a/src/components/calltree/CallTree.js +++ b/src/components/calltree/CallTree.js @@ -8,10 +8,13 @@ import memoize from 'memoize-immutable'; import explicitConnect from 'firefox-profiler/utils/connect'; import { TreeView } from 'firefox-profiler/components/shared/TreeView'; import { CallTreeEmptyReasons } from './CallTreeEmptyReasons'; -import { Icon } from 'firefox-profiler/components/shared/Icon'; +import { + treeColumnsForTracingMs, + treeColumnsForSamples, + treeColumnsForBytes, +} from './columns'; import { getInvertCallstack, - getImplementationFilter, getSearchStringsAsRegExp, getSelectedThreadsKey, } from 'firefox-profiler/selectors/url-state'; @@ -36,7 +39,6 @@ import { assertExhaustiveCheck } from 'firefox-profiler/utils/flow'; import type { State, - ImplementationFilter, ThreadsKey, CategoryList, IndexIntoCallNodeTable, @@ -69,7 +71,6 @@ type StateProps = {| +searchStringsRegExp: RegExp | null, +disableOverscan: boolean, +invertCallstack: boolean, - +implementationFilter: ImplementationFilter, +callNodeMaxDepthPlusOne: number, +weightType: WeightType, +tableViewOptions: TableViewOptions, @@ -107,95 +108,11 @@ class CallTreeImpl extends PureComponent { (weightType: WeightType): MaybeResizableColumn[] => { switch (weightType) { case 'tracing-ms': - return [ - { - propName: 'totalPercent', - titleL10nId: '', - initialWidth: 50, - hideDividerAfter: true, - }, - { - propName: 'total', - titleL10nId: 'CallTree--tracing-ms-total', - minWidth: 30, - initialWidth: 70, - resizable: true, - headerWidthAdjustment: 50, - }, - { - propName: 'self', - titleL10nId: 'CallTree--tracing-ms-self', - minWidth: 30, - initialWidth: 70, - resizable: true, - }, - { - propName: 'icon', - titleL10nId: '', - component: Icon, - initialWidth: 10, - }, - ]; + return treeColumnsForTracingMs; case 'samples': - return [ - { - propName: 'totalPercent', - titleL10nId: '', - initialWidth: 50, - hideDividerAfter: true, - }, - { - propName: 'total', - titleL10nId: 'CallTree--samples-total', - minWidth: 30, - initialWidth: 70, - resizable: true, - headerWidthAdjustment: 50, - }, - { - propName: 'self', - titleL10nId: 'CallTree--samples-self', - minWidth: 30, - initialWidth: 70, - resizable: true, - }, - { - propName: 'icon', - titleL10nId: '', - component: Icon, - initialWidth: 10, - }, - ]; + return treeColumnsForSamples; case 'bytes': - return [ - { - propName: 'totalPercent', - titleL10nId: '', - initialWidth: 50, - hideDividerAfter: true, - }, - { - propName: 'total', - titleL10nId: 'CallTree--bytes-total', - minWidth: 30, - initialWidth: 140, - resizable: true, - headerWidthAdjustment: 50, - }, - { - propName: 'self', - titleL10nId: 'CallTree--bytes-self', - minWidth: 30, - initialWidth: 90, - resizable: true, - }, - { - propName: 'icon', - titleL10nId: '', - component: Icon, - initialWidth: 10, - }, - ]; + return treeColumnsForBytes; default: throw assertExhaustiveCheck(weightType, 'Unhandled WeightType.'); } @@ -413,7 +330,6 @@ export const CallTree = explicitConnect<{||}, StateProps, DispatchProps>({ searchStringsRegExp: getSearchStringsAsRegExp(state), disableOverscan: getPreviewSelection(state).isModifying, invertCallstack: getInvertCallstack(state), - implementationFilter: getImplementationFilter(state), // Use the filtered call node max depth, rather than the preview filtered call node // max depth so that the width of the TreeView component is stable across preview // selections. diff --git a/src/components/calltree/FunctionList.js b/src/components/calltree/FunctionList.js new file mode 100644 index 0000000000..425fa7781a --- /dev/null +++ b/src/components/calltree/FunctionList.js @@ -0,0 +1,250 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +// @flow + +import React, { PureComponent } from 'react'; +import memoize from 'memoize-immutable'; +import explicitConnect from 'firefox-profiler/utils/connect'; +import { TreeView } from 'firefox-profiler/components/shared/TreeView'; +import { CallTreeEmptyReasons } from './CallTreeEmptyReasons'; +import { + getSearchStringsAsRegExp, + getSelectedThreadsKey, +} from 'firefox-profiler/selectors/url-state'; +import { + getScrollToSelectionGeneration, + getFocusCallTreeGeneration, + getPreviewSelection, + getCurrentTableViewOptions, +} from 'firefox-profiler/selectors/profile'; +import { selectedThreadSelectors } from 'firefox-profiler/selectors/per-thread'; +import { + changeRightClickedFunctionIndex, + changeSelectedFunctionIndex, + addTransformToStack, + changeTableViewOptions, + updateBottomBoxContentsAndMaybeOpen, +} from 'firefox-profiler/actions/profile-view'; +import { assertExhaustiveCheck } from 'firefox-profiler/utils/flow'; +import { + treeColumnsForTracingMs, + treeColumnsForSamples, + treeColumnsForBytes, +} from './columns'; + +import type { + State, + ThreadsKey, + IndexIntoFuncTable, + CallNodeDisplayData, + WeightType, + TableViewOptions, + SelectionContext, +} from 'firefox-profiler/types'; +import type { FunctionListTree } from 'firefox-profiler/profile-logic/call-tree'; + +import type { + Column, + MaybeResizableColumn, +} from 'firefox-profiler/components/shared/TreeView'; +import type { ConnectedProps } from 'firefox-profiler/utils/connect'; + +import './CallTree.css'; + +type StateProps = {| + +threadsKey: ThreadsKey, + +scrollToSelectionGeneration: number, + +focusCallTreeGeneration: number, + +tree: FunctionListTree, + +selectedFunctionIndex: IndexIntoFuncTable | null, + +rightClickedFunctionIndex: IndexIntoFuncTable | null, + +searchStringsRegExp: RegExp | null, + +disableOverscan: boolean, + +weightType: WeightType, + +tableViewOptions: TableViewOptions, +|}; + +type DispatchProps = {| + +changeSelectedFunctionIndex: typeof changeSelectedFunctionIndex, + +changeRightClickedFunctionIndex: typeof changeRightClickedFunctionIndex, + +addTransformToStack: typeof addTransformToStack, + +updateBottomBoxContentsAndMaybeOpen: typeof updateBottomBoxContentsAndMaybeOpen, + +onTableViewOptionsChange: (TableViewOptions) => any, +|}; + +type Props = ConnectedProps<{||}, StateProps, DispatchProps>; + +class FunctionListImpl extends PureComponent { + _mainColumn: Column = { + propName: 'name', + titleL10nId: '', + }; + _appendageColumn: Column = { + propName: 'lib', + titleL10nId: '', + }; + _treeView: TreeView | null = null; + _takeTreeViewRef = (treeView) => (this._treeView = treeView); + + _expandedIndexes: Array = []; + + /** + * Call Trees can have different types of "weights" for the data. Choose the + * appropriate labels for the call tree based on this weight. + */ + _weightTypeToColumns = memoize( + (weightType: WeightType): MaybeResizableColumn[] => { + switch (weightType) { + case 'tracing-ms': + return treeColumnsForTracingMs; + case 'samples': + return treeColumnsForSamples; + case 'bytes': + return treeColumnsForBytes; + default: + throw assertExhaustiveCheck(weightType, 'Unhandled WeightType.'); + } + }, + // Use a Map cache, as the function only takes one argument, which is a simple string. + { cache: new Map() } + ); + + componentDidMount() { + this.focus(); + + if (this.props.selectedFunctionIndex !== null && this._treeView) { + this._treeView.scrollSelectionIntoView(); + } + } + + componentDidUpdate(prevProps) { + if ( + this.props.focusCallTreeGeneration > prevProps.focusCallTreeGeneration + ) { + this.focus(); + } + + if ( + this.props.selectedFunctionIndex !== null && + this.props.scrollToSelectionGeneration > + prevProps.scrollToSelectionGeneration && + this._treeView + ) { + this._treeView.scrollSelectionIntoView(); + } + } + + focus() { + if (this._treeView) { + this._treeView.focus(); + } + } + + _onSelectionChange = ( + newSelectedFunction: IndexIntoFuncTable, + context: SelectionContext + ) => { + const { threadsKey, changeSelectedFunctionIndex } = this.props; + changeSelectedFunctionIndex(threadsKey, newSelectedFunction, context); + }; + + _onRightClickSelection = (newSelectedFunction: IndexIntoFuncTable) => { + const { threadsKey, changeRightClickedFunctionIndex } = this.props; + changeRightClickedFunctionIndex(threadsKey, newSelectedFunction); + }; + + _onExpandedCallNodesChange = ( + _newExpandedCallNodeIndexes: Array + ) => {}; + + _onKeyDown = (_event: SyntheticKeyboardEvent<>) => { + // const { + // selectedFunctionIndex, + // rightClickedFunctionIndex, + // threadsKey, + // } = this.props; + // const nodeIndex = + // rightClickedFunctionIndex !== null + // ? rightClickedFunctionIndex + // : selectedFunctionIndex; + // if (nodeIndex === null) { + // return; + // } + // handleCallNodeTransformShortcut(event, threadsKey, nodeIndex); + }; + + _onEnterOrDoubleClick = (_nodeId: IndexIntoFuncTable) => { + // const { tree, updateBottomBoxContentsAndMaybeOpen } = this.props; + // const bottomBoxInfo = tree.getBottomBoxInfoForCallNode(nodeId); + // updateBottomBoxContentsAndMaybeOpen('calltree', bottomBoxInfo); + }; + + render() { + const { + tree, + selectedFunctionIndex, + rightClickedFunctionIndex, + searchStringsRegExp, + disableOverscan, + weightType, + tableViewOptions, + onTableViewOptionsChange, + } = this.props; + if (tree.getRoots().length === 0) { + return ; + } + return ( + + ); + } +} + +export const FunctionList = explicitConnect<{||}, StateProps, DispatchProps>({ + mapStateToProps: (state: State) => ({ + threadsKey: getSelectedThreadsKey(state), + scrollToSelectionGeneration: getScrollToSelectionGeneration(state), + focusCallTreeGeneration: getFocusCallTreeGeneration(state), + tree: selectedThreadSelectors.getFunctionListTree(state), + selectedFunctionIndex: + selectedThreadSelectors.getSelectedFunctionIndex(state), + rightClickedFunctionIndex: + selectedThreadSelectors.getRightClickedFunctionIndex(state), + searchStringsRegExp: getSearchStringsAsRegExp(state), + disableOverscan: getPreviewSelection(state).isModifying, + weightType: selectedThreadSelectors.getWeightTypeForCallTree(state), + tableViewOptions: getCurrentTableViewOptions(state), + }), + mapDispatchToProps: { + changeSelectedFunctionIndex, + changeRightClickedFunctionIndex, + addTransformToStack, + updateBottomBoxContentsAndMaybeOpen, + onTableViewOptionsChange: (options: TableViewOptions) => + changeTableViewOptions('function-list', options), + }, + component: FunctionListImpl, +}); diff --git a/src/components/calltree/ProfileFunctionListView.js b/src/components/calltree/ProfileFunctionListView.js new file mode 100644 index 0000000000..790de91a62 --- /dev/null +++ b/src/components/calltree/ProfileFunctionListView.js @@ -0,0 +1,23 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// @flow + +import React from 'react'; +import { FunctionList } from './FunctionList'; +import { StackSettings } from 'firefox-profiler/components/shared/StackSettings'; +import { TransformNavigator } from 'firefox-profiler/components/shared/TransformNavigator'; + +export const ProfileFunctionListView = () => ( +
+ + + +
+); diff --git a/src/components/calltree/columns.js b/src/components/calltree/columns.js new file mode 100644 index 0000000000..13828371a6 --- /dev/null +++ b/src/components/calltree/columns.js @@ -0,0 +1,100 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +// @flow + +import { Icon } from 'firefox-profiler/components/shared/Icon'; + +import type { MaybeResizableColumn } from 'firefox-profiler/components/shared/TreeView'; +import type { CallNodeDisplayData } from 'firefox-profiler/types'; + +export const treeColumnsForTracingMs: MaybeResizableColumn[] = + [ + { + propName: 'totalPercent', + titleL10nId: '', + initialWidth: 50, + hideDividerAfter: true, + }, + { + propName: 'total', + titleL10nId: 'CallTree--tracing-ms-total', + minWidth: 30, + initialWidth: 70, + resizable: true, + headerWidthAdjustment: 50, + }, + { + propName: 'self', + titleL10nId: 'CallTree--tracing-ms-self', + minWidth: 30, + initialWidth: 70, + resizable: true, + }, + { + propName: 'icon', + titleL10nId: '', + component: Icon, + initialWidth: 10, + }, + ]; + +export const treeColumnsForSamples = [ + { + propName: 'totalPercent', + titleL10nId: '', + initialWidth: 50, + hideDividerAfter: true, + }, + { + propName: 'total', + titleL10nId: 'CallTree--samples-total', + minWidth: 30, + initialWidth: 70, + resizable: true, + headerWidthAdjustment: 50, + }, + { + propName: 'self', + titleL10nId: 'CallTree--samples-self', + minWidth: 30, + initialWidth: 70, + resizable: true, + }, + { + propName: 'icon', + titleL10nId: '', + component: Icon, + initialWidth: 10, + }, +]; +export const treeColumnsForBytes: MaybeResizableColumn[] = + [ + { + propName: 'totalPercent', + titleL10nId: '', + initialWidth: 50, + hideDividerAfter: true, + }, + { + propName: 'total', + titleL10nId: 'CallTree--bytes-total', + minWidth: 30, + initialWidth: 140, + resizable: true, + headerWidthAdjustment: 50, + }, + { + propName: 'self', + titleL10nId: 'CallTree--bytes-self', + minWidth: 30, + initialWidth: 90, + resizable: true, + }, + { + propName: 'icon', + titleL10nId: '', + component: Icon, + initialWidth: 10, + }, + ]; diff --git a/src/components/sidebar/index.js b/src/components/sidebar/index.js index 7309373ce7..768b535797 100644 --- a/src/components/sidebar/index.js +++ b/src/components/sidebar/index.js @@ -17,6 +17,7 @@ export function selectSidebar( ): React.ComponentType<{||}> | null { return { calltree: CallTreeSidebar, + 'function-list': CallTreeSidebar, 'flame-graph': CallTreeSidebar, 'stack-chart': null, 'marker-chart': null, diff --git a/src/profile-logic/call-tree.js b/src/profile-logic/call-tree.js index 69a2c75b84..0d5fffcb69 100644 --- a/src/profile-logic/call-tree.js +++ b/src/profile-logic/call-tree.js @@ -19,6 +19,7 @@ import type { SamplesLikeTable, WeightType, CallNodeTable, + CallNodeTableBitSet, CallNodePath, IndexIntoCallNodeTable, CallNodeData, @@ -33,6 +34,7 @@ import type { import ExtensionIcon from '../../res/img/svg/extension.svg'; import { formatCallNodeNumber, formatPercent } from '../utils/format-numbers'; import { assertExhaustiveCheck, ensureExists } from '../utils/flow'; +import { checkBit } from '../utils/bitset'; import * as ProfileData from './profile-data'; import type { CallTreeSummaryStrategy } from '../types/actions'; import type { CallNodeInfo, CallNodeInfoInverted } from './call-node-info'; @@ -61,6 +63,13 @@ export type CallTreeTimingsInverted = {| hasChildrenPerRootFunc: Uint8Array, |}; +export type FunctionListTimings = {| + funcSelf: Float32Array, + funcTotal: Float32Array, + sortedFuncs: IndexIntoFuncTable[], + rootTotalSummary: number, +|}; + export type CallTreeTimings = | {| type: 'NON_INVERTED', timings: CallTreeTimingsNonInverted |} | {| type: 'INVERTED', timings: CallTreeTimingsInverted |}; @@ -594,6 +603,254 @@ export class CallTree { } } +export class FunctionListTree { + _categories: CategoryList; + _thread: Thread; + _callNodeInfoInverted: CallNodeInfoInverted; + _timings: FunctionListTimings; + _rootTotalSummary: number; + _displayDataByIndex: Map; + // _children is indexed by IndexIntoCallNodeTable. Since they are + // integers, using an array directly is faster than going through a Map. + _children: Array; + _roots: IndexIntoCallNodeTable[]; + _isHighPrecision: boolean; + _weightType: WeightType; + + constructor( + thread: Thread, + categories: CategoryList, + timings: FunctionListTimings, + callNodeInfoInverted: CallNodeInfoInverted, + isHighPrecision: boolean, + weightType: WeightType + ) { + this._categories = categories; + this._timings = timings; + this._callNodeInfoInverted = callNodeInfoInverted; + this._thread = thread; + this._rootTotalSummary = timings.rootTotalSummary; + this._displayDataByIndex = new Map(); + this._roots = timings.sortedFuncs; + this._isHighPrecision = isHighPrecision; + this._weightType = weightType; + } + + _getSelfAndTotal(funcIndex: IndexIntoFuncTable): SelfAndTotal { + return { + self: this._timings.funcSelf[funcIndex], + total: this._timings.funcTotal[funcIndex], + }; + } + + getRoots() { + return this._roots; + } + + getChildren(_funcIndex: IndexIntoFuncTable): CallNodeChildren { + return []; + } + + hasChildren(_funcIndex: IndexIntoFuncTable): boolean { + return false; + } + + _addDescendantsToSet( + _funcIndex: IndexIntoFuncTable, + _set: Set + ): void {} + + getAllDescendants( + _funcIndex: IndexIntoFuncTable + ): Set { + return new Set(); + } + + getParent(_funcIndex: IndexIntoFuncTable): IndexIntoCallNodeTable | -1 { + return -1; + } + + getDepth(_funcIndex: IndexIntoFuncTable): number { + return 0; + } + + getNodeData(funcIndex: IndexIntoFuncTable): CallNodeData { + const funcName = this._thread.stringTable.getString( + this._thread.funcTable.name[funcIndex] + ); + + const { self, total } = this._getSelfAndTotal(funcIndex); + const totalRelative = total / this._rootTotalSummary; + const selfRelative = self / this._rootTotalSummary; + + return { + funcName, + total, + totalRelative, + self, + selfRelative, + }; + } + + _getInliningBadge( + funcIndex: IndexIntoFuncTable, + funcName: string + ): ExtraBadgeInfo | void { + const calledFunction = getFunctionName(funcName); + const inlinedIntoNativeSymbol = + this._callNodeInfoInverted.sourceFramesInlinedIntoSymbolForNode( + funcIndex + ); + if (inlinedIntoNativeSymbol === -2) { + return undefined; + } + + if (inlinedIntoNativeSymbol === -1) { + return { + name: 'divergent-inlining', + vars: { calledFunction }, + localizationId: 'CallTree--divergent-inlining-badge', + contentFallback: '', + titleFallback: `Some calls to ${calledFunction} were inlined by the compiler.`, + }; + } + + const outerFunction = getFunctionName( + this._thread.stringTable.getString( + this._thread.nativeSymbols.name[inlinedIntoNativeSymbol] + ) + ); + return { + name: 'inlined', + vars: { calledFunction, outerFunction }, + localizationId: 'CallTree--inlining-badge', + contentFallback: '(inlined)', + titleFallback: `Calls to ${calledFunction} were inlined into ${outerFunction} by the compiler.`, + }; + } + + getDisplayData(funcIndex: IndexIntoFuncTable): CallNodeDisplayData { + let displayData: CallNodeDisplayData | void = + this._displayDataByIndex.get(funcIndex); + if (displayData === undefined) { + const { funcName, total, totalRelative, self } = + this.getNodeData(funcIndex); + const categoryIndex = + this._callNodeInfoInverted.categoryForNode(funcIndex); + const subcategoryIndex = + this._callNodeInfoInverted.subcategoryForNode(funcIndex); + const badge = this._getInliningBadge(funcIndex, funcName); + const resourceIndex = this._thread.funcTable.resource[funcIndex]; + const resourceType = this._thread.resourceTable.type[resourceIndex]; + const isFrameLabel = resourceIndex === -1; + const libName = this._getOriginAnnotation(funcIndex); + const weightType = this._weightType; + + let iconSrc = null; + let icon = null; + + if (resourceType === resourceTypes.webhost) { + icon = iconSrc = extractFaviconFromLibname(libName); + } else if (resourceType === resourceTypes.addon) { + iconSrc = ExtensionIcon; + + const resourceNameIndex = + this._thread.resourceTable.name[resourceIndex]; + const iconText = this._thread.stringTable.getString(resourceNameIndex); + icon = iconText; + } + + const formattedTotal = formatCallNodeNumber( + weightType, + this._isHighPrecision, + total + ); + const formattedSelf = formatCallNodeNumber( + weightType, + this._isHighPrecision, + self + ); + const totalPercent = `${formatPercent(totalRelative)}`; + + let ariaLabel; + let totalWithUnit; + let selfWithUnit; + switch (weightType) { + case 'tracing-ms': { + totalWithUnit = `${formattedTotal}ms`; + selfWithUnit = `${formattedSelf}ms`; + ariaLabel = oneLine` + ${funcName}, + running time is ${totalWithUnit} (${totalPercent}), + self time is ${selfWithUnit} + `; + break; + } + case 'samples': { + // TODO - L10N pluralization + totalWithUnit = + total === 1 + ? `${formattedTotal} sample` + : `${formattedTotal} samples`; + selfWithUnit = + self === 1 ? `${formattedSelf} sample` : `${formattedSelf} samples`; + ariaLabel = oneLine` + ${funcName}, + running count is ${totalWithUnit} (${totalPercent}), + self count is ${selfWithUnit} + `; + break; + } + case 'bytes': { + totalWithUnit = `${formattedTotal} bytes`; + selfWithUnit = `${formattedSelf} bytes`; + ariaLabel = oneLine` + ${funcName}, + total size is ${totalWithUnit} (${totalPercent}), + self size is ${selfWithUnit} + `; + break; + } + default: + throw assertExhaustiveCheck(weightType, 'Unhandled WeightType.'); + } + + displayData = { + total: total === 0 ? '—' : formattedTotal, + totalWithUnit: total === 0 ? '—' : totalWithUnit, + self: self === 0 ? '—' : formattedSelf, + selfWithUnit: self === 0 ? '—' : selfWithUnit, + totalPercent, + name: funcName, + lib: libName.slice(0, 1000), + // Dim platform pseudo-stacks. + isFrameLabel, + badge, + categoryName: getCategoryPairLabel( + this._categories, + categoryIndex, + subcategoryIndex + ), + categoryColor: this._categories[categoryIndex].color, + iconSrc, + icon, + ariaLabel, + }; + this._displayDataByIndex.set(funcIndex, displayData); + } + return displayData; + } + + _getOriginAnnotation(funcIndex: IndexIntoFuncTable): string { + return getOriginAnnotationForFunc( + funcIndex, + this._thread.funcTable, + this._thread.resourceTable, + this._thread.stringTable + ); + } +} + /** * Compute the self time for each call node, and the sum of the absolute self * values. @@ -750,9 +1007,100 @@ export function computeCallTreeTimingsInverted( }; } +function computeFuncSelf( + callNodeTable: CallNodeTable, + callNodeSelf: Float32Array, + funcCount: number +): Float32Array { + const callNodeTableFuncCol = callNodeTable.func; + const callNodeCount = callNodeSelf.length; + + const funcSelf = new Float32Array(funcCount); + for (let i = 0; i < callNodeCount; i++) { + const self = callNodeSelf[i]; + if (self !== 0) { + const func = callNodeTableFuncCol[i]; + funcSelf[func] += self; + } + } + + return funcSelf; +} + +export function computeFunctionListTimings( + callNodeInfo: CallNodeInfo, + callNodeFuncIsDuplicate: CallNodeTableBitSet, + { callNodeSelf, rootTotalSummary }: CallNodeSelfAndSummary, + funcCount: number +): FunctionListTimings { + const callNodeTable = callNodeInfo.getNonInvertedCallNodeTable(); + const funcSelf = computeFuncSelf(callNodeTable, callNodeSelf, funcCount); + + const callNodeTableFuncCol = callNodeTable.func; + const callNodeTablePrefixCol = callNodeTable.prefix; + const callNodeCount = callNodeTable.length; + + const callNodeChildrenTotal = new Float32Array(callNodeCount); + const funcTotal = new Float32Array(funcCount); + + const seenPerFunc = new Uint8Array(funcCount); + const sortedFuncs = []; + + // We loop the call node table in reverse, so that we find the children + // before their parents, and the total is known at the time we reach a + // node. + for ( + let callNodeIndex = callNodeCount - 1; + callNodeIndex >= 0; + callNodeIndex-- + ) { + const self = callNodeSelf[callNodeIndex]; + const childrenTotal = callNodeChildrenTotal[callNodeIndex]; + const total = self + childrenTotal; + + if (total === 0) { + continue; + } + + const prefix = callNodeTablePrefixCol[callNodeIndex]; + if (prefix !== -1) { + callNodeChildrenTotal[prefix] += total; + + if (checkBit(callNodeFuncIsDuplicate, callNodeIndex)) { + // Will be picked up by ancestor. + continue; + } + } + + const func = callNodeTableFuncCol[callNodeIndex]; + funcTotal[func] += total; + + if (seenPerFunc[func] === 0) { + seenPerFunc[func] = 1; + sortedFuncs.push(func); + } + } + + const abs = Math.abs; + sortedFuncs.sort((a, b) => { + const absDiff = abs(funcTotal[b]) - abs(funcTotal[a]); + if (absDiff !== 0) { + return absDiff; + } + return a - b; + }); + + return { + funcSelf, + funcTotal, + sortedFuncs, + rootTotalSummary, + }; +} + export function computeCallTreeTimings( callNodeInfo: CallNodeInfo, - CallNodeSelfAndSummary: CallNodeSelfAndSummary + callNodeSelfAndSummary: CallNodeSelfAndSummary ): CallTreeTimings { const callNodeInfoInverted = callNodeInfo.asInverted(); if (callNodeInfoInverted !== null) { @@ -760,7 +1108,7 @@ export function computeCallTreeTimings( type: 'INVERTED', timings: computeCallTreeTimingsInverted( callNodeInfoInverted, - CallNodeSelfAndSummary + callNodeSelfAndSummary ), }; } @@ -768,7 +1116,7 @@ export function computeCallTreeTimings( type: 'NON_INVERTED', timings: computeCallTreeTimingsNonInverted( callNodeInfo, - CallNodeSelfAndSummary + callNodeSelfAndSummary ), }; } @@ -779,10 +1127,10 @@ export function computeCallTreeTimings( */ export function computeCallTreeTimingsNonInverted( callNodeInfo: CallNodeInfo, - CallNodeSelfAndSummary: CallNodeSelfAndSummary + callNodeSelfAndSummary: CallNodeSelfAndSummary ): CallTreeTimingsNonInverted { const callNodeTable = callNodeInfo.getNonInvertedCallNodeTable(); - const { callNodeSelf, rootTotalSummary } = CallNodeSelfAndSummary; + const { callNodeSelf, rootTotalSummary } = callNodeSelfAndSummary; // Compute the following variables: const callNodeTotalSummary = new Float32Array(callNodeTable.length); @@ -868,6 +1216,23 @@ export function getCallTree( }); } +export function getFunctionListTree( + thread: Thread, + callNodeInfoInverted: CallNodeInfoInverted, + categories: CategoryList, + timings: FunctionListTimings, + weightType: WeightType +): FunctionListTree { + return new FunctionListTree( + thread, + categories, + timings, + callNodeInfoInverted, + Boolean(thread.isJsTracer), + weightType + ); +} + /** * Returns a table with the appropriate data for the call tree summary strategy, * for use by the call tree or flame graph. diff --git a/src/profile-logic/profile-data.js b/src/profile-logic/profile-data.js index 2dbe0e794e..31c1614aa4 100644 --- a/src/profile-logic/profile-data.js +++ b/src/profile-logic/profile-data.js @@ -27,6 +27,7 @@ import { } from 'firefox-profiler/app-logic/constants'; import { timeCode } from 'firefox-profiler/utils/time-code'; import { bisectionRight, bisectionLeft } from 'firefox-profiler/utils/bisect'; +import { makeBitSet } from 'firefox-profiler/utils/bitset'; import { parseFileNameFromSymbolication } from 'firefox-profiler/utils/special-paths'; import { StringTable } from 'firefox-profiler/utils/string-table'; import { @@ -62,6 +63,7 @@ import type { IndexIntoStackTable, IndexIntoResourceTable, IndexIntoNativeSymbolTable, + CallNodeTableBitSet, ThreadIndex, Category, RawCounter, @@ -801,6 +803,74 @@ export function getSamplesSelectedStates( ); } +/** + * Go through the samples, and determine their current state. + * + * For samples that are neither 'FILTERED_OUT_*' nor 'SELECTED', + * this function uses 'UNSELECTED_ORDERED_AFTER_SELECTED'. It uses the same + * ordering as the function compareCallNodes in getTreeOrderComparator. + */ +export function getSamplesSelectedStatesForFunction( + sampleCallNodes: Array, + activeTabFilteredCallNodes: Array, + selectedFunctionIndex: IndexIntoFuncTable | null, + callNodeTable: CallNodeTable +): SelectedState[] { + if (selectedFunctionIndex === null) { + return _getSamplesSelectedStatesForNoSelection( + sampleCallNodes, + activeTabFilteredCallNodes + ); + } + + const sampleCount = sampleCallNodes.length; + + // Go through each call node, and label it as containing the function or not. + // callNodeContainsFunc is a callNodeIndex => bool map, implemented as a U8 typed + // array for better performance. 0 means false, 1 means true. + const callNodeCount = callNodeTable.length; + const callNodeContainsFunc = new Uint8Array(callNodeCount); + for (let callNodeIndex = 0; callNodeIndex < callNodeCount; callNodeIndex++) { + const prefix = callNodeTable.prefix[callNodeIndex]; + const funcIndex = callNodeTable.func[callNodeIndex]; + if ( + funcIndex === selectedFunctionIndex || + // The parent of this stack contained the function. + (prefix !== -1 && callNodeContainsFunc[prefix] === 1) + ) { + callNodeContainsFunc[callNodeIndex] = 1; + } + } + + // Go through each sample, and label its state. + const samplesSelectedStates = new Array(sampleCount); + for ( + let sampleIndex = 0; + sampleIndex < sampleCallNodes.length; + sampleIndex++ + ) { + let sampleSelectedState: SelectedState = 'SELECTED'; + const callNodeIndex = sampleCallNodes[sampleIndex]; + if (callNodeIndex !== null) { + if (callNodeContainsFunc[callNodeIndex] === 1) { + sampleSelectedState = 'SELECTED'; + } else { + sampleSelectedState = 'UNSELECTED_ORDERED_AFTER_SELECTED'; + } + } else { + // This sample was filtered out. + sampleSelectedState = + activeTabFilteredCallNodes[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; +} + /** * This function returns the function index for a specific call node path. This * is the last element of this path, or the leaf element of the path. @@ -1153,6 +1223,93 @@ export function getTimingsForCallNodeIndex( return { forPath: pathTimings, rootTime }; } +export function computeCallNodeFuncIsDuplicate( + callNodeTable: CallNodeTable +): CallNodeTableBitSet { + const callNodeCount = callNodeTable.length; + const maxDepth = callNodeTable.maxDepth; + const depthCol = callNodeTable.depth; + const funcCol = callNodeTable.func; + + const nodeFuncIsDuplicateBitSet = makeBitSet(callNodeCount); + + const depthToDedupDepth = new Int32Array(maxDepth + 1); + const funcsOnStack = new Int32Array(maxDepth + 1); + outer: for ( + let callNodeIndex = 0; + callNodeIndex < callNodeCount; + callNodeIndex++ + ) { + const depth = depthCol[callNodeIndex]; + const func = funcCol[callNodeIndex]; + + if (depth === 0) { + funcsOnStack[0] = func; + continue; + } + + // Check if `func` is already on the stack. + const prefixDepth = depth - 1; + const dedupPrefixDepth = depthToDedupDepth[prefixDepth]; + for (let ancDepth = dedupPrefixDepth; ancDepth >= 0; ancDepth--) { + if (funcsOnStack[ancDepth] === func) { + depthToDedupDepth[depth] = dedupPrefixDepth; + + // Mark this call node as having a duplicate func. + // Equivalent to setBit(nodeFuncIsDuplicateBitSet, callNodeIndex); + // Inlined manually for a 1.55x perf improvement in Firefox. + const q = callNodeIndex >> 5; + const r = callNodeIndex & 0b11111; + nodeFuncIsDuplicateBitSet[q] |= 1 << r; + continue outer; + } + } + + const dedupDepth = dedupPrefixDepth + 1; + funcsOnStack[dedupDepth] = func; + depthToDedupDepth[depth] = dedupDepth; + } + + return nodeFuncIsDuplicateBitSet; +} + +/** + * This function returns the timings for a specific function. + * + * Note that the unfilteredThread should be the original thread before any filtering + * (by range or other) happens. Also sampleIndexOffset needs to be properly + * specified and is the offset to be applied on thread's indexes to access + * the same samples in unfilteredThread. + */ +export function getTimingsForFunction( + _funcIndex: IndexIntoFuncTable | null, + _interval: Milliseconds, + _thread: Thread, + _unfilteredThread: Thread, + _sampleIndexOffset: number, + _categories: CategoryList, + _samples: SamplesLikeTable, + _unfilteredSamples: SamplesLikeTable, + _displayImplementation: boolean +): TimingsForPath { + // TODO + return { + forPath: { + selfTime: { + value: 0, + breakdownByImplementation: null, + breakdownByCategory: null, + }, + totalTime: { + value: 0, + breakdownByImplementation: null, + breakdownByCategory: null, + }, + }, + rootTime: 1, + }; +} + // This function computes the time range for a thread, using both its samples // and markers data. It's memoized and exported below, because it's called both // here in getTimeRangeIncludingAllThreads, and in selectors when dealing with diff --git a/src/reducers/profile-view.js b/src/reducers/profile-view.js index 46116902b4..a386a87b62 100644 --- a/src/reducers/profile-view.js +++ b/src/reducers/profile-view.js @@ -26,6 +26,7 @@ import type { ThreadViewOptionsPerThreads, TableViewOptionsPerTab, RightClickedCallNode, + RightClickedFunction, MarkerReference, ActiveTabTimeline, CallNodePath, @@ -157,6 +158,7 @@ export const defaultThreadViewOptions: ThreadViewOptions = { selectedInvertedCallNodePath: [], expandedNonInvertedCallNodePaths: new PathSet(), expandedInvertedCallNodePaths: new PathSet(), + selectedFunctionIndex: null, selectedMarker: null, selectedNetworkMarker: null, }; @@ -302,6 +304,23 @@ const viewOptionsPerThread: Reducer = ( } ); } + case 'CHANGE_SELECTED_FUNCTION': { + const { selectedFunctionIndex, threadsKey } = action; + + const threadState = _getThreadViewOptions(state, threadsKey); + + const previousSelectedFunction = threadState.selectedFunctionIndex; + + // If the selected function doesn't actually change, let's return the previous + // state to avoid rerenders. + if (selectedFunctionIndex === previousSelectedFunction) { + return state; + } + + return _updateThreadViewOptions(state, threadsKey, { + selectedFunctionIndex, + }); + } case 'CHANGE_INVERT_CALLSTACK': { const { newSelectedCallNodePath, @@ -592,6 +611,7 @@ const scrollToSelectionGeneration: Reducer = (state = 0, action) => { case 'CHANGE_NETWORK_SEARCH_STRING': return state + 1; case 'CHANGE_SELECTED_CALL_NODE': + case 'CHANGE_SELECTED_FUNCTION': case 'CHANGE_SELECTED_MARKER': case 'CHANGE_SELECTED_NETWORK_MARKER': if (action.context.source === 'pointer') { @@ -744,6 +764,59 @@ const rightClickedCallNode: Reducer = ( } }; +const rightClickedFunction: Reducer = ( + state = null, + action +) => { + switch (action.type) { + case 'BULK_SYMBOLICATION': { + if (state === null) { + return null; + } + + const { oldFuncToNewFuncsMaps } = action; + // This doesn't support a ThreadsKey with multiple threads. + const oldFuncToNewFuncsMap = oldFuncToNewFuncsMaps.get(+state.threadsKey); + if (oldFuncToNewFuncsMap === undefined) { + return state; + } + const functionIndexes = oldFuncToNewFuncsMap.get(state.functionIndex); + if (functionIndexes === undefined || functionIndexes.length === 0) { + return null; + } + + return { + ...state, + functionIndex: functionIndexes[0], + }; + } + case 'CHANGE_RIGHT_CLICKED_FUNCTION': + if (action.functionIndex !== null) { + return { + threadsKey: action.threadsKey, + functionIndex: action.functionIndex, + }; + } + + return null; + case 'SET_CONTEXT_MENU_VISIBILITY': + // We want to change the state only when the menu is hidden. + if (action.isVisible) { + return state; + } + + return null; + case 'PROFILE_LOADED': + case 'CHANGE_INVERT_CALLSTACK': + case 'ADD_TRANSFORM_TO_STACK': + case 'POP_TRANSFORMS_FROM_STACK': + case 'CHANGE_IMPLEMENTATION_FILTER': + return null; + default: + return state; + } +}; + const rightClickedMarker: Reducer = ( state = null, action @@ -857,6 +930,7 @@ const profileViewReducer: Reducer = wrapReducerInResetter( lastNonShiftClick, rightClickedTrack, rightClickedCallNode, + rightClickedFunction, rightClickedMarker, hoveredMarker, mouseTimePosition, diff --git a/src/selectors/per-thread/index.js b/src/selectors/per-thread/index.js index 9620bb7dde..2ec5791ffd 100644 --- a/src/selectors/per-thread/index.js +++ b/src/selectors/per-thread/index.js @@ -353,3 +353,109 @@ export const selectedNodeSelectors: NodeSelectors = (() => { getAssemblyViewAddressTimings, }; })(); + +// export type FunctionSelectors = {| +// +getTimingsForSidebar: Selector, +// +getSourceViewStackLineInfo: Selector, +// +getSourceViewLineTimings: Selector, +// |}; + +// export const selectedFunctionTableNodeSelectors: FunctionSelectors = (() => { +// // const getName: Selector = createSelector( +// // selectedThreadSelectors.getSelectedFunctionTableFunction, +// // selectedThreadSelectors.getFilteredThread, +// // (selectedFunction, { stringTable, funcTable }) => { +// // if (selectedFunction === null) { +// // return ''; +// // } + +// // return stringTable.getString(funcTable.name[selectedFunction]); +// // } +// // ); + +// // const getIsJS: Selector = createSelector( +// // selectedThreadSelectors.getSelectedFunctionTableFunction, +// // selectedThreadSelectors.getFilteredThread, +// // (selectedFunction, { funcTable }) => { +// // return selectedFunction !== null && funcTable.isJS[selectedFunction]; +// // } +// // ); + +// // const getLib: Selector = createSelector( +// // selectedThreadSelectors.getSelectedFunctionTableFunction, +// // selectedThreadSelectors.getFilteredThread, +// // (selectedFunction, { stringTable, funcTable, resourceTable }) => { +// // if (selectedFunction === null) { +// // return ''; +// // } + +// // return ProfileData.getOriginAnnotationForFunc( +// // selectedFunction, +// // funcTable, +// // resourceTable, +// // stringTable +// // ); +// // } +// // ); + +// const getTimingsForSidebar: Selector = createSelector( +// selectedThreadSelectors.getSelectedFunctionIndex, +// ProfileSelectors.getProfileInterval, +// selectedThreadSelectors.getPreviewFilteredThread, +// selectedThreadSelectors.getThread, +// selectedThreadSelectors.getSampleIndexOffsetFromPreviewRange, +// ProfileSelectors.getCategories, +// selectedThreadSelectors.getPreviewFilteredSamplesForCallTree, +// selectedThreadSelectors.getUnfilteredSamplesForCallTree, +// ProfileSelectors.getProfileUsesFrameImplementation, +// ProfileData.getTimingsForFunction +// ); + +// const getSourceViewStackLineInfo: Selector = +// createSelector( +// selectedThreadSelectors.getFilteredThread, +// UrlState.getSourceViewFile, +// selectedThreadSelectors.getCallNodeInfo, +// selectedThreadSelectors.getSelectedCallNodeIndex, +// UrlState.getInvertCallstack, +// ( +// { stackTable, frameTable, funcTable, stringTable }: Thread, +// sourceViewFile, +// callNodeInfo, +// selectedCallNodeIndex, +// invertCallStack +// ): StackLineInfo | null => { +// if (sourceViewFile === null || selectedCallNodeIndex === null) { +// return null; +// } +// const selectedFunc = +// callNodeInfo.callNodeTable.func[selectedCallNodeIndex]; +// const selectedFuncFile = funcTable.fileName[selectedFunc]; +// if ( +// selectedFuncFile === null || +// stringTable.getString(selectedFuncFile) !== sourceViewFile +// ) { +// return null; +// } +// return getStackLineInfoForCallNode( +// stackTable, +// frameTable, +// selectedCallNodeIndex, +// callNodeInfo, +// invertCallStack +// ); +// } +// ); + +// const getSourceViewLineTimings: Selector = createSelector( +// getSourceViewStackLineInfo, +// selectedThreadSelectors.getPreviewFilteredSamplesForCallTree, +// getLineTimings +// ); + +// return { +// // getTimingsForSidebar, +// // getSourceViewStackLineInfo, +// // getSourceViewLineTimings, +// }; +// })(); diff --git a/src/selectors/per-thread/stack-sample.js b/src/selectors/per-thread/stack-sample.js index c48cb0ccc9..70b1b3adfc 100644 --- a/src/selectors/per-thread/stack-sample.js +++ b/src/selectors/per-thread/stack-sample.js @@ -35,6 +35,7 @@ import type { StackAddressInfo, LineTimings, AddressTimings, + IndexIntoFuncTable, IndexIntoCallNodeTable, IndexIntoNativeSymbolTable, SelectedState, @@ -44,8 +45,12 @@ import type { ThreadsKey, SelfAndTotal, CallNodeSelfAndSummary, + CallNodeTableBitSet, } from 'firefox-profiler/types'; -import type { CallNodeInfo } from 'firefox-profiler/profile-logic/call-node-info'; +import type { + CallNodeInfo, + CallNodeInfoInverted, +} from 'firefox-profiler/profile-logic/call-node-info'; import type { ThreadSelectorsPerThread } from './thread'; import type { MarkerSelectorsPerThread } from './markers'; @@ -114,7 +119,7 @@ export function getStackAndSampleSelectorsPerThread( ProfileData.getCallNodeInfo ); - const _getInvertedCallNodeInfo: Selector = + const _getInvertedCallNodeInfo: Selector = createSelectorWithTwoCacheSlots( _getNonInvertedCallNodeInfo, ProfileSelectors.getDefaultCategory, @@ -136,6 +141,15 @@ export function getStackAndSampleSelectorsPerThread( return _getNonInvertedCallNodeInfo(state); }; + const _getCallNodeFuncIsDuplicate: Selector = + createSelector( + getCallNodeInfo, + (callNodeInfo) => { + const callNodeTable = callNodeInfo.getNonInvertedCallNodeTable(); + return ProfileData.computeCallNodeFuncIsDuplicate(callNodeTable); + } + ); + const getSourceViewStackLineInfo: Selector = createSelector( threadSelectors.getFilteredThread, @@ -202,6 +216,14 @@ export function getStackAndSampleSelectorsPerThread( : threadViewOptions.selectedNonInvertedCallNodePath ); + const getSelectedFunctionIndex: Selector = + createSelector( + threadSelectors.getViewOptions, + (threadViewOptions): IndexIntoFuncTable | null => { + return threadViewOptions.selectedFunctionIndex; + } + ); + const getSelectedCallNodeIndex: Selector = createSelector( getCallNodeInfo, @@ -338,6 +360,22 @@ export function getStackAndSampleSelectorsPerThread( CallTree.computeCallTreeTimingsNonInverted ); + // const _getCallTreeTimingsInverted: Selector = + // createSelector( + // _getInvertedCallNodeInfo, + // getCallNodeSelfAndSummary, + // CallTree.computeCallTreeTimingsInverted + // ); + + const getFunctionListTimings: Selector = + createSelector( + getCallNodeInfo, + _getCallNodeFuncIsDuplicate, + getCallNodeSelfAndSummary, + (state) => threadSelectors.getFilteredThread(state).funcTable.length, + CallTree.computeFunctionListTimings + ); + const getCallTree: Selector = createSelector( threadSelectors.getFilteredThread, getCallNodeInfo, @@ -347,6 +385,16 @@ export function getStackAndSampleSelectorsPerThread( CallTree.getCallTree ); + const getFunctionListTree: Selector = + createSelector( + threadSelectors.getPreviewFilteredThread, + _getInvertedCallNodeInfo, + ProfileSelectors.getCategories, + getFunctionListTimings, + getWeightTypeForCallTree, + CallTree.getFunctionListTree + ); + const getSourceViewLineTimings: Selector = createSelector( getSourceViewStackLineInfo, threadSelectors.getPreviewFilteredCtssSamples, @@ -444,6 +492,23 @@ export function getStackAndSampleSelectorsPerThread( } ); + const getRightClickedFunctionIndex: Selector = + createSelector( + ProfileSelectors.getProfileViewOptions, + (profileViewOptions) => { + const rightClickedFunctionInfo = + profileViewOptions.rightClickedFunction; + if ( + rightClickedFunctionInfo !== null && + threadsKey === rightClickedFunctionInfo.threadsKey + ) { + return rightClickedFunctionInfo.functionIndex; + } + + return null; + } + ); + return { unfilteredSamplesRange, getWeightTypeForCallTree, @@ -453,12 +518,14 @@ export function getStackAndSampleSelectorsPerThread( getAssemblyViewStackAddressInfo, getSelectedCallNodePath, getSelectedCallNodeIndex, + getSelectedFunctionIndex, getExpandedCallNodePaths, getExpandedCallNodeIndexes, getSampleIndexToNonInvertedCallNodeIndexForFilteredThread, getSamplesSelectedStatesInFilteredThread, getTreeOrderComparatorInFilteredThread, getCallTree, + getFunctionListTree, getSourceViewLineTimings, getAssemblyViewAddressTimings, getTracedTiming, @@ -467,5 +534,6 @@ export function getStackAndSampleSelectorsPerThread( getFilteredCallNodeMaxDepthPlusOne, getFlameGraphTiming, getRightClickedCallNodeIndex, + getRightClickedFunctionIndex, }; } diff --git a/src/test/components/Details.test.js b/src/test/components/Details.test.js index 9d40cd18a0..b6d23e5738 100644 --- a/src/test/components/Details.test.js +++ b/src/test/components/Details.test.js @@ -22,6 +22,9 @@ import type { TabSlug } from '../../app-logic/tabs-handling'; jest.mock('../../components/calltree/ProfileCallTreeView', () => ({ ProfileCallTreeView: 'call-tree', })); +jest.mock('../../components/calltree/ProfileFunctionListView', () => ({ + ProfileFunctionListView: 'function-list', +})); jest.mock('../../components/flame-graph', () => ({ FlameGraph: 'flame-graph', })); @@ -73,7 +76,12 @@ describe('app/Details', function () { store.dispatch(changeSelectedTab(tabSlug)); }); // The call tree has a special handling, see the comment above for more information. - const expectedCustomName = tabSlug === 'calltree' ? 'call-tree' : tabSlug; + const table = { + calltree: 'call-tree', + 'function-list': 'function-list', + }; + const expectedCustomName = + tabSlug in table ? table[(tabSlug: string)] : tabSlug; expect(container.querySelector(expectedCustomName)).toBeTruthy(); }); }); diff --git a/src/test/components/DetailsContainer.test.js b/src/test/components/DetailsContainer.test.js index 532839be55..7df3853700 100644 --- a/src/test/components/DetailsContainer.test.js +++ b/src/test/components/DetailsContainer.test.js @@ -39,6 +39,7 @@ describe('app/DetailsContainer', function () { const expectedSidebar: { [TabSlug]: boolean } = { calltree: true, + 'function-list': true, 'flame-graph': true, 'stack-chart': false, 'marker-chart': false, diff --git a/src/test/components/TooltipCallnode.test.js b/src/test/components/TooltipCallnode.test.js index 3a4d0f1396..c31340003d 100644 --- a/src/test/components/TooltipCallnode.test.js +++ b/src/test/components/TooltipCallnode.test.js @@ -150,9 +150,9 @@ describe('TooltipCallNode', function () { iframeUrl, }); + expect(container).toMatchSnapshot(); expect(getByText(iframeUrl)).toBeInTheDocument(); expect(getByText(pageUrl)).toBeInTheDocument(); - expect(container.firstChild).toMatchSnapshot(); }); it('displays the private browsing information', () => { diff --git a/src/test/store/useful-tabs.test.js b/src/test/store/useful-tabs.test.js index 8e4241179f..04fc108127 100644 --- a/src/test/store/useful-tabs.test.js +++ b/src/test/store/useful-tabs.test.js @@ -25,6 +25,7 @@ describe('getUsefulTabs', function () { const { getState } = storeWithProfile(profile); expect(selectedThreadSelectors.getUsefulTabs(getState())).toEqual([ 'calltree', + 'function-list', 'flame-graph', 'stack-chart', 'marker-chart', @@ -57,6 +58,7 @@ describe('getUsefulTabs', function () { const { getState, dispatch } = storeWithProfile(profile); expect(selectedThreadSelectors.getUsefulTabs(getState())).toEqual([ 'calltree', + 'function-list', 'flame-graph', 'stack-chart', 'marker-chart', diff --git a/src/test/unit/bitset.test.js b/src/test/unit/bitset.test.js new file mode 100644 index 0000000000..bc36be5840 --- /dev/null +++ b/src/test/unit/bitset.test.js @@ -0,0 +1,64 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// @flow + +import { makeBitSet, setBit, clearBit, checkBit } from '../../utils/bitset'; + +describe('BitSet', function () { + it('uses an empty typed array when empty', function () { + expect(makeBitSet(0).length).toBe(0); + }); + + it('allocates the right amount of slots', function () { + expect(makeBitSet(31).length).toBe(1); + expect(makeBitSet(32).length).toBe(1); + expect(makeBitSet(33).length).toBe(2); + expect(makeBitSet(63).length).toBe(2); + expect(makeBitSet(64).length).toBe(2); + expect(makeBitSet(65).length).toBe(3); + }); + + it('works in simple cases', function () { + const bitset = makeBitSet(7); + setBit(bitset, 3); + expect(checkBit(bitset, 0)).toBeFalse(); + expect(checkBit(bitset, 3)).toBeTrue(); + expect(checkBit(bitset, 4)).toBeFalse(); + setBit(bitset, 5); + expect(checkBit(bitset, 3)).toBeTrue(); + expect(checkBit(bitset, 5)).toBeTrue(); + setBit(bitset, 3); + expect(checkBit(bitset, 3)).toBeTrue(); + expect(checkBit(bitset, 5)).toBeTrue(); + clearBit(bitset, 5); + expect(checkBit(bitset, 3)).toBeTrue(); + expect(checkBit(bitset, 5)).toBeFalse(); + clearBit(bitset, 3); + expect(checkBit(bitset, 3)).toBeFalse(); + expect(checkBit(bitset, 5)).toBeFalse(); + }); + + it('works when it has to flip the sign bit', function () { + const bitset = makeBitSet(65); + setBit(bitset, 30); + expect(checkBit(bitset, 30)).toBeTrue(); + expect(checkBit(bitset, 31)).toBeFalse(); + setBit(bitset, 31); + expect(checkBit(bitset, 30)).toBeTrue(); + expect(checkBit(bitset, 31)).toBeTrue(); + expect(checkBit(bitset, 32)).toBeFalse(); + setBit(bitset, 32); + setBit(bitset, 63); + expect(checkBit(bitset, 32)).toBeTrue(); + expect(checkBit(bitset, 62)).toBeFalse(); + expect(checkBit(bitset, 63)).toBeTrue(); + expect(checkBit(bitset, 64)).toBeFalse(); + clearBit(bitset, 31); + expect(checkBit(bitset, 30)).toBeTrue(); + expect(checkBit(bitset, 31)).toBeFalse(); + expect(checkBit(bitset, 32)).toBeTrue(); + expect(checkBit(bitset, 63)).toBeTrue(); + }); +}); diff --git a/src/types/actions.js b/src/types/actions.js index 2b928ed405..9de2b736ea 100644 --- a/src/types/actions.js +++ b/src/types/actions.js @@ -46,6 +46,7 @@ import type { } from './state'; import type { CssPixels, StartEndRange, Milliseconds } from './units'; import type { BrowserConnectionStatus } from '../app-logic/browser-connection'; +import type { IndexIntoFuncTable } from '../types'; export type DataSource = | 'none' @@ -195,6 +196,12 @@ type ProfileAction = +type: 'ROUTE_NOT_FOUND', +url: string, |} + | {| + +type: 'CHANGE_SELECTED_FUNCTION', + +threadsKey: ThreadsKey, + +selectedFunctionIndex: IndexIntoFuncTable | null, + +context: SelectionContext, + |} | {| +type: 'ASSIGN_TASK_TRACER_NAMES', +addressIndices: number[], diff --git a/src/types/profile-derived.js b/src/types/profile-derived.js index b504632c7e..3ab3d2f780 100644 --- a/src/types/profile-derived.js +++ b/src/types/profile-derived.js @@ -37,6 +37,7 @@ import type { IndexIntoSubcategoryListForCategory, } from './profile'; import type { IndexedArray } from './utils'; +import type { BitSet } from '../utils/bitset'; import type { StackTiming } from '../profile-logic/stack-timing'; import type { StringTable } from '../utils/string-table'; export type IndexIntoCallNodeTable = number; @@ -303,6 +304,10 @@ export type CallNodeTable = { length: number, }; +// A bitset which indicates something per call node. Use `checkBit` from +// utils/bitset to check whether a call node is part of the set. +export type CallNodeTableBitSet = BitSet; + export type LineNumber = number; // Stores the line numbers which are hit by each stack, for one specific source diff --git a/src/types/state.js b/src/types/state.js index 332a5459ac..622e0bd429 100644 --- a/src/types/state.js +++ b/src/types/state.js @@ -45,6 +45,7 @@ import type { IndexIntoZipFileTable } from '../profile-logic/zip-files'; import type { PathSet } from '../utils/path.js'; import type { UploadedProfileInformation as ImportedUploadedProfileInformation } from 'firefox-profiler/app-logic/uploaded-profiles-db'; import type { BrowserConnectionStatus } from 'firefox-profiler/app-logic/browser-connection'; +import type { IndexIntoFuncTable } from 'firefox-profiler/types'; export type Reducer = (T | void, Action) => T; @@ -60,6 +61,7 @@ export type ThreadViewOptions = {| +selectedInvertedCallNodePath: CallNodePath, +expandedNonInvertedCallNodePaths: PathSet, +expandedInvertedCallNodePaths: PathSet, + +selectedFunctionIndex: IndexIntoFuncTable | null, +selectedMarker: MarkerIndex | null, +selectedNetworkMarker: MarkerIndex | null, |}; @@ -77,6 +79,11 @@ export type RightClickedCallNode = {| +callNodePath: CallNodePath, |}; +export type RightClickedFunction = {| + +threadsKey: ThreadsKey, + +functionIndex: IndexIntoFuncTable, +|}; + export type MarkerReference = {| +threadsKey: ThreadsKey, +markerIndex: MarkerIndex, @@ -120,6 +127,7 @@ export type ProfileViewState = { lastNonShiftClick: LastNonShiftClickInformation | null, rightClickedTrack: TrackReference | null, rightClickedCallNode: RightClickedCallNode | null, + rightClickedFunction: RightClickedFunction | null, rightClickedMarker: MarkerReference | null, hoveredMarker: MarkerReference | null, mouseTimePosition: Milliseconds | null, diff --git a/src/utils/bitset.js b/src/utils/bitset.js new file mode 100644 index 0000000000..b6fbd183ef --- /dev/null +++ b/src/utils/bitset.js @@ -0,0 +1,32 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// @flow + +export type BitSet = Int32Array; + +export function makeBitSet(length: number): BitSet { + const lastIndex = length - 1; + const lastSlot = lastIndex >> 5; + const slotCount = lastSlot + 1; + return new Int32Array(slotCount); +} + +export function setBit(bitSet: BitSet, bitIndex: number) { + const q = bitIndex >> 5; + const r = bitIndex & 0b11111; + bitSet[q] |= 1 << r; +} + +export function clearBit(bitSet: BitSet, bitIndex: number) { + const q = bitIndex >> 5; + const r = bitIndex & 0b11111; + bitSet[q] &= ~(1 << r); +} + +export function checkBit(bitSet: BitSet, bitIndex: number): boolean { + const q = bitIndex >> 5; + const r = bitIndex & 0b11111; + return (bitSet[q] & (1 << r)) !== 0; +} diff --git a/src/utils/flow.js b/src/utils/flow.js index 1f6e834b3c..4e11d7780b 100644 --- a/src/utils/flow.js +++ b/src/utils/flow.js @@ -43,6 +43,7 @@ export function toValidTabSlug(tabSlug: any): TabSlug | null { const coercedTabSlug = (tabSlug: TabSlug); switch (coercedTabSlug) { case 'calltree': + case 'function-list': case 'stack-chart': case 'marker-chart': case 'network-chart': From fb7aa5b05c682adf1f60f2a5e8652de426c6be15 Mon Sep 17 00:00:00 2001 From: Markus Stange Date: Sat, 30 Nov 2024 19:04:56 -0500 Subject: [PATCH 23/25] Add inverted butterfly wing. --- src/actions/profile-view.js | 58 ++- src/components/app/DetailsContainer.css | 6 +- src/components/calltree/Butterfly.css | 9 + src/components/calltree/LowerWing.js | 343 ++++++++++++++++++ .../calltree/ProfileFunctionListView.js | 14 +- src/profile-logic/call-tree.js | 53 +++ src/reducers/profile-view.js | 81 +++-- src/selectors/per-thread/stack-sample.js | 93 ++++- src/selectors/right-clicked-call-node.js | 3 + src/types/actions.js | 6 +- src/types/state.js | 5 + 11 files changed, 622 insertions(+), 49 deletions(-) create mode 100644 src/components/calltree/Butterfly.css create mode 100644 src/components/calltree/LowerWing.js diff --git a/src/actions/profile-view.js b/src/actions/profile-view.js index e669854a3a..feaf498670 100644 --- a/src/actions/profile-view.js +++ b/src/actions/profile-view.js @@ -125,7 +125,7 @@ export function changeSelectedCallNode( const isInverted = getInvertCallstack(getState()); dispatch({ type: 'CHANGE_SELECTED_CALL_NODE', - isInverted, + area: isInverted ? 'INVERTED_TREE' : 'NON_INVERTED_TREE', selectedCallNodePath, optionalExpandedToCallNodePath, threadsKey, @@ -134,6 +134,21 @@ export function changeSelectedCallNode( }; } +export function changeLowerWingSelectedCallNode( + threadsKey: ThreadsKey, + selectedCallNodePath: CallNodePath, + context: SelectionContext = { source: 'auto' } +): Action { + return { + type: 'CHANGE_SELECTED_CALL_NODE', + area: 'LOWER_WING', + selectedCallNodePath, + optionalExpandedToCallNodePath: [], + threadsKey, + context, + }; +} + /** * Select a function for a given thread in the function list. */ @@ -158,11 +173,15 @@ export function changeSelectedFunctionIndex( export function changeRightClickedCallNode( threadsKey: ThreadsKey, callNodePath: CallNodePath | null -) { - return { - type: 'CHANGE_RIGHT_CLICKED_CALL_NODE', - threadsKey, - callNodePath, +): ThunkAction { + return (dispatch, getState) => { + const isInverted = getInvertCallstack(getState()); + dispatch({ + type: 'CHANGE_RIGHT_CLICKED_CALL_NODE', + threadsKey, + area: isInverted ? 'INVERTED_TREE' : 'NON_INVERTED_TREE', + callNodePath, + }); }; } @@ -177,6 +196,18 @@ export function changeRightClickedFunctionIndex( }; } +export function changeLowerWingRightClickedCallNode( + threadsKey: ThreadsKey, + callNodePath: CallNodePath | null +) { + return { + type: 'CHANGE_RIGHT_CLICKED_CALL_NODE', + threadsKey, + area: 'LOWER_WING', + callNodePath, + }; +} + /** * Given a threadIndex and a sampleIndex, select the call node which carries the * sample's self time. In the inverted tree, this will be a root node. @@ -1660,12 +1691,25 @@ export function changeExpandedCallNodes( const isInverted = getInvertCallstack(getState()); dispatch({ type: 'CHANGE_EXPANDED_CALL_NODES', - isInverted, + area: isInverted ? 'INVERTED_TREE' : 'NON_INVERTED_TREE', threadsKey, expandedCallNodePaths, }); }; } + +export function changeLowerWingExpandedCallNodes( + threadsKey: ThreadsKey, + expandedCallNodePaths: Array +): Action { + return { + type: 'CHANGE_EXPANDED_CALL_NODES', + area: 'LOWER_WING', + threadsKey, + expandedCallNodePaths, + }; +} + export function changeSelectedMarker( threadsKey: ThreadsKey, selectedMarker: MarkerIndex | null, diff --git a/src/components/app/DetailsContainer.css b/src/components/app/DetailsContainer.css index f46c2679eb..4ea0c4b6fd 100644 --- a/src/components/app/DetailsContainer.css +++ b/src/components/app/DetailsContainer.css @@ -4,7 +4,7 @@ box-sizing: border-box; } -.DetailsContainer .layout-pane:not(.layout-pane-primary) { +.DetailsContainer > .layout-pane:not(.layout-pane-primary) { max-width: 600px; } @@ -15,12 +15,12 @@ position: unset; } -.DetailsContainer .layout-splitter { +.DetailsContainer > .layout-splitter { border-top: 1px solid var(--grey-30); border-left: 1px solid var(--grey-30); background: var(--grey-10); /* Same background as sidebars */ } -.DetailsContainer .layout-splitter:hover { +.DetailsContainer > .layout-splitter:hover { background: var(--grey-30); /* same as the border above */ } diff --git a/src/components/calltree/Butterfly.css b/src/components/calltree/Butterfly.css new file mode 100644 index 0000000000..110fd0486c --- /dev/null +++ b/src/components/calltree/Butterfly.css @@ -0,0 +1,9 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +.butterflyWrapper { + position: relative; + min-height: 0; + flex: 1; +} diff --git a/src/components/calltree/LowerWing.js b/src/components/calltree/LowerWing.js new file mode 100644 index 0000000000..4bc5e091c1 --- /dev/null +++ b/src/components/calltree/LowerWing.js @@ -0,0 +1,343 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +// @flow + +import React, { PureComponent } from 'react'; +import memoize from 'memoize-immutable'; +import explicitConnect from 'firefox-profiler/utils/connect'; +import { TreeView } from 'firefox-profiler/components/shared/TreeView'; +import { CallTreeEmptyReasons } from './CallTreeEmptyReasons'; +import { + treeColumnsForTracingMs, + treeColumnsForSamples, + treeColumnsForBytes, +} from './columns'; +import { + getSearchStringsAsRegExp, + getSelectedThreadsKey, +} from 'firefox-profiler/selectors/url-state'; +import { + getScrollToSelectionGeneration, + getPreviewSelection, + getCategories, + getCurrentTableViewOptions, +} from 'firefox-profiler/selectors/profile'; +import { selectedThreadSelectors } from 'firefox-profiler/selectors/per-thread'; +import { + changeLowerWingSelectedCallNode, + changeLowerWingRightClickedCallNode, + changeLowerWingExpandedCallNodes, + addTransformToStack, + handleCallNodeTransformShortcut, + changeTableViewOptions, + updateBottomBoxContentsAndMaybeOpen, +} from 'firefox-profiler/actions/profile-view'; +import { assertExhaustiveCheck } from 'firefox-profiler/utils/flow'; + +import type { + State, + ThreadsKey, + CategoryList, + IndexIntoCallNodeTable, + CallNodeDisplayData, + WeightType, + TableViewOptions, + 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, + MaybeResizableColumn, +} from 'firefox-profiler/components/shared/TreeView'; +import type { ConnectedProps } from 'firefox-profiler/utils/connect'; + +import './CallTree.css'; + +type StateProps = {| + +threadsKey: ThreadsKey, + +scrollToSelectionGeneration: number, + +tree: CallTreeType, + +callNodeInfo: CallNodeInfo, + +categories: CategoryList, + +selectedCallNodeIndex: IndexIntoCallNodeTable | null, + +rightClickedCallNodeIndex: IndexIntoCallNodeTable | null, + +expandedCallNodeIndexes: Array, + +searchStringsRegExp: RegExp | null, + +disableOverscan: boolean, + +callNodeMaxDepthPlusOne: number, + +weightType: WeightType, + +tableViewOptions: TableViewOptions, +|}; + +type DispatchProps = {| + +changeLowerWingSelectedCallNode: typeof changeLowerWingSelectedCallNode, + +changeLowerWingRightClickedCallNode: typeof changeLowerWingRightClickedCallNode, + +changeLowerWingExpandedCallNodes: typeof changeLowerWingExpandedCallNodes, + +addTransformToStack: typeof addTransformToStack, + +handleCallNodeTransformShortcut: typeof handleCallNodeTransformShortcut, + +updateBottomBoxContentsAndMaybeOpen: typeof updateBottomBoxContentsAndMaybeOpen, + +onTableViewOptionsChange: (TableViewOptions) => any, +|}; + +type Props = ConnectedProps<{||}, StateProps, DispatchProps>; + +class LowerWingImpl extends PureComponent { + _mainColumn: Column = { + propName: 'name', + titleL10nId: '', + }; + _appendageColumn: Column = { + propName: 'lib', + titleL10nId: '', + }; + _treeView: TreeView | null = null; + _takeTreeViewRef = (treeView) => (this._treeView = treeView); + + /** + * Call Trees can have different types of "weights" for the data. Choose the + * appropriate labels for the call tree based on this weight. + */ + _weightTypeToColumns = memoize( + (weightType: WeightType): MaybeResizableColumn[] => { + switch (weightType) { + case 'tracing-ms': + return treeColumnsForTracingMs; + case 'samples': + return treeColumnsForSamples; + case 'bytes': + return treeColumnsForBytes; + default: + throw assertExhaustiveCheck(weightType, 'Unhandled WeightType.'); + } + }, + // Use a Map cache, as the function only takes one argument, which is a simple string. + { cache: new Map() } + ); + + componentDidMount() { + this.focus(); + this.maybeProcureInterestingInitialSelection(); + + if (this.props.selectedCallNodeIndex !== null && this._treeView) { + this._treeView.scrollSelectionIntoView(); + } + } + + componentDidUpdate(prevProps) { + this.maybeProcureInterestingInitialSelection(); + + if ( + this.props.selectedCallNodeIndex !== null && + this.props.scrollToSelectionGeneration > + prevProps.scrollToSelectionGeneration && + this._treeView + ) { + this._treeView.scrollSelectionIntoView(); + } + } + + focus() { + if (this._treeView) { + this._treeView.focus(); + } + } + + _onSelectedCallNodeChange = ( + newSelectedCallNode: IndexIntoCallNodeTable, + context: SelectionContext + ) => { + const { callNodeInfo, threadsKey, changeLowerWingSelectedCallNode } = + this.props; + changeLowerWingSelectedCallNode( + threadsKey, + callNodeInfo.getCallNodePathFromIndex(newSelectedCallNode), + context + ); + }; + + _onRightClickSelection = (newSelectedCallNode: IndexIntoCallNodeTable) => { + const { callNodeInfo, threadsKey, changeLowerWingRightClickedCallNode } = + this.props; + changeLowerWingRightClickedCallNode( + threadsKey, + callNodeInfo.getCallNodePathFromIndex(newSelectedCallNode) + ); + }; + + _onExpandedCallNodesChange = ( + newExpandedCallNodeIndexes: Array + ) => { + const { callNodeInfo, threadsKey, changeLowerWingExpandedCallNodes } = + this.props; + changeLowerWingExpandedCallNodes( + threadsKey, + newExpandedCallNodeIndexes.map((callNodeIndex) => + callNodeInfo.getCallNodePathFromIndex(callNodeIndex) + ) + ); + }; + + _onKeyDown = (event: SyntheticKeyboardEvent<>) => { + const { + selectedCallNodeIndex, + rightClickedCallNodeIndex, + handleCallNodeTransformShortcut, + threadsKey, + } = this.props; + const nodeIndex = + rightClickedCallNodeIndex !== null + ? rightClickedCallNodeIndex + : selectedCallNodeIndex; + if (nodeIndex === null) { + return; + } + handleCallNodeTransformShortcut(event, threadsKey, nodeIndex); + }; + + _onEnterOrDoubleClick = (nodeId: IndexIntoCallNodeTable) => { + const { tree, updateBottomBoxContentsAndMaybeOpen } = this.props; + const bottomBoxInfo = tree.getBottomBoxInfoForCallNode(nodeId); + updateBottomBoxContentsAndMaybeOpen('calltree', bottomBoxInfo); + }; + + maybeProcureInterestingInitialSelection() { + // Expand the heaviest callstack up to a certain depth and select the frame + // at that depth. + const { + tree, + expandedCallNodeIndexes, + selectedCallNodeIndex, + callNodeInfo, + categories, + } = this.props; + + if (selectedCallNodeIndex !== null || expandedCallNodeIndexes.length > 0) { + // Let's not change some existing state. + return; + } + + const idleCategoryIndex = categories.findIndex( + (category) => category.name === 'Idle' + ); + + const newExpandedCallNodeIndexes = expandedCallNodeIndexes.slice(); + const maxInterestingDepth = 17; // scientifically determined + let currentCallNodeIndex = tree.getRoots()[0]; + if (currentCallNodeIndex === undefined) { + // This tree is empty. + return; + } + newExpandedCallNodeIndexes.push(currentCallNodeIndex); + for (let i = 0; i < maxInterestingDepth; i++) { + const children = tree.getChildren(currentCallNodeIndex); + if (children.length === 0) { + break; + } + + // Let's find if there's a non idle children. + const firstNonIdleNode = children.find( + (nodeIndex) => + callNodeInfo.categoryForNode(nodeIndex) !== idleCategoryIndex + ); + + // If there's a non idle children, use it; otherwise use the first + // children (that will be idle). + currentCallNodeIndex = + firstNonIdleNode !== undefined ? firstNonIdleNode : children[0]; + newExpandedCallNodeIndexes.push(currentCallNodeIndex); + } + this._onExpandedCallNodesChange(newExpandedCallNodeIndexes); + + 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 + // this graph. Because this isn't probably what the average user wants we + // do it only when the category is something different. + this._onSelectedCallNodeChange(currentCallNodeIndex, { source: 'auto' }); + } + } + + render() { + const { + tree, + selectedCallNodeIndex, + rightClickedCallNodeIndex, + expandedCallNodeIndexes, + searchStringsRegExp, + disableOverscan, + callNodeMaxDepthPlusOne, + weightType, + tableViewOptions, + onTableViewOptionsChange, + } = this.props; + if (tree.getRoots().length === 0) { + return ; + } + return ( + + ); + } +} + +export const LowerWing = explicitConnect<{||}, StateProps, DispatchProps>({ + mapStateToProps: (state: State) => ({ + threadsKey: getSelectedThreadsKey(state), + scrollToSelectionGeneration: getScrollToSelectionGeneration(state), + tree: selectedThreadSelectors.getLowerWingCallTree(state), + callNodeInfo: selectedThreadSelectors.getLowerWingCallNodeInfo(state), + categories: getCategories(state), + selectedCallNodeIndex: + selectedThreadSelectors.getLowerWingSelectedCallNodeIndex(state), + rightClickedCallNodeIndex: + selectedThreadSelectors.getLowerWingRightClickedCallNodeIndex(state), + expandedCallNodeIndexes: + selectedThreadSelectors.getLowerWingExpandedCallNodeIndexes(state), + searchStringsRegExp: getSearchStringsAsRegExp(state), + disableOverscan: getPreviewSelection(state).isModifying, + // Use the filtered call node max depth, rather than the preview filtered call node + // max depth so that the width of the TreeView component is stable across preview + // selections. + callNodeMaxDepthPlusOne: + selectedThreadSelectors.getFilteredCallNodeMaxDepthPlusOne(state), + weightType: selectedThreadSelectors.getWeightTypeForCallTree(state), + tableViewOptions: getCurrentTableViewOptions(state), + }), + mapDispatchToProps: { + changeLowerWingSelectedCallNode, + changeLowerWingRightClickedCallNode, + changeLowerWingExpandedCallNodes, + addTransformToStack, + handleCallNodeTransformShortcut, + updateBottomBoxContentsAndMaybeOpen, + onTableViewOptionsChange: (options: TableViewOptions) => + changeTableViewOptions('function-list', options), + }, + component: LowerWingImpl, +}); diff --git a/src/components/calltree/ProfileFunctionListView.js b/src/components/calltree/ProfileFunctionListView.js index 790de91a62..164ba232c6 100644 --- a/src/components/calltree/ProfileFunctionListView.js +++ b/src/components/calltree/ProfileFunctionListView.js @@ -5,10 +5,14 @@ // @flow import React from 'react'; +import SplitterLayout from 'react-splitter-layout'; import { FunctionList } from './FunctionList'; +import { LowerWing } from './LowerWing'; import { StackSettings } from 'firefox-profiler/components/shared/StackSettings'; import { TransformNavigator } from 'firefox-profiler/components/shared/TransformNavigator'; +import './Butterfly.css'; + export const ProfileFunctionListView = () => (
( > - +
+ + + + + + + +
); diff --git a/src/profile-logic/call-tree.js b/src/profile-logic/call-tree.js index 0d5fffcb69..73abc8c62d 100644 --- a/src/profile-logic/call-tree.js +++ b/src/profile-logic/call-tree.js @@ -1098,6 +1098,59 @@ export function computeFunctionListTimings( }; } +function _computeLowerWingCallNodeSelf( + callNodeSelf: Float32Array, + callNodeTable: CallNodeTable, + selectedFuncIndex: IndexIntoFuncTable +): Float32Array { + // There is an implicit mapping so that every call node in the non-inverted table is mapped to: + // - either the root-most ancestor whose func is selectedFuncIndex, or + // - -1 if no such ancestor exists + const callNodeCount = callNodeTable.length; + const funcCol = callNodeTable.func; + const subtreeEndCol = callNodeTable.subtreeRangeEnd; + const mappedSelf = new Float32Array(callNodeCount); + for (let i = 0; i < callNodeCount; i++) { + if (funcCol[i] !== selectedFuncIndex) { + continue; + } + + // Call node i is the root of a subtree for the selected function. + const subtreeEnd = subtreeEndCol[i]; + let subtreeTotal = 0; + for (let j = i; j < subtreeEnd; j++) { + subtreeTotal += callNodeSelf[j]; + } + mappedSelf[i] = subtreeTotal; + i = subtreeEnd - 1; + } + return mappedSelf; +} + +export function computeLowerWingTimings( + callNodeInfo: CallNodeInfoInverted, + { callNodeSelf, rootTotalSummary }: CallNodeSelfAndSummary, + selectedFuncIndex: IndexIntoFuncTable | null +): CallTreeTimings { + const callNodeTable = callNodeInfo.getNonInvertedCallNodeTable(); + const mappedSelf = + selectedFuncIndex !== null + ? _computeLowerWingCallNodeSelf( + callNodeSelf, + callNodeTable, + selectedFuncIndex + ) + : new Float32Array(callNodeSelf.length); + + return { + type: 'INVERTED', + timings: computeCallTreeTimingsInverted(callNodeInfo, { + callNodeSelf: mappedSelf, + rootTotalSummary, + }), + }; +} + export function computeCallTreeTimings( callNodeInfo: CallNodeInfo, callNodeSelfAndSummary: CallNodeSelfAndSummary diff --git a/src/reducers/profile-view.js b/src/reducers/profile-view.js index a386a87b62..a7e25623c3 100644 --- a/src/reducers/profile-view.js +++ b/src/reducers/profile-view.js @@ -40,7 +40,7 @@ import { } from '../profile-logic/symbolication'; import type { TabSlug } from '../app-logic/tabs-handling'; -import { objectMap } from '../utils/flow'; +import { objectMap, assertExhaustiveCheck } from '../utils/flow'; const profile: Reducer = (state = null, action) => { switch (action.type) { @@ -156,8 +156,10 @@ const symbolicationStatus: Reducer = ( export const defaultThreadViewOptions: ThreadViewOptions = { selectedNonInvertedCallNodePath: [], selectedInvertedCallNodePath: [], + selectedLowerWingCallNodePath: [], expandedNonInvertedCallNodePaths: new PathSet(), expandedInvertedCallNodePaths: new PathSet(), + expandedLowerWingCallNodePaths: new PathSet(), selectedFunctionIndex: null, selectedMarker: null, selectedNetworkMarker: null, @@ -243,7 +245,7 @@ const viewOptionsPerThread: Reducer = ( } case 'CHANGE_SELECTED_CALL_NODE': { const { - isInverted, + area, selectedCallNodePath, threadsKey, optionalExpandedToCallNodePath, @@ -251,9 +253,11 @@ const viewOptionsPerThread: Reducer = ( const threadState = _getThreadViewOptions(state, threadsKey); - const previousSelectedCallNodePath = isInverted - ? threadState.selectedInvertedCallNodePath - : threadState.selectedNonInvertedCallNodePath; + const previousSelectedCallNodePath = { + INVERTED_TREE: threadState.selectedInvertedCallNodePath, + NON_INVERTED_TREE: threadState.selectedNonInvertedCallNodePath, + LOWER_WING: threadState.selectedLowerWingCallNodePath, + }[area]; // If the selected node doesn't actually change, let's return the previous // state to avoid rerenders. @@ -264,9 +268,12 @@ const viewOptionsPerThread: Reducer = ( return state; } - let expandedCallNodePaths = isInverted - ? threadState.expandedInvertedCallNodePaths - : threadState.expandedNonInvertedCallNodePaths; + let expandedCallNodePaths = { + INVERTED_TREE: threadState.expandedInvertedCallNodePaths, + NON_INVERTED_TREE: threadState.expandedNonInvertedCallNodePaths, + LOWER_WING: threadState.expandedLowerWingCallNodePaths, + }[area]; + const expandToNode = optionalExpandedToCallNodePath ? optionalExpandedToCallNodePath : selectedCallNodePath; @@ -290,19 +297,25 @@ const viewOptionsPerThread: Reducer = ( ); } - return _updateThreadViewOptions( - state, - threadsKey, - isInverted - ? { - selectedInvertedCallNodePath: selectedCallNodePath, - expandedInvertedCallNodePaths: expandedCallNodePaths, - } - : { - selectedNonInvertedCallNodePath: selectedCallNodePath, - expandedNonInvertedCallNodePaths: expandedCallNodePaths, - } - ); + switch (area) { + case 'INVERTED_TREE': + return _updateThreadViewOptions(state, threadsKey, { + selectedInvertedCallNodePath: selectedCallNodePath, + expandedInvertedCallNodePaths: expandedCallNodePaths, + }); + case 'NON_INVERTED_TREE': + return _updateThreadViewOptions(state, threadsKey, { + selectedNonInvertedCallNodePath: selectedCallNodePath, + expandedNonInvertedCallNodePaths: expandedCallNodePaths, + }); + case 'LOWER_WING': + return _updateThreadViewOptions(state, threadsKey, { + selectedLowerWingCallNodePath: selectedCallNodePath, + expandedLowerWingCallNodePaths: expandedCallNodePaths, + }); + default: + throw assertExhaustiveCheck(area, 'Unhandled case'); + } } case 'CHANGE_SELECTED_FUNCTION': { const { selectedFunctionIndex, threadsKey } = action; @@ -355,16 +368,25 @@ const viewOptionsPerThread: Reducer = ( }); } case 'CHANGE_EXPANDED_CALL_NODES': { - const { threadsKey, isInverted } = action; + const { threadsKey, area } = action; const expandedCallNodePaths = new PathSet(action.expandedCallNodePaths); - return _updateThreadViewOptions( - state, - threadsKey, - isInverted - ? { expandedInvertedCallNodePaths: expandedCallNodePaths } - : { expandedNonInvertedCallNodePaths: expandedCallNodePaths } - ); + switch (area) { + case 'INVERTED_TREE': + return _updateThreadViewOptions(state, threadsKey, { + expandedInvertedCallNodePaths: expandedCallNodePaths, + }); + case 'NON_INVERTED_TREE': + return _updateThreadViewOptions(state, threadsKey, { + expandedNonInvertedCallNodePaths: expandedCallNodePaths, + }); + case 'LOWER_WING': + return _updateThreadViewOptions(state, threadsKey, { + expandedLowerWingCallNodePaths: expandedCallNodePaths, + }); + default: + throw assertExhaustiveCheck(area, 'Unhandled case'); + } } case 'CHANGE_SELECTED_MARKER': { const { threadsKey, selectedMarker } = action; @@ -741,6 +763,7 @@ const rightClickedCallNode: Reducer = ( if (action.callNodePath !== null) { return { threadsKey: action.threadsKey, + area: action.area, callNodePath: action.callNodePath, }; } diff --git a/src/selectors/per-thread/stack-sample.js b/src/selectors/per-thread/stack-sample.js index 70b1b3adfc..822ee47107 100644 --- a/src/selectors/per-thread/stack-sample.js +++ b/src/selectors/per-thread/stack-sample.js @@ -141,14 +141,13 @@ export function getStackAndSampleSelectorsPerThread( return _getNonInvertedCallNodeInfo(state); }; + const getLowerWingCallNodeInfo = _getInvertedCallNodeInfo; + const _getCallNodeFuncIsDuplicate: Selector = - createSelector( - getCallNodeInfo, - (callNodeInfo) => { - const callNodeTable = callNodeInfo.getNonInvertedCallNodeTable(); - return ProfileData.computeCallNodeFuncIsDuplicate(callNodeTable); - } - ); + createSelector(getCallNodeInfo, (callNodeInfo) => { + const callNodeTable = callNodeInfo.getNonInvertedCallNodeTable(); + return ProfileData.computeCallNodeFuncIsDuplicate(callNodeTable); + }); const getSourceViewStackLineInfo: Selector = createSelector( @@ -224,6 +223,13 @@ export function getStackAndSampleSelectorsPerThread( } ); + const getLowerWingSelectedCallNodePath: Selector = + createSelector( + threadSelectors.getViewOptions, + (threadViewOptions): CallNodePath => + threadViewOptions.selectedLowerWingCallNodePath + ); + const getSelectedCallNodeIndex: Selector = createSelector( getCallNodeInfo, @@ -233,6 +239,15 @@ export function getStackAndSampleSelectorsPerThread( } ); + const getLowerWingSelectedCallNodeIndex: Selector = + createSelector( + getLowerWingCallNodeInfo, + getLowerWingSelectedCallNodePath, + (callNodeInfo, callNodePath) => { + return callNodeInfo.getCallNodeIndexFromPath(callNodePath); + } + ); + const getExpandedCallNodePaths: Selector = createSelector( threadSelectors.getViewOptions, UrlState.getInvertCallstack, @@ -242,6 +257,11 @@ export function getStackAndSampleSelectorsPerThread( : threadViewOptions.expandedNonInvertedCallNodePaths ); + const getLowerWingExpandedCallNodePaths: Selector = createSelector( + threadSelectors.getViewOptions, + (threadViewOptions) => threadViewOptions.expandedLowerWingCallNodePaths + ); + const getExpandedCallNodeIndexes: Selector< Array, > = createSelector( @@ -253,6 +273,17 @@ export function getStackAndSampleSelectorsPerThread( ) ); + const getLowerWingExpandedCallNodeIndexes: Selector< + Array, + > = createSelector( + getLowerWingCallNodeInfo, + getLowerWingExpandedCallNodePaths, + (callNodeInfo, callNodePaths) => + Array.from(callNodePaths).map((path) => + callNodeInfo.getCallNodeIndexFromPath(path) + ) + ); + const _getPreviewFilteredCtssSampleIndexToNonInvertedCallNodeIndex: Selector< Array, > = createSelector( @@ -353,6 +384,14 @@ export function getStackAndSampleSelectorsPerThread( CallTree.computeCallTreeTimings ); + const _getLowerWingCallTreeTimings: Selector = + createSelector( + _getInvertedCallNodeInfo, + getCallNodeSelfAndSummary, + getSelectedFunctionIndex, + CallTree.computeLowerWingTimings + ); + const getCallTreeTimingsNonInverted: Selector = createSelector( getCallNodeInfo, @@ -395,6 +434,15 @@ export function getStackAndSampleSelectorsPerThread( CallTree.getFunctionListTree ); + const getLowerWingCallTree: Selector = createSelector( + threadSelectors.getPreviewFilteredThread, + getLowerWingCallNodeInfo, + ProfileSelectors.getCategories, + _getLowerWingCallTreeTimings, + getWeightTypeForCallTree, + CallTree.getCallTree + ); + const getSourceViewLineTimings: Selector = createSelector( getSourceViewStackLineInfo, threadSelectors.getPreviewFilteredCtssSamples, @@ -482,6 +530,30 @@ export function getStackAndSampleSelectorsPerThread( if ( rightClickedCallNodeInfo !== null && threadsKey === rightClickedCallNodeInfo.threadsKey + ) { + const expectedArea = callNodeInfo.isInverted() + ? 'INVERTED_TREE' + : 'NON_INVERTED_TREE'; + if (rightClickedCallNodeInfo.area === expectedArea) { + return callNodeInfo.getCallNodeIndexFromPath( + rightClickedCallNodeInfo.callNodePath + ); + } + } + + return null; + } + ); + + const getLowerWingRightClickedCallNodeIndex: Selector = + createSelector( + getRightClickedCallNodeInfo, + getCallNodeInfo, + (rightClickedCallNodeInfo, callNodeInfo) => { + if ( + rightClickedCallNodeInfo !== null && + rightClickedCallNodeInfo.threadsKey === threadsKey && + rightClickedCallNodeInfo.area === 'LOWER_WING' ) { return callNodeInfo.getCallNodeIndexFromPath( rightClickedCallNodeInfo.callNodePath @@ -513,19 +585,25 @@ export function getStackAndSampleSelectorsPerThread( unfilteredSamplesRange, getWeightTypeForCallTree, getCallNodeInfo, + getLowerWingCallNodeInfo, getSourceViewStackLineInfo, getAssemblyViewNativeSymbolIndex, getAssemblyViewStackAddressInfo, getSelectedCallNodePath, getSelectedCallNodeIndex, + getLowerWingSelectedCallNodePath, + getLowerWingSelectedCallNodeIndex, getSelectedFunctionIndex, getExpandedCallNodePaths, getExpandedCallNodeIndexes, + getLowerWingExpandedCallNodePaths, + getLowerWingExpandedCallNodeIndexes, getSampleIndexToNonInvertedCallNodeIndexForFilteredThread, getSamplesSelectedStatesInFilteredThread, getTreeOrderComparatorInFilteredThread, getCallTree, getFunctionListTree, + getLowerWingCallTree, getSourceViewLineTimings, getAssemblyViewAddressTimings, getTracedTiming, @@ -535,5 +613,6 @@ export function getStackAndSampleSelectorsPerThread( getFlameGraphTiming, getRightClickedCallNodeIndex, getRightClickedFunctionIndex, + getLowerWingRightClickedCallNodeIndex, }; } diff --git a/src/selectors/right-clicked-call-node.js b/src/selectors/right-clicked-call-node.js index 7dabb2501b..2dd80af0f4 100644 --- a/src/selectors/right-clicked-call-node.js +++ b/src/selectors/right-clicked-call-node.js @@ -13,8 +13,11 @@ import type { Selector, } from 'firefox-profiler/types'; +import type { CallNodeArea } from '../types/state'; + export type RightClickedCallNodeInfo = {| +threadsKey: ThreadsKey, + +area: CallNodeArea, +callNodePath: CallNodePath, |}; diff --git a/src/types/actions.js b/src/types/actions.js index 9de2b736ea..d0183f3400 100644 --- a/src/types/actions.js +++ b/src/types/actions.js @@ -43,6 +43,7 @@ import type { ApiQueryError, TableViewOptions, DecodedInstruction, + CallNodeArea, } from './state'; import type { CssPixels, StartEndRange, Milliseconds } from './units'; import type { BrowserConnectionStatus } from '../app-logic/browser-connection'; @@ -209,7 +210,7 @@ type ProfileAction = |} | {| +type: 'CHANGE_SELECTED_CALL_NODE', - +isInverted: boolean, + +area: CallNodeArea, +threadsKey: ThreadsKey, +selectedCallNodePath: CallNodePath, +optionalExpandedToCallNodePath: ?CallNodePath, @@ -223,6 +224,7 @@ type ProfileAction = | {| +type: 'CHANGE_RIGHT_CLICKED_CALL_NODE', +threadsKey: ThreadsKey, + +area: CallNodeArea, +callNodePath: CallNodePath | null, |} | {| @@ -231,7 +233,7 @@ type ProfileAction = | {| +type: 'CHANGE_EXPANDED_CALL_NODES', +threadsKey: ThreadsKey, - +isInverted: boolean, + +area: CallNodeArea, +expandedCallNodePaths: Array, |} | {| diff --git a/src/types/state.js b/src/types/state.js index 622e0bd429..5e43d754e8 100644 --- a/src/types/state.js +++ b/src/types/state.js @@ -62,6 +62,8 @@ export type ThreadViewOptions = {| +expandedNonInvertedCallNodePaths: PathSet, +expandedInvertedCallNodePaths: PathSet, +selectedFunctionIndex: IndexIntoFuncTable | null, + +selectedLowerWingCallNodePath: CallNodePath, + +expandedLowerWingCallNodePaths: PathSet, +selectedMarker: MarkerIndex | null, +selectedNetworkMarker: MarkerIndex | null, |}; @@ -74,8 +76,11 @@ export type TableViewOptions = {| export type TableViewOptionsPerTab = { [TabSlug]: TableViewOptions }; +export type CallNodeArea = 'NON_INVERTED_TREE' | 'INVERTED_TREE' | 'LOWER_WING'; + export type RightClickedCallNode = {| +threadsKey: ThreadsKey, + +area: CallNodeArea, +callNodePath: CallNodePath, |}; From adbd19e58bbe061593bb20ad049cc927e5a8a4c3 Mon Sep 17 00:00:00 2001 From: Markus Stange Date: Sat, 30 Nov 2024 20:15:04 -0500 Subject: [PATCH 24/25] Add .jj to various ignore files. --- .eslintignore | 1 + .prettierignore | 1 + 2 files changed, 2 insertions(+) diff --git a/.eslintignore b/.eslintignore index 8636cf776d..19019b154c 100644 --- a/.eslintignore +++ b/.eslintignore @@ -2,3 +2,4 @@ src/profile-logic/import/proto src/types/libdef/npm docs-user coverage +.jj diff --git a/.prettierignore b/.prettierignore index 4e1370a9db..2b64abfed0 100644 --- a/.prettierignore +++ b/.prettierignore @@ -7,3 +7,4 @@ dist coverage taskcluster/ .taskcluster.yml +.jj From 4d44cf7b50ff8d0498710ff9267fe12a3b180ff2 Mon Sep 17 00:00:00 2001 From: Markus Stange Date: Sat, 30 Nov 2024 20:31:59 -0500 Subject: [PATCH 25/25] Add the upper butterfly wing. --- src/actions/profile-view.js | 39 ++ .../calltree/ProfileFunctionListView.js | 3 +- src/components/calltree/UpperWing.js | 343 ++++++++++++++++ src/profile-logic/data-structures.js | 2 +- src/profile-logic/profile-data.js | 374 +++++++++++++++++- src/reducers/profile-view.js | 27 ++ src/selectors/per-thread/stack-sample.js | 115 ++++++ src/types/profile-derived.js | 2 +- src/types/state.js | 4 +- 9 files changed, 904 insertions(+), 5 deletions(-) create mode 100644 src/components/calltree/UpperWing.js diff --git a/src/actions/profile-view.js b/src/actions/profile-view.js index feaf498670..be4e02f37e 100644 --- a/src/actions/profile-view.js +++ b/src/actions/profile-view.js @@ -149,6 +149,21 @@ export function changeLowerWingSelectedCallNode( }; } +export function changeUpperWingSelectedCallNode( + threadsKey: ThreadsKey, + selectedCallNodePath: CallNodePath, + context: SelectionContext = { source: 'auto' } +): Action { + return { + type: 'CHANGE_SELECTED_CALL_NODE', + area: 'UPPER_WING', + selectedCallNodePath, + optionalExpandedToCallNodePath: [], + threadsKey, + context, + }; +} + /** * Select a function for a given thread in the function list. */ @@ -208,6 +223,18 @@ export function changeLowerWingRightClickedCallNode( }; } +export function changeUpperWingRightClickedCallNode( + threadsKey: ThreadsKey, + callNodePath: CallNodePath | null +) { + return { + type: 'CHANGE_RIGHT_CLICKED_CALL_NODE', + threadsKey, + area: 'UPPER_WING', + callNodePath, + }; +} + /** * Given a threadIndex and a sampleIndex, select the call node which carries the * sample's self time. In the inverted tree, this will be a root node. @@ -1710,6 +1737,18 @@ export function changeLowerWingExpandedCallNodes( }; } +export function changeUpperWingExpandedCallNodes( + threadsKey: ThreadsKey, + expandedCallNodePaths: Array +): Action { + return { + type: 'CHANGE_EXPANDED_CALL_NODES', + area: 'UPPER_WING', + threadsKey, + expandedCallNodePaths, + }; +} + export function changeSelectedMarker( threadsKey: ThreadsKey, selectedMarker: MarkerIndex | null, diff --git a/src/components/calltree/ProfileFunctionListView.js b/src/components/calltree/ProfileFunctionListView.js index 164ba232c6..ef22c4cc07 100644 --- a/src/components/calltree/ProfileFunctionListView.js +++ b/src/components/calltree/ProfileFunctionListView.js @@ -7,6 +7,7 @@ import React from 'react'; import SplitterLayout from 'react-splitter-layout'; import { FunctionList } from './FunctionList'; +import { UpperWing } from './UpperWing'; import { LowerWing } from './LowerWing'; import { StackSettings } from 'firefox-profiler/components/shared/StackSettings'; import { TransformNavigator } from 'firefox-profiler/components/shared/TransformNavigator'; @@ -26,7 +27,7 @@ export const ProfileFunctionListView = () => ( - + diff --git a/src/components/calltree/UpperWing.js b/src/components/calltree/UpperWing.js new file mode 100644 index 0000000000..5f07d5d71c --- /dev/null +++ b/src/components/calltree/UpperWing.js @@ -0,0 +1,343 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +// @flow + +import React, { PureComponent } from 'react'; +import memoize from 'memoize-immutable'; +import explicitConnect from 'firefox-profiler/utils/connect'; +import { TreeView } from 'firefox-profiler/components/shared/TreeView'; +import { CallTreeEmptyReasons } from './CallTreeEmptyReasons'; +import { + treeColumnsForTracingMs, + treeColumnsForSamples, + treeColumnsForBytes, +} from './columns'; +import { + getSearchStringsAsRegExp, + getSelectedThreadsKey, +} from 'firefox-profiler/selectors/url-state'; +import { + getScrollToSelectionGeneration, + getPreviewSelection, + getCategories, + getCurrentTableViewOptions, +} from 'firefox-profiler/selectors/profile'; +import { selectedThreadSelectors } from 'firefox-profiler/selectors/per-thread'; +import { + changeUpperWingSelectedCallNode, + changeUpperWingRightClickedCallNode, + changeUpperWingExpandedCallNodes, + addTransformToStack, + handleCallNodeTransformShortcut, + changeTableViewOptions, + updateBottomBoxContentsAndMaybeOpen, +} from 'firefox-profiler/actions/profile-view'; +import { assertExhaustiveCheck } from 'firefox-profiler/utils/flow'; + +import type { + State, + ThreadsKey, + CategoryList, + IndexIntoCallNodeTable, + CallNodeDisplayData, + WeightType, + TableViewOptions, + 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, + MaybeResizableColumn, +} from 'firefox-profiler/components/shared/TreeView'; +import type { ConnectedProps } from 'firefox-profiler/utils/connect'; + +import './CallTree.css'; + +type StateProps = {| + +threadsKey: ThreadsKey, + +scrollToSelectionGeneration: number, + +tree: CallTreeType, + +callNodeInfo: CallNodeInfo, + +categories: CategoryList, + +selectedCallNodeIndex: IndexIntoCallNodeTable | null, + +rightClickedCallNodeIndex: IndexIntoCallNodeTable | null, + +expandedCallNodeIndexes: Array, + +searchStringsRegExp: RegExp | null, + +disableOverscan: boolean, + +callNodeMaxDepthPlusOne: number, + +weightType: WeightType, + +tableViewOptions: TableViewOptions, +|}; + +type DispatchProps = {| + +changeUpperWingSelectedCallNode: typeof changeUpperWingSelectedCallNode, + +changeUpperWingRightClickedCallNode: typeof changeUpperWingRightClickedCallNode, + +changeUpperWingExpandedCallNodes: typeof changeUpperWingExpandedCallNodes, + +addTransformToStack: typeof addTransformToStack, + +handleCallNodeTransformShortcut: typeof handleCallNodeTransformShortcut, + +updateBottomBoxContentsAndMaybeOpen: typeof updateBottomBoxContentsAndMaybeOpen, + +onTableViewOptionsChange: (TableViewOptions) => any, +|}; + +type Props = ConnectedProps<{||}, StateProps, DispatchProps>; + +class UpperWingImpl extends PureComponent { + _mainColumn: Column = { + propName: 'name', + titleL10nId: '', + }; + _appendageColumn: Column = { + propName: 'lib', + titleL10nId: '', + }; + _treeView: TreeView | null = null; + _takeTreeViewRef = (treeView) => (this._treeView = treeView); + + /** + * Call Trees can have different types of "weights" for the data. Choose the + * appropriate labels for the call tree based on this weight. + */ + _weightTypeToColumns = memoize( + (weightType: WeightType): MaybeResizableColumn[] => { + switch (weightType) { + case 'tracing-ms': + return treeColumnsForTracingMs; + case 'samples': + return treeColumnsForSamples; + case 'bytes': + return treeColumnsForBytes; + default: + throw assertExhaustiveCheck(weightType, 'Unhandled WeightType.'); + } + }, + // Use a Map cache, as the function only takes one argument, which is a simple string. + { cache: new Map() } + ); + + componentDidMount() { + this.focus(); + this.maybeProcureInterestingInitialSelection(); + + if (this.props.selectedCallNodeIndex !== null && this._treeView) { + this._treeView.scrollSelectionIntoView(); + } + } + + componentDidUpdate(prevProps) { + this.maybeProcureInterestingInitialSelection(); + + if ( + this.props.selectedCallNodeIndex !== null && + this.props.scrollToSelectionGeneration > + prevProps.scrollToSelectionGeneration && + this._treeView + ) { + this._treeView.scrollSelectionIntoView(); + } + } + + focus() { + if (this._treeView) { + this._treeView.focus(); + } + } + + _onSelectedCallNodeChange = ( + newSelectedCallNode: IndexIntoCallNodeTable, + context: SelectionContext + ) => { + const { callNodeInfo, threadsKey, changeUpperWingSelectedCallNode } = + this.props; + changeUpperWingSelectedCallNode( + threadsKey, + callNodeInfo.getCallNodePathFromIndex(newSelectedCallNode), + context + ); + }; + + _onRightClickSelection = (newSelectedCallNode: IndexIntoCallNodeTable) => { + const { callNodeInfo, threadsKey, changeUpperWingRightClickedCallNode } = + this.props; + changeUpperWingRightClickedCallNode( + threadsKey, + callNodeInfo.getCallNodePathFromIndex(newSelectedCallNode) + ); + }; + + _onExpandedCallNodesChange = ( + newExpandedCallNodeIndexes: Array + ) => { + const { callNodeInfo, threadsKey, changeUpperWingExpandedCallNodes } = + this.props; + changeUpperWingExpandedCallNodes( + threadsKey, + newExpandedCallNodeIndexes.map((callNodeIndex) => + callNodeInfo.getCallNodePathFromIndex(callNodeIndex) + ) + ); + }; + + _onKeyDown = (event: SyntheticKeyboardEvent<>) => { + const { + selectedCallNodeIndex, + rightClickedCallNodeIndex, + handleCallNodeTransformShortcut, + threadsKey, + } = this.props; + const nodeIndex = + rightClickedCallNodeIndex !== null + ? rightClickedCallNodeIndex + : selectedCallNodeIndex; + if (nodeIndex === null) { + return; + } + handleCallNodeTransformShortcut(event, threadsKey, nodeIndex); + }; + + _onEnterOrDoubleClick = (nodeId: IndexIntoCallNodeTable) => { + const { tree, updateBottomBoxContentsAndMaybeOpen } = this.props; + const bottomBoxInfo = tree.getBottomBoxInfoForCallNode(nodeId); + updateBottomBoxContentsAndMaybeOpen('calltree', bottomBoxInfo); + }; + + maybeProcureInterestingInitialSelection() { + // Expand the heaviest callstack up to a certain depth and select the frame + // at that depth. + const { + tree, + expandedCallNodeIndexes, + selectedCallNodeIndex, + callNodeInfo, + categories, + } = this.props; + + if (selectedCallNodeIndex !== null || expandedCallNodeIndexes.length > 0) { + // Let's not change some existing state. + return; + } + + const idleCategoryIndex = categories.findIndex( + (category) => category.name === 'Idle' + ); + + const newExpandedCallNodeIndexes = expandedCallNodeIndexes.slice(); + const maxInterestingDepth = 17; // scientifically determined + let currentCallNodeIndex = tree.getRoots()[0]; + if (currentCallNodeIndex === undefined) { + // This tree is empty. + return; + } + newExpandedCallNodeIndexes.push(currentCallNodeIndex); + for (let i = 0; i < maxInterestingDepth; i++) { + const children = tree.getChildren(currentCallNodeIndex); + if (children.length === 0) { + break; + } + + // Let's find if there's a non idle children. + const firstNonIdleNode = children.find( + (nodeIndex) => + callNodeInfo.categoryForNode(nodeIndex) !== idleCategoryIndex + ); + + // If there's a non idle children, use it; otherwise use the first + // children (that will be idle). + currentCallNodeIndex = + firstNonIdleNode !== undefined ? firstNonIdleNode : children[0]; + newExpandedCallNodeIndexes.push(currentCallNodeIndex); + } + this._onExpandedCallNodesChange(newExpandedCallNodeIndexes); + + 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 + // this graph. Because this isn't probably what the average user wants we + // do it only when the category is something different. + this._onSelectedCallNodeChange(currentCallNodeIndex, { source: 'auto' }); + } + } + + render() { + const { + tree, + selectedCallNodeIndex, + rightClickedCallNodeIndex, + expandedCallNodeIndexes, + searchStringsRegExp, + disableOverscan, + callNodeMaxDepthPlusOne, + weightType, + tableViewOptions, + onTableViewOptionsChange, + } = this.props; + if (tree.getRoots().length === 0) { + return ; + } + return ( + + ); + } +} + +export const UpperWing = explicitConnect<{||}, StateProps, DispatchProps>({ + mapStateToProps: (state: State) => ({ + threadsKey: getSelectedThreadsKey(state), + scrollToSelectionGeneration: getScrollToSelectionGeneration(state), + tree: selectedThreadSelectors.getUpperWingCallTree(state), + callNodeInfo: selectedThreadSelectors.getUpperWingCallNodeInfo(state), + categories: getCategories(state), + selectedCallNodeIndex: + selectedThreadSelectors.getUpperWingSelectedCallNodeIndex(state), + rightClickedCallNodeIndex: + selectedThreadSelectors.getUpperWingRightClickedCallNodeIndex(state), + expandedCallNodeIndexes: + selectedThreadSelectors.getUpperWingExpandedCallNodeIndexes(state), + searchStringsRegExp: getSearchStringsAsRegExp(state), + disableOverscan: getPreviewSelection(state).isModifying, + // Use the filtered call node max depth, rather than the preview filtered call node + // max depth so that the width of the TreeView component is stable across preview + // selections. + callNodeMaxDepthPlusOne: + selectedThreadSelectors.getFilteredCallNodeMaxDepthPlusOne(state), + weightType: selectedThreadSelectors.getWeightTypeForCallTree(state), + tableViewOptions: getCurrentTableViewOptions(state), + }), + mapDispatchToProps: { + changeUpperWingSelectedCallNode, + changeUpperWingRightClickedCallNode, + changeUpperWingExpandedCallNodes, + addTransformToStack, + handleCallNodeTransformShortcut, + updateBottomBoxContentsAndMaybeOpen, + onTableViewOptionsChange: (options: TableViewOptions) => + changeTableViewOptions('function-list', options), + }, + component: UpperWingImpl, +}); diff --git a/src/profile-logic/data-structures.js b/src/profile-logic/data-structures.js index 53bcaf2e81..fa40419ec7 100644 --- a/src/profile-logic/data-structures.js +++ b/src/profile-logic/data-structures.js @@ -456,7 +456,7 @@ export function getEmptyCallNodeTable(): CallNodeTable { subcategory: new Int32Array(0), innerWindowID: new Float64Array(0), sourceFramesInlinedIntoSymbol: new Int32Array(0), - depth: [], + depth: new Int32Array(0), maxDepth: -1, length: 0, }; diff --git a/src/profile-logic/profile-data.js b/src/profile-logic/profile-data.js index 31c1614aa4..faa23a0674 100644 --- a/src/profile-logic/profile-data.js +++ b/src/profile-logic/profile-data.js @@ -347,7 +347,7 @@ function _createCallNodeTableFromUnorderedComponents( const subcategorySorted = new Int32Array(length); const innerWindowIDSorted = new Float64Array(length); const sourceFramesInlinedIntoSymbolSorted = new Int32Array(length); - const depthSorted = new Array(length); + const depthSorted = new Int32Array(length); let maxDepth = 0; // Traverse the entire tree, as follows: @@ -4382,3 +4382,375 @@ export function computeStackTableFromRawStackTable( length: rawStackTable.length, }; } + +export function createUpperWingCallNodeInfo( + callNodeInfo: CallNodeInfo, + selectedFunc: IndexIntoFuncTable | null, + defaultCategory: IndexIntoCategoryList +): CallNodeInfo { + const originalCallNodeTable = callNodeInfo.getNonInvertedCallNodeTable(); + const originalStackIndexToCallNodeIndex = + callNodeInfo.getStackIndexToNonInvertedCallNodeIndex(); + const { callNodeTable, stackIndexToCallNodeIndex } = + _computeSelectedFuncCallNodeTable( + selectedFunc, + originalCallNodeTable, + originalStackIndexToCallNodeIndex, + defaultCategory + ); + return new CallNodeInfoNonInverted(callNodeTable, stackIndexToCallNodeIndex); +} + +function _createSelectedFuncCallNodeTableStructure( + selectedFuncIndex: IndexIntoFuncTable, + callNodeTable: CallNodeTable +) { + const callNodeCount = callNodeTable.length; + const firstChildCol = new Int32Array(callNodeCount); + const lastChildCol = new Int32Array(callNodeCount); + const nextSiblingCol = new Int32Array(callNodeCount); + const childrenMayHaveChangedCol = new Uint8Array(callNodeCount); + let rootIndex = -1; // A single root whose func is the selected function + + const oldToNew = new Int32Array(callNodeCount); + + const funcCol = callNodeTable.func; + const prefixCol = callNodeTable.prefix; + const subtreeEndCol = callNodeTable.subtreeRangeEnd; + for (let i = 0; i < callNodeCount; i++) { + if (funcCol[i] !== selectedFuncIndex) { + oldToNew[i] = -1; + continue; + } + + if (rootIndex === -1) { + rootIndex = i; + firstChildCol[i] = -1; + lastChildCol[i] = -1; + nextSiblingCol[i] = -1; + oldToNew[i] = i; + } else { + childrenMayHaveChangedCol[rootIndex] = 1; + oldToNew[i] = rootIndex; + } + + // Call node i is the root of a subtree for the selected function. + const subtreeEnd = subtreeEndCol[i]; + + for (let j = i + 1; j < subtreeEnd; j++) { + let existingSiblingWithSameFunc = -1; + let newPrefix = -1; + const func = funcCol[j]; + if (func === selectedFuncIndex) { + // We found a deeper subtree of the selected func. + // Reparent it to the root. This is different than what the focus-function + // transform does; it's as if we combined focus-function with collapse-recursion. + existingSiblingWithSameFunc = rootIndex; + } else { + const prefix = prefixCol[j]; + // assert(prefix !== -1, "We're inside i's subtree, all nodes in this subtree have a prefix"); + newPrefix = oldToNew[prefix]; + const siblingsMayHaveChanged = childrenMayHaveChangedCol[newPrefix]; + + // Check if the parent has a child with our func + if (siblingsMayHaveChanged) { + // See if this node needs to combine with existing siblings. + for ( + let currentSibling = firstChildCol[newPrefix]; + currentSibling !== -1; + currentSibling = nextSiblingCol[currentSibling] + ) { + if (funcCol[currentSibling] === func) { + childrenMayHaveChangedCol[newPrefix] = 1; + existingSiblingWithSameFunc = currentSibling; + break; + } + } + } + } + + if (existingSiblingWithSameFunc !== -1) { + oldToNew[j] = existingSiblingWithSameFunc; + childrenMayHaveChangedCol[existingSiblingWithSameFunc] = 1; + continue; + } + + // Keep this node. + oldToNew[j] = j; + + const prevSibling = lastChildCol[newPrefix]; + if (prevSibling !== -1) { + nextSiblingCol[prevSibling] = j; + } else { + firstChildCol[newPrefix] = j; + } + lastChildCol[newPrefix] = j; + firstChildCol[j] = -1; + lastChildCol[j] = -1; + nextSiblingCol[j] = -1; + } + + i = subtreeEnd - 1; + } + + return { + firstChildCol, + nextSiblingCol, + oldToNew, + rootIndex, + }; +} + +function _computeSelectedFuncCallNodeTable( + selectedFuncIndex: IndexIntoFuncTable | null, + callNodeTable: CallNodeTable, + stackIndexToCallNodeIndex: Int32Array, + defaultCategory: IndexIntoCategoryList +): CallNodeTableAndStackMap { + if (selectedFuncIndex === null || callNodeTable.length === 0) { + return { + callNodeTable: getEmptyCallNodeTable(), + stackIndexToCallNodeIndex: new Int32Array(0), + }; + } + + const { firstChildCol, nextSiblingCol, oldToNew, rootIndex } = + _createSelectedFuncCallNodeTableStructure(selectedFuncIndex, callNodeTable); + + if (rootIndex === -1) { + return { + callNodeTable: getEmptyCallNodeTable(), + stackIndexToCallNodeIndex: new Int32Array(0), + }; + } + + const dfsOrder = _createDFSOrder(firstChildCol, nextSiblingCol, rootIndex); + + const newCallNodeTable = _rearrangeStuff( + dfsOrder, + callNodeTable, + oldToNew, + defaultCategory + ); + + const stackIndexToNewCallNodeIndex = _createUpdatedStackIndexToCallNodeIndex( + stackIndexToCallNodeIndex, + oldToNew, + dfsOrder.oldIndexToNewIndex + ); + + return { + callNodeTable: newCallNodeTable, + stackIndexToCallNodeIndex: stackIndexToNewCallNodeIndex, + }; +} + +type DFSOrder = {| + sortedLen: number, + oldIndexToNewIndex: Int32Array, + nextSiblingSorted: Int32Array, + subtreeRangeEndSorted: Uint32Array, + prefixSorted: Int32Array, + depthSorted: Int32Array, + maxDepth: number, +|}; + +function _createDFSOrder( + firstChild: Int32Array, + nextSibling: Int32Array, + firstRoot: number +): DFSOrder { + // Traverse the entire tree, as follows: + // 1. nextOldIndex is the next node in DFS order. Copy over all values from + // the unsorted columns into the sorted columns. + // 2. Find the next node in DFS order, set nextOldIndex to it, and continue + // to the next loop iteration. + const oldLen = firstChild.length; + const oldIndexToNewIndex = new Int32Array(firstChild.length); + oldIndexToNewIndex.fill(-1); + + const newNextSiblingCol = new Int32Array(oldLen); + const newSubtreeEndCol = new Uint32Array(oldLen); + + const prefixSorted = new Int32Array(oldLen); + const depthSorted = new Int32Array(oldLen); + let maxDepth = 0; + + let nextOldIndex = firstRoot; + let nextNewIndex = 0; + const oldIndexStack = []; + const newIndexStack = []; + outer: while (true) { + const oldIndex = nextOldIndex; + const newIndex = nextNewIndex++; + oldIndexToNewIndex[oldIndex] = newIndex; + + const depth = newIndexStack.length; + depthSorted[newIndex] = depth; + prefixSorted[newIndex] = depth === 0 ? -1 : newIndexStack[depth - 1]; + if (depth > maxDepth) { + maxDepth = depth; + } + + // Find the next index in DFS order: If we have children, then our first child + // is next. Otherwise, we need to advance to our next sibling, if we have one, + // otherwise to the next sibling of the first ancestor which has one. + const oldFirstChild = firstChild[oldIndex]; + if (oldFirstChild !== -1) { + // We have children. Our first child is the next node in DFS order. + oldIndexStack.push(oldIndex); + newIndexStack.push(newIndex); + nextOldIndex = oldFirstChild; + continue; + } + + // We have no children. If we have a next sibling, that's the next node. + newSubtreeEndCol[newIndex] = nextNewIndex; + const oldNextSibling = nextSibling[oldIndex]; + if (oldNextSibling !== -1) { + newNextSiblingCol[newIndex] = nextNewIndex; + nextOldIndex = oldNextSibling; + continue; + } + + // We have neither children nor a next sibling. We proceed with the next + // sibling of the closest ancestor that has one. + newNextSiblingCol[newIndex] = -1; + while (oldIndexStack.length !== 0) { + const oldAncestor = oldIndexStack.pop(); + const newAncestor = newIndexStack.pop(); + newSubtreeEndCol[newAncestor] = nextNewIndex; + const oldAncestorNextSibling = nextSibling[oldAncestor]; + if (oldAncestorNextSibling !== -1) { + newNextSiblingCol[newAncestor] = nextNewIndex; + nextOldIndex = oldAncestorNextSibling; + continue outer; + } + newNextSiblingCol[newAncestor] = -1; + } + + // No nodes left, we're done! + break; + } + + const sortedLen = nextNewIndex; + + return { + sortedLen, + oldIndexToNewIndex, + nextSiblingSorted: newNextSiblingCol.subarray(0, sortedLen), + subtreeRangeEndSorted: newSubtreeEndCol.subarray(0, sortedLen), + prefixSorted: prefixSorted.subarray(0, sortedLen), + depthSorted: depthSorted.subarray(0, sortedLen), + maxDepth, + }; +} + +function _rearrangeStuff( + dfsOrder: DFSOrder, + oldCallNodeTable: CallNodeTable, + originalOldToNew: Int32Array, + defaultCategory: IndexIntoCategoryList +): CallNodeTable { + const { + oldIndexToNewIndex, + sortedLen, + nextSiblingSorted, + subtreeRangeEndSorted, + prefixSorted, + depthSorted, + maxDepth, + } = dfsOrder; + + const oldCallNodeCount = oldCallNodeTable.length; + const { + func, + category, + subcategory, + innerWindowID, + sourceFramesInlinedIntoSymbol, + } = oldCallNodeTable; + const funcSorted = new Int32Array(sortedLen); + const categorySorted = new Int32Array(sortedLen); + const subcategorySorted = new Int32Array(sortedLen); + const innerWindowIDSorted = new Float64Array(sortedLen); + const sourceFramesInlinedIntoSymbolSorted = new Int32Array(sortedLen); + + const didInitializeAtNewIndex = new Uint8Array(sortedLen); + + for (let oldIndex = 0; oldIndex < oldCallNodeCount; oldIndex++) { + const midIndex = originalOldToNew[oldIndex]; + if (midIndex === -1) { + continue; + } + const newIndex = oldIndexToNewIndex[midIndex]; + + if (didInitializeAtNewIndex[newIndex] === 0) { + funcSorted[newIndex] = func[oldIndex]; + categorySorted[newIndex] = category[oldIndex]; + subcategorySorted[newIndex] = subcategory[oldIndex]; + innerWindowIDSorted[newIndex] = innerWindowID[oldIndex]; + sourceFramesInlinedIntoSymbolSorted[newIndex] = + sourceFramesInlinedIntoSymbol[oldIndex]; + + didInitializeAtNewIndex[newIndex] = 1; + } else { + // Resolve category conflicts, by resetting a conflicting subcategory or + // category to the default category. + if (category[oldIndex] !== categorySorted[newIndex]) { + // Conflicting origin stack categories -> default category + subcategory. + categorySorted[newIndex] = defaultCategory; + subcategorySorted[newIndex] = 0; + } else if (subcategory[oldIndex] !== subcategorySorted[newIndex]) { + // Conflicting origin stack subcategories -> "Other" subcategory. + subcategorySorted[newIndex] = 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 ( + sourceFramesInlinedIntoSymbol[oldIndex] !== + sourceFramesInlinedIntoSymbolSorted[newIndex] + ) { + // Conflicting inlining: -1. + sourceFramesInlinedIntoSymbolSorted[newIndex] = -1; + } + } + } + + const callNodeTable: CallNodeTable = { + prefix: prefixSorted, + subtreeRangeEnd: subtreeRangeEndSorted, + nextSibling: nextSiblingSorted, + func: funcSorted, + category: categorySorted, + subcategory: subcategorySorted, + innerWindowID: innerWindowIDSorted, + sourceFramesInlinedIntoSymbol: sourceFramesInlinedIntoSymbolSorted, + depth: depthSorted, + maxDepth, + length: sortedLen, + }; + + return callNodeTable; +} + +function _createUpdatedStackIndexToCallNodeIndex( + originalStackIndexToCallNodeIndex: Int32Array, + originalOldToNew: Int32Array, + oldIndexToNewIndex: Int32Array +): Int32Array { + const stackCount = originalStackIndexToCallNodeIndex.length; + const stackIndexToCallNodeIndex = new Int32Array(stackCount); + for (let i = 0; i < stackCount; i++) { + const oldIndex = originalStackIndexToCallNodeIndex[i]; + const midIndex = originalOldToNew[oldIndex]; + stackIndexToCallNodeIndex[i] = + midIndex !== -1 ? oldIndexToNewIndex[midIndex] : -1; + } + + return stackIndexToCallNodeIndex; +} diff --git a/src/reducers/profile-view.js b/src/reducers/profile-view.js index a7e25623c3..836319f392 100644 --- a/src/reducers/profile-view.js +++ b/src/reducers/profile-view.js @@ -157,9 +157,11 @@ export const defaultThreadViewOptions: ThreadViewOptions = { selectedNonInvertedCallNodePath: [], selectedInvertedCallNodePath: [], selectedLowerWingCallNodePath: [], + selectedUpperWingCallNodePath: [], expandedNonInvertedCallNodePaths: new PathSet(), expandedInvertedCallNodePaths: new PathSet(), expandedLowerWingCallNodePaths: new PathSet(), + expandedUpperWingCallNodePaths: new PathSet(), selectedFunctionIndex: null, selectedMarker: null, selectedNetworkMarker: null, @@ -257,6 +259,7 @@ const viewOptionsPerThread: Reducer = ( INVERTED_TREE: threadState.selectedInvertedCallNodePath, NON_INVERTED_TREE: threadState.selectedNonInvertedCallNodePath, LOWER_WING: threadState.selectedLowerWingCallNodePath, + UPPER_WING: threadState.selectedUpperWingCallNodePath, }[area]; // If the selected node doesn't actually change, let's return the previous @@ -272,6 +275,7 @@ const viewOptionsPerThread: Reducer = ( INVERTED_TREE: threadState.expandedInvertedCallNodePaths, NON_INVERTED_TREE: threadState.expandedNonInvertedCallNodePaths, LOWER_WING: threadState.expandedLowerWingCallNodePaths, + UPPER_WING: threadState.expandedUpperWingCallNodePaths, }[area]; const expandToNode = optionalExpandedToCallNodePath @@ -313,6 +317,11 @@ const viewOptionsPerThread: Reducer = ( selectedLowerWingCallNodePath: selectedCallNodePath, expandedLowerWingCallNodePaths: expandedCallNodePaths, }); + case 'UPPER_WING': + return _updateThreadViewOptions(state, threadsKey, { + selectedUpperWingCallNodePath: selectedCallNodePath, + expandedUpperWingCallNodePaths: expandedCallNodePaths, + }); default: throw assertExhaustiveCheck(area, 'Unhandled case'); } @@ -330,6 +339,20 @@ const viewOptionsPerThread: Reducer = ( return state; } + if (selectedFunctionIndex !== null) { + return _updateThreadViewOptions(state, threadsKey, { + selectedFunctionIndex, + selectedLowerWingCallNodePath: [selectedFunctionIndex], + expandedLowerWingCallNodePaths: new PathSet([ + [selectedFunctionIndex], + ]), + selectedUpperWingCallNodePath: [selectedFunctionIndex], + expandedUpperWingCallNodePaths: new PathSet([ + [selectedFunctionIndex], + ]), + }); + } + return _updateThreadViewOptions(state, threadsKey, { selectedFunctionIndex, }); @@ -384,6 +407,10 @@ const viewOptionsPerThread: Reducer = ( return _updateThreadViewOptions(state, threadsKey, { expandedLowerWingCallNodePaths: expandedCallNodePaths, }); + case 'UPPER_WING': + return _updateThreadViewOptions(state, threadsKey, { + expandedUpperWingCallNodePaths: expandedCallNodePaths, + }); default: throw assertExhaustiveCheck(area, 'Unhandled case'); } diff --git a/src/selectors/per-thread/stack-sample.js b/src/selectors/per-thread/stack-sample.js index 822ee47107..0120bcf9aa 100644 --- a/src/selectors/per-thread/stack-sample.js +++ b/src/selectors/per-thread/stack-sample.js @@ -223,6 +223,13 @@ export function getStackAndSampleSelectorsPerThread( } ); + const getUpperWingCallNodeInfo: Selector = createSelector( + _getNonInvertedCallNodeInfo, + getSelectedFunctionIndex, + ProfileSelectors.getDefaultCategory, + ProfileData.createUpperWingCallNodeInfo + ); + const getLowerWingSelectedCallNodePath: Selector = createSelector( threadSelectors.getViewOptions, @@ -230,6 +237,13 @@ export function getStackAndSampleSelectorsPerThread( threadViewOptions.selectedLowerWingCallNodePath ); + const getUpperWingSelectedCallNodePath: Selector = + createSelector( + threadSelectors.getViewOptions, + (threadViewOptions): CallNodePath => + threadViewOptions.selectedUpperWingCallNodePath + ); + const getSelectedCallNodeIndex: Selector = createSelector( getCallNodeInfo, @@ -248,6 +262,15 @@ export function getStackAndSampleSelectorsPerThread( } ); + const getUpperWingSelectedCallNodeIndex: Selector = + createSelector( + getUpperWingCallNodeInfo, + getUpperWingSelectedCallNodePath, + (callNodeInfo, callNodePath) => { + return callNodeInfo.getCallNodeIndexFromPath(callNodePath); + } + ); + const getExpandedCallNodePaths: Selector = createSelector( threadSelectors.getViewOptions, UrlState.getInvertCallstack, @@ -262,6 +285,11 @@ export function getStackAndSampleSelectorsPerThread( (threadViewOptions) => threadViewOptions.expandedLowerWingCallNodePaths ); + const getUpperWingExpandedCallNodePaths: Selector = createSelector( + threadSelectors.getViewOptions, + (threadViewOptions) => threadViewOptions.expandedUpperWingCallNodePaths + ); + const getExpandedCallNodeIndexes: Selector< Array, > = createSelector( @@ -284,6 +312,17 @@ export function getStackAndSampleSelectorsPerThread( ) ); + const getUpperWingExpandedCallNodeIndexes: Selector< + Array, + > = createSelector( + getUpperWingCallNodeInfo, + getUpperWingExpandedCallNodePaths, + (callNodeInfo, callNodePaths) => + Array.from(callNodePaths).map((path) => + callNodeInfo.getCallNodeIndexFromPath(path) + ) + ); + const _getPreviewFilteredCtssSampleIndexToNonInvertedCallNodeIndex: Selector< Array, > = createSelector( @@ -292,6 +331,26 @@ export function getStackAndSampleSelectorsPerThread( ProfileData.getSampleIndexToCallNodeIndex ); + const _getPreviewFilteredCtssSampleIndexToUpperWingCallNodeIndex: Selector< + Array, + > = createSelector( + (state) => threadSelectors.getPreviewFilteredCtssSamples(state).stack, + (state) => + getUpperWingCallNodeInfo(state).getStackIndexToNonInvertedCallNodeIndex(), + (sampleStacks, stackIndexToCallNodeIndex) => { + return sampleStacks.map((stackIndex) => { + if (stackIndex === null) { + return null; + } + const callNodeIndex = stackIndexToCallNodeIndex[stackIndex]; + if (callNodeIndex === -1) { + return null; + } + return callNodeIndex; + }); + } + ); + const getSampleIndexToNonInvertedCallNodeIndexForFilteredThread: Selector< Array, > = createSelector( @@ -378,6 +437,20 @@ export function getStackAndSampleSelectorsPerThread( } ); + const getUpperWingCallNodeSelfAndSummary: Selector = + createSelector( + threadSelectors.getPreviewFilteredCtssSamples, + _getPreviewFilteredCtssSampleIndexToUpperWingCallNodeIndex, + getUpperWingCallNodeInfo, + (samples, sampleIndexToCallNodeIndex, callNodeInfo) => { + return CallTree.computeCallNodeSelfAndSummary( + samples, + sampleIndexToCallNodeIndex, + callNodeInfo.getNonInvertedCallNodeTable().length + ); + } + ); + const getCallTreeTimings: Selector = createSelector( getCallNodeInfo, getCallNodeSelfAndSummary, @@ -392,6 +465,13 @@ export function getStackAndSampleSelectorsPerThread( CallTree.computeLowerWingTimings ); + const _getUpperWingCallTreeTimings: Selector = + createSelector( + getUpperWingCallNodeInfo, + getUpperWingCallNodeSelfAndSummary, + CallTree.computeCallTreeTimings + ); + const getCallTreeTimingsNonInverted: Selector = createSelector( getCallNodeInfo, @@ -434,6 +514,15 @@ export function getStackAndSampleSelectorsPerThread( CallTree.getFunctionListTree ); + const getUpperWingCallTree: Selector = createSelector( + threadSelectors.getPreviewFilteredThread, + getUpperWingCallNodeInfo, + ProfileSelectors.getCategories, + _getUpperWingCallTreeTimings, + getWeightTypeForCallTree, + CallTree.getCallTree + ); + const getLowerWingCallTree: Selector = createSelector( threadSelectors.getPreviewFilteredThread, getLowerWingCallNodeInfo, @@ -564,6 +653,25 @@ export function getStackAndSampleSelectorsPerThread( } ); + const getUpperWingRightClickedCallNodeIndex: Selector = + createSelector( + getRightClickedCallNodeInfo, + getCallNodeInfo, + (rightClickedCallNodeInfo, callNodeInfo) => { + if ( + rightClickedCallNodeInfo !== null && + rightClickedCallNodeInfo.threadsKey === threadsKey && + rightClickedCallNodeInfo.area === 'UPPER_WING' + ) { + return callNodeInfo.getCallNodeIndexFromPath( + rightClickedCallNodeInfo.callNodePath + ); + } + + return null; + } + ); + const getRightClickedFunctionIndex: Selector = createSelector( ProfileSelectors.getProfileViewOptions, @@ -586,6 +694,7 @@ export function getStackAndSampleSelectorsPerThread( getWeightTypeForCallTree, getCallNodeInfo, getLowerWingCallNodeInfo, + getUpperWingCallNodeInfo, getSourceViewStackLineInfo, getAssemblyViewNativeSymbolIndex, getAssemblyViewStackAddressInfo, @@ -593,17 +702,22 @@ export function getStackAndSampleSelectorsPerThread( getSelectedCallNodeIndex, getLowerWingSelectedCallNodePath, getLowerWingSelectedCallNodeIndex, + getUpperWingSelectedCallNodePath, + getUpperWingSelectedCallNodeIndex, getSelectedFunctionIndex, getExpandedCallNodePaths, getExpandedCallNodeIndexes, getLowerWingExpandedCallNodePaths, getLowerWingExpandedCallNodeIndexes, + getUpperWingExpandedCallNodePaths, + getUpperWingExpandedCallNodeIndexes, getSampleIndexToNonInvertedCallNodeIndexForFilteredThread, getSamplesSelectedStatesInFilteredThread, getTreeOrderComparatorInFilteredThread, getCallTree, getFunctionListTree, getLowerWingCallTree, + getUpperWingCallTree, getSourceViewLineTimings, getAssemblyViewAddressTimings, getTracedTiming, @@ -614,5 +728,6 @@ export function getStackAndSampleSelectorsPerThread( getRightClickedCallNodeIndex, getRightClickedFunctionIndex, getLowerWingRightClickedCallNodeIndex, + getUpperWingRightClickedCallNodeIndex, }; } diff --git a/src/types/profile-derived.js b/src/types/profile-derived.js index 3ab3d2f780..f288e4da44 100644 --- a/src/types/profile-derived.js +++ b/src/types/profile-derived.js @@ -297,7 +297,7 @@ export type CallNodeTable = { // -2: no inlining sourceFramesInlinedIntoSymbol: Int32Array, // The depth of the call node. Roots have depth 0. - depth: number[], + depth: Int32Array, // The maximum value in the depth column, or -1 if this table is empty. maxDepth: number, // The number of call nodes. All columns in this table have this length. diff --git a/src/types/state.js b/src/types/state.js index 5e43d754e8..42a6963ffd 100644 --- a/src/types/state.js +++ b/src/types/state.js @@ -64,6 +64,8 @@ export type ThreadViewOptions = {| +selectedFunctionIndex: IndexIntoFuncTable | null, +selectedLowerWingCallNodePath: CallNodePath, +expandedLowerWingCallNodePaths: PathSet, + +selectedUpperWingCallNodePath: CallNodePath, + +expandedUpperWingCallNodePaths: PathSet, +selectedMarker: MarkerIndex | null, +selectedNetworkMarker: MarkerIndex | null, |}; @@ -76,7 +78,7 @@ export type TableViewOptions = {| export type TableViewOptionsPerTab = { [TabSlug]: TableViewOptions }; -export type CallNodeArea = 'NON_INVERTED_TREE' | 'INVERTED_TREE' | 'LOWER_WING'; +export type CallNodeArea = 'NON_INVERTED_TREE' | 'INVERTED_TREE' | 'LOWER_WING' | 'UPPER_WING'; export type RightClickedCallNode = {| +threadsKey: ThreadsKey,