Skip to content

Commit

Permalink
Optimize handling of roots.
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
mstange committed Aug 7, 2024
1 parent b18ef2d commit 1dc1d54
Show file tree
Hide file tree
Showing 3 changed files with 53 additions and 67 deletions.
28 changes: 11 additions & 17 deletions src/profile-logic/call-node-info.js
Original file line number Diff line number Diff line change
Expand Up @@ -449,13 +449,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<SuffixOrderIndex, IndexIntoNonInvertedCallNodeTable>.
// It lists the non-inverted call nodes in "suffix order", i.e. ordered by
// comparing their call paths from back to front.
Expand Down Expand Up @@ -497,16 +500,7 @@ export class CallNodeInfoInvertedImpl implements CallNodeInfoInverted {
this._suffixOrderedCallNodes = suffixOrderedCallNodes;
this._suffixOrderIndexes = suffixOrderIndexes;
this._defaultCategory = defaultCategory;

const rootCount = rootSuffixOrderIndexRangeEndCol.length;
this._rootCount = rootCount;

const roots = new Array(rootCount);
for (let i = 0; i < rootCount; i++) {
roots[i] = i;
}
this._roots = roots;

this._rootCount = rootSuffixOrderIndexRangeEndCol.length;
this._funcCountBuf = funcCountBuf;
this._funcCountBuf.fill(0);
const invertedRootCallNodeTable = _createInvertedRootCallNodeTable(
Expand Down Expand Up @@ -544,8 +538,8 @@ export class CallNodeInfoInvertedImpl implements CallNodeInfoInverted {
return this._suffixOrderIndexes;
}

getRoots(): Array<InvertedCallNodeHandle> {
return this._roots;
getFuncCount(): number {
return this._rootCount;
}

isRoot(nodeHandle: InvertedCallNodeHandle): boolean {
Expand Down
60 changes: 25 additions & 35 deletions src/profile-logic/call-tree.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,8 @@ export type CallTreeTimingsInverted = {|
callNodeSelf: Float32Array,
rootTotalSummary: number,
sortedRoots: IndexIntoFuncTable[],
totalPerRootNode: Map<IndexIntoCallNodeTable, number>,
rootNodesWithChildren: Set<IndexIntoCallNodeTable>,
totalPerRootFunc: Float32Array,
hasChildrenPerRootFunc: Uint8Array,
|};

export type CallTreeTimings =
Expand Down Expand Up @@ -185,8 +185,8 @@ class CallTreeInternalInverted implements CallTreeInternal {
_callNodeSelf: Float32Array;
_rootNodes: IndexIntoCallNodeTable[];
_funcCount: number;
_totalPerRootNode: Map<IndexIntoCallNodeTable, number>;
_rootNodesWithChildren: Set<IndexIntoCallNodeTable>;
_totalPerRootFunc: Float32Array;
_hasChildrenPerRootFunc: Uint8Array;
_totalAndHasChildrenPerNonRootNode: Map<
IndexIntoCallNodeTable,
TotalAndHasChildren,
Expand All @@ -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;
}

Expand All @@ -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;
}
Expand All @@ -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);
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -693,13 +693,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) {
Expand All @@ -710,36 +711,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,
};
}

Expand Down
32 changes: 17 additions & 15 deletions src/types/profile-derived.js
Original file line number Diff line number Diff line change
Expand Up @@ -162,9 +162,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;

Expand Down Expand Up @@ -283,17 +280,17 @@ 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)
* - [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)
* - [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)
* - [in7] A (so:4..5) = B <- A <- A = ... A -> A -> B (cn5)
* - [in2] 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
Expand All @@ -308,14 +305,19 @@ export type SuffixOrderIndex = number;
* 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)
* Suffix order indexes: [0, 3, 2, 6, 1, 4, 5] (cnX -> soY)
*
* cnX: Non-inverted call node index X
* soX: Suffix order index X
* inX: Inverted call node index X
* so:X..Y: Suffix order index range soX..soY (soY excluded)
*/
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.
Expand Down

0 comments on commit 1dc1d54

Please sign in to comment.