-
Notifications
You must be signed in to change notification settings - Fork 462
Expand file tree
/
Copy pathflame-graph.ts
More file actions
309 lines (276 loc) · 12 KB
/
flame-graph.ts
File metadata and controls
309 lines (276 loc) · 12 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
/* 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/. */
import type {
UnitIntervalOfProfileRange,
CallNodeTable,
FuncTable,
IndexIntoCallNodeTable,
} from 'firefox-profiler/types';
import type { StringTable } from 'firefox-profiler/utils/string-table';
import type { CallTreeTimingsNonInverted } from './call-tree';
import { bisectionRightByStrKey } from 'firefox-profiler/utils/bisect';
export type FlameGraphDepth = number;
export type IndexIntoFlameGraphTiming = number;
/**
* FlameGraphTiming is an array containing data used for rendering the
* flame graph. Each element in the array describes one row in the
* graph. Each such element in turn contains one or more functions,
* drawn as boxes with start and end positions, represented as unit
* intervals of the profile range. It should be noted that start and
* end does not represent units of time, but only positions on the
* x-axis, derived from an alphabetical sort.
*
* callNode allows extracting information such as function names which
* are shown in the flame graph.
*
* selfRelative contains the self time relative to the total time,
* which is used to color the drawn functions.
*/
export type FlameGraphTiming = Array<{
start: UnitIntervalOfProfileRange[];
end: UnitIntervalOfProfileRange[];
selfRelative: Array<number>;
callNode: IndexIntoCallNodeTable[];
length: number;
}>;
/**
* FlameGraphRows is an array of rows, where each row is an array of call node
* indexes. This is a timing-invariant representation of the flame graph and can
* be cached independently of the sample / timing information.
*
* When combined with the timing information, it is used to produce FlameGraphTiming.
*
* In FlameGraphRows, rows[depth] contains all the call nodes of that depth.
* The call nodes are ordered in the same order that they'll be displayed in the
* flame graph:
*
* - Siblings are ordered by function name.
* - Siblings are grouped, i.e. all nodes with the same prefix are next to each other.
* - The order of these groups with respect to each other is determined by how
* their prefix call nodes are ordered in the previous row.
*
* # Example ([call node index] [function name])
*
* ```
* - 0 A
* - 1 D
* - 2 I
* - 3 B
* - 4 C
* - 5 F
* - 6 E
* - 7 G
* - 8 H
* ```
*
* The call node table above produces the following FlameGraphRows:
* Depth 0: [0, 8] // ["A", "H"]
* Depth 1: [3, 1, 7] // ["B", "D", "G"]
* Depth 2: [4, 6, 5, 2] // ["C", "E", "F", "I"]
*
* Note that [4, 6, 5] are the children of 3 ("B"), sorted by name, and these
* children have been moved before 2 ("I") to match the order of their parents.
*/
export type FlameGraphRows = IndexIntoCallNodeTable[][];
/**
* Compute the FlameGraphRows. The result is independent of timing information.
*/
export function computeFlameGraphRows(
callNodeTable: CallNodeTable,
funcTable: FuncTable,
stringTable: StringTable
): FlameGraphRows {
if (callNodeTable.length === 0) {
return [[]];
}
const { func, nextSibling, subtreeRangeEnd, maxDepth } = callNodeTable;
const funcTableNameColumn = funcTable.name;
// flameGraphRows is what we'll return from this function.
//
// Each row is conceptually partitioned into two parts: "Finished nodes" and
// "pending nodes".
//
// For row d, flameGraphRows[d] is partitioned as follows (a..b is the half-open
// range which includes a but excludes b):
//
// - flameGraphRows[d][0..pendingRangeStartAtDepth[d]] are "finished"
// - flameGraphRows[d][pendingRangeStartAtDepth[d]..] are "pending"
//
// A node starts out as "pending" when we initially add it to the row.
// A node becomes "finished" once we've decided to process its children.
//
// This is used to queue up a bunch of siblings before we process their
// children.
// We need to queue up nodes before we can process their children because
// we can only process children once their parents are in the right order.
const flameGraphRows: FlameGraphRows = Array.from(
{ length: maxDepth + 1 },
() => []
);
const pendingRangeStartAtDepth = new Int32Array(maxDepth + 1);
// At the beginning of each turn of this loop, add currentCallNode and all its
// siblings as "pending" to row[currentDepth], ordered by name. Then find the
// first pending call node with children, and go to the next iteration.
let currentCallNode = 0;
let currentDepth = 0; // always set to depth[currentCallNode]
outer: while (true) {
// assert(depth[currentCallNode] === currentDepth);
// Add currentCallNode and all its siblings to the current row. Ensure correct
// ordering when inserting each sibling.
const rowAtThisDepth = flameGraphRows[currentDepth];
const siblingIndexRangeStart = rowAtThisDepth.length; // index into rowAtThisDepth
for (
let currentSibling = currentCallNode;
currentSibling !== -1;
currentSibling = nextSibling[currentSibling]
) {
const siblingIndexRangeEnd = rowAtThisDepth.length;
if (siblingIndexRangeStart === siblingIndexRangeEnd) {
// This is the first sibling that we see. We don't need to compute an
// insertion index because we don't have any other siblings to compare
// to yet.
rowAtThisDepth.push(currentSibling);
} else {
// There are other siblings already present in rowAtThisDepth[siblingIndexRangeStart..].
// Do an ordered insert, to keep siblings ordered by function name.
// assert(siblingIndexRangeStart < siblingIndexRangeEnd)
const thisFunc = func[currentSibling];
const funcName = stringTable.getString(funcTableNameColumn[thisFunc]);
const insertionIndex = bisectionRightByStrKey(
rowAtThisDepth,
funcName,
(cn) => stringTable.getString(funcTableNameColumn[func[cn]]),
siblingIndexRangeStart,
siblingIndexRangeEnd
);
rowAtThisDepth.splice(insertionIndex, 0, currentSibling);
}
}
// Now currentCallNode and all its siblings have been added to the row, and
// they are ordered correctly. They are all marked as pending;
// pendingRangeStartAtDepth has not been advanced.
// In the remainder of this loop iteration, all we'll be doing is to find
// the next node for processing. Starting at the current depth, but going to
// to more shallow depths if needed, we want to find the first pending node
// which has children.
// We know that the current row has at least one remaining pending node
// (currentCallNode) so we start with this row.
let candidateDepth = currentDepth;
let candidateRow = rowAtThisDepth;
let indexInCandidateRow = pendingRangeStartAtDepth[candidateDepth];
let candidateNode = candidateRow[indexInCandidateRow];
// candidateNode may not have any children. Keep searching, in this row and
// in more shallow rows, until we find a node which does have children.
// At the end of this loop, candidateNode will be set to a node which has
// children, and the following will be true:
// candidateNode === flameGraphRows[candidateDepth][pendingRangeStartAtDepth[candidateDepth]]
//
// "while (!hasChildren(candidateNode))"
while (subtreeRangeEnd[candidateNode] === candidateNode + 1) {
// candidateNode does not have any children.
// "Finish" candidateNode by incrementing pendingRangeStartAtDepth[candidateDepth].
indexInCandidateRow++;
pendingRangeStartAtDepth[candidateDepth] = indexInCandidateRow;
// Find the next row which still has pending nodes, going to shallower
// depths until we hit the end.
while (indexInCandidateRow === candidateRow.length) {
// There are no more pending nodes in the current row - all nodes at
// this depth are already finished.
if (candidateDepth === 0) {
// We must have processed the entire tree at this point, and we are done.
break outer;
}
// Go to a shallower depth and continue the search there.
candidateDepth--;
candidateRow = flameGraphRows[candidateDepth];
indexInCandidateRow = pendingRangeStartAtDepth[candidateDepth];
}
// candidateRow now has at least one pending node left.
candidateNode = candidateRow[indexInCandidateRow];
}
// Now candidateNode is a pending node which has at least one child.
// assert(candidateNode === flameGraphRows[candidateDepth][pendingRangeStartAtDepth[candidateDepth]])
// assert(subtreeRangeEnd[candidateNode] !== candidateNode + 1)
// We have now decided to process this node, i.e. we know that we will add
// this node's children in the next loop iteration.
// "Finish" candidateNode by incrementing pendingRangeStartAtDepth[candidateDepth].
pendingRangeStartAtDepth[candidateDepth] = indexInCandidateRow + 1;
// Advance to candidateNode's first child. Due to the way call nodes are ordered,
// the first child of x (if present) is always at x + 1.
currentCallNode = candidateNode + 1; // "currentCallNode = firstChild[candidateNode];"
currentDepth = candidateDepth + 1;
}
return flameGraphRows;
}
/**
* Build a FlameGraphTiming table from a call tree.
*/
export function getFlameGraphTiming(
flameGraphRows: FlameGraphRows,
callNodeTable: CallNodeTable,
callTreeTimings: CallTreeTimingsNonInverted
): FlameGraphTiming {
const { total, self, rootTotalSummary } = callTreeTimings;
const { prefix } = callNodeTable;
// This is where we build up the return value, one row at a time.
const timing = [];
// This is used to adjust the start position of a call node's box based on the
// start position of its prefix node's box.
const startPerCallNode = new Float32Array(callNodeTable.length);
// Workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=1858310
const abs = Math.abs;
// Go row by row.
for (let depth = 0; depth < flameGraphRows.length; depth++) {
const rowNodes = flameGraphRows[depth];
const start = [];
const end = [];
const selfRelative = [];
const timingCallNodes = [];
// Process the call nodes in this row. Sibling boxes are adjacent to each other.
// Whenever the prefix changes, we need to add a gap so that the child boxes
// start at the same position as the parent box.
//
// Previous row: [B ][D ] [G ]
// Current row: [C][E][F] [I ]
// (Note that this is upside down from how the flame graph is usually displayed)
let currentStart = 0;
let previousPrefixCallNode = -1;
for (let indexInRow = 0; indexInRow < rowNodes.length; indexInRow++) {
const nodeIndex = rowNodes[indexInRow];
const totalVal = total[nodeIndex];
if (totalVal === 0) {
// Skip boxes with zero width.
continue;
}
const nodePrefix = prefix[nodeIndex];
if (nodePrefix !== previousPrefixCallNode) {
// We have advanced to a node with a different parent, so we need to
// jump ahead to the parent box's start position.
currentStart = startPerCallNode[nodePrefix];
previousPrefixCallNode = nodePrefix;
}
// Write down the start position of this call node so that it can be
// checked later by this node's children.
startPerCallNode[nodeIndex] = currentStart;
// Take the absolute value, as native deallocations can be negative.
const totalRelativeVal = abs(totalVal / rootTotalSummary);
const selfRelativeVal = abs(self[nodeIndex] / rootTotalSummary);
const currentEnd = currentStart + totalRelativeVal;
start.push(currentStart);
end.push(currentEnd);
selfRelative.push(selfRelativeVal);
timingCallNodes.push(nodeIndex);
// The start position of the next box is the end position of the current box.
currentStart = currentEnd;
}
timing[depth] = {
start,
end,
selfRelative,
callNode: timingCallNodes,
length: timingCallNodes.length,
};
}
return timing;
}