-
Notifications
You must be signed in to change notification settings - Fork 463
Expand file tree
/
Copy pathcall-node-info.ts
More file actions
1718 lines (1568 loc) · 69 KB
/
call-node-info.ts
File metadata and controls
1718 lines (1568 loc) · 69 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
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/* 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 {
hashPath,
concatHash,
hashPathSingleFunc,
} from 'firefox-profiler/utils/path';
import { ensureExists } from '../utils/types';
import { bisectionRightByKey } from '../utils/bisect';
import type {
IndexIntoFuncTable,
CallNodeTable,
CallNodePath,
IndexIntoCallNodeTable,
IndexIntoCategoryList,
IndexIntoNativeSymbolTable,
IndexIntoSubcategoryListForCategory,
InnerWindowID,
} from 'firefox-profiler/types';
/**
* 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().
getCallNodeTable(): 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<IndexIntoStackTable, IndexIntoCallNodeTable | -1>.
//
// 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;
// These functions return various properties about each node. You could also
// get these properties from the call node table, but that only works if the
// call node is a non-inverted call node (because we only have a non-inverted
// call node table). If your code is generic over inverted / non-inverted mode,
// and you just have a IndexIntoCallNodeTable and a CallNodeInfo instance,
// call the functions below.
prefixForNode(
callNodeIndex: IndexIntoCallNodeTable
): IndexIntoCallNodeTable | -1;
funcForNode(callNodeIndex: IndexIntoCallNodeTable): IndexIntoFuncTable;
categoryForNode(callNodeIndex: IndexIntoCallNodeTable): IndexIntoCategoryList;
subcategoryForNode(
callNodeIndex: IndexIntoCallNodeTable
): IndexIntoCategoryList;
innerWindowIDForNode(
callNodeIndex: IndexIntoCallNodeTable
): IndexIntoCategoryList;
depthForNode(callNodeIndex: IndexIntoCallNodeTable): number;
sourceFramesInlinedIntoSymbolForNode(
callNodeIndex: IndexIntoCallNodeTable
): IndexIntoNativeSymbolTable | -1 | -2;
}
/**
* The implementation of the CallNodeInfo interface for the non-inverted tree.
*/
export class CallNodeInfoNonInverted implements CallNodeInfo {
// The call node table. (always non-inverted)
_callNodeTable: CallNodeTable;
// The mapping of stack index to corresponding non-inverted call node index.
_stackIndexToNonInvertedCallNodeIndex: Int32Array;
// This is a Map<CallNodePathHash, IndexIntoCallNodeTable>. This map speeds up
// the look-up process by caching every CallNodePath we handle which avoids
// looking up parents again and again.
_cache: Map<string, IndexIntoCallNodeTable> = new Map();
constructor(
callNodeTable: CallNodeTable,
stackIndexToNonInvertedCallNodeIndex: Int32Array
) {
this._callNodeTable = callNodeTable;
this._stackIndexToNonInvertedCallNodeIndex =
stackIndexToNonInvertedCallNodeIndex;
}
isInverted(): boolean {
return false;
}
asInverted(): CallNodeInfoInverted | null {
return null;
}
getCallNodeTable(): CallNodeTable {
return this._callNodeTable;
}
getStackIndexToNonInvertedCallNodeIndex(): Int32Array {
return this._stackIndexToNonInvertedCallNodeIndex;
}
getCallNodePathFromIndex(
callNodeIndex: IndexIntoCallNodeTable | null
): CallNodePath {
if (callNodeIndex === null || callNodeIndex === -1) {
return [];
}
const callNodePath = [];
let cni = callNodeIndex;
while (cni !== -1) {
callNodePath.push(this._callNodeTable.func[cni]);
cni = this._callNodeTable.prefix[cni];
}
callNodePath.reverse();
return callNodePath;
}
getCallNodeIndexFromPath(
callNodePath: CallNodePath
): IndexIntoCallNodeTable | null {
const cache = this._cache;
const hashFullPath = hashPath(callNodePath);
const result = cache.get(hashFullPath);
if (result !== undefined) {
// The cache already has the result for the full path.
return result;
}
// This array serves as a map and stores the hashes of callNodePath's
// parents to speed up the algorithm. First we'll follow the tree from the
// bottom towards the top, pushing hashes as we compute them, and then we'll
// move back towards the bottom popping hashes from this array.
const sliceHashes = [hashFullPath];
// Step 1: find whether we already computed the index for one of the path's
// parents, starting from the closest parent and looping towards the "top" of
// the tree.
// If we find it for one of the parents, we'll be able to start at this point
// in the following look up.
let i = callNodePath.length;
let index;
while (--i > 0) {
// Looking up each parent for this call node, starting from the deepest node.
// If we find a parent this makes it possible to start the look up from this location.
const subPath = callNodePath.slice(0, i);
const hash = hashPath(subPath);
index = cache.get(hash);
if (index !== undefined) {
// Yay, we already have the result for a parent!
break;
}
// Cache the hashed value because we'll need it later, after resolving this path.
// Note we don't add the hash if we found the parent in the cache, so the
// last added element here will accordingly be the first popped in the next
// algorithm.
sliceHashes.push(hash);
}
// Step 2: look for the requested path using the call node table, starting at
// the parent we already know if we found one, and looping down the tree.
// We're contributing to the cache at the same time.
// `index` is undefined if no parent was found in the cache. In that case we
// start from the start, and use `-1` which is the prefix we use to indicate
// the root node.
if (index === undefined) {
// assert(i === 0);
index = -1;
}
while (i < callNodePath.length) {
// Resolving the index for subpath `callNodePath.slice(0, i+1)` given we
// know the index for the subpath `callNodePath.slice(0, i)` (its parent).
const func = callNodePath[i];
const nextNodeIndex = this.getCallNodeIndexFromParentAndFunc(index, func);
// We couldn't find this path into the call node table. This shouldn't
// normally happen.
if (nextNodeIndex === null) {
return null;
}
// Contributing to the shared cache
const hash = sliceHashes.pop()!;
cache.set(hash, nextNodeIndex);
index = nextNodeIndex;
i++;
}
return index < 0 ? null : index;
}
getCallNodeIndexFromParentAndFunc(
parent: IndexIntoCallNodeTable | -1,
func: IndexIntoFuncTable
): IndexIntoCallNodeTable | null {
const callNodeTable = this._callNodeTable;
if (parent === -1) {
if (callNodeTable.length === 0) {
return null;
}
} else if (callNodeTable.subtreeRangeEnd[parent] === parent + 1) {
// parent has no children.
return null;
}
// Node children always come after their parents in the call node table,
// that's why we start looping at `parent + 1`.
// Note that because the root parent is `-1`, we correctly start at `0` when
// we look for a root.
const firstChild = parent + 1;
for (
let callNodeIndex = firstChild;
callNodeIndex !== -1;
callNodeIndex = callNodeTable.nextSibling[callNodeIndex]
) {
if (callNodeTable.func[callNodeIndex] === func) {
return callNodeIndex;
}
}
return null;
}
prefixForNode(
callNodeIndex: IndexIntoCallNodeTable
): IndexIntoCallNodeTable | -1 {
return this._callNodeTable.prefix[callNodeIndex];
}
funcForNode(callNodeIndex: IndexIntoCallNodeTable): IndexIntoFuncTable {
return this._callNodeTable.func[callNodeIndex];
}
categoryForNode(
callNodeIndex: IndexIntoCallNodeTable
): IndexIntoCategoryList {
return this._callNodeTable.category[callNodeIndex];
}
subcategoryForNode(
callNodeIndex: IndexIntoCallNodeTable
): IndexIntoCategoryList {
return this._callNodeTable.subcategory[callNodeIndex];
}
innerWindowIDForNode(
callNodeIndex: IndexIntoCallNodeTable
): IndexIntoCategoryList {
return this._callNodeTable.innerWindowID[callNodeIndex];
}
depthForNode(callNodeIndex: IndexIntoCallNodeTable): number {
return this._callNodeTable.depth[callNodeIndex];
}
sourceFramesInlinedIntoSymbolForNode(
callNodeIndex: IndexIntoCallNodeTable
): IndexIntoNativeSymbolTable | -1 | -2 {
return this._callNodeTable.sourceFramesInlinedIntoSymbol[callNodeIndex];
}
}
// A "subtype" of IndexIntoCallNodeTable, used in places where it is known that
// we are referring to an inverted call node. We just use it as a convention,
// Flow doesn't actually treat this any different from any other index and won't
// catch incorrect uses.
type InvertedCallNodeHandle = number;
// An index into InvertedNonRootCallNodeTable. This is usually created by
// taking an InvertedCallNodeHandle and subtracting rootCount.
type IndexIntoInvertedNonRootCallNodeTable = number;
// Information about the roots of the inverted call tree. We compute this
// information upfront for all roots. The root count is fixed, so most of the
// arrays in this struct are fixed-size typed arrays.
// The number of roots is the same as the number of functions in the funcTable.
type InvertedRootCallNodeTable = {
category: Int32Array; // IndexIntoFuncTable -> IndexIntoCategoryList
subcategory: Int32Array; // IndexIntoFuncTable -> IndexIntoSubcategoryListForCategory
innerWindowID: Float64Array; // IndexIntoFuncTable -> InnerWindowID
// IndexIntoNativeSymbolTable: all frames that collapsed into this call node inlined into the same native symbol
// -1: divergent: some, but not all, frames that collapsed into this call node were inlined, or they are from different symbols
// -2: no inlining
sourceFramesInlinedIntoSymbol: Int32Array; // IndexIntoFuncTable -> IndexIntoNativeSymbolTable | -1 | -2
// The (exclusive) end of the suffix order index range for each root node.
// The beginning of the range is given by suffixOrderIndexRangeEnd[i - 1], or by
// zero. This is possible because both the inverted root order and the suffix order
// are determined by the func order.
suffixOrderIndexRangeEnd: Uint32Array; // IndexIntoFuncTable -> SuffixOrderIndex,
length: number;
};
// Information about the non-root nodes of the inverted call tree. This table
// grows on-demand, as new inverted call nodes are materialized.
type InvertedNonRootCallNodeTable = {
prefix: InvertedCallNodeHandle[];
func: IndexIntoFuncTable[]; // IndexIntoInvertedNonRootCallNodeTable -> IndexIntoFuncTable
pathHash: string[]; // IndexIntoInvertedNonRootCallNodeTable -> string
category: IndexIntoCategoryList[]; // IndexIntoInvertedNonRootCallNodeTable -> IndexIntoCategoryList
subcategory: IndexIntoSubcategoryListForCategory[]; // IndexIntoInvertedNonRootCallNodeTable -> IndexIntoSubcategoryListForCategory
innerWindowID: InnerWindowID[]; // IndexIntoInvertedNonRootCallNodeTable -> InnerWindowID
// IndexIntoNativeSymbolTable: all frames that collapsed into this call node inlined into the same native symbol
// -1: divergent: some, but not all, frames that collapsed into this call node were inlined, or they are from different symbols
// -2: no inlining
sourceFramesInlinedIntoSymbol: Array<IndexIntoNativeSymbolTable | -1 | -2>;
suffixOrderIndexRangeStart: SuffixOrderIndex[]; // IndexIntoInvertedNonRootCallNodeTable -> SuffixOrderIndex
suffixOrderIndexRangeEnd: SuffixOrderIndex[]; // IndexIntoInvertedNonRootCallNodeTable -> SuffixOrderIndex
// Non-null for non-root nodes whose children haven't been created yet.
// The array at index x caches ancestors of the non-inverted nodes belonging
// to the inverted node x, specifically the ancestor "k steps up" from each
// non-inverted node, with k being the depth of the inverted node.
// This is useful to quickly compute the children for this inverted node.
// Afterwards it's set to null for that index, and the next level is passed on
// to the newly-created children.
// Please refer to 'Why do we keep a "deepNodes" property in the inverted table?'
// in the comment on CallNodeInvertedImpl below for a more detailed explanation.
//
// For every inverted non-root call node x with deepNodes[x] !== null:
// For every suffix order index i in suffixOrderIndexRangeStart[x]..suffixOrderIndexRangeEnd[x],
// the k'th parent node of suffixOrderedCallNodes[i] is stored at
// deepNodes[x][i - suffixOrderIndexRangeStart[x]], with k = depth[x].
deepNodes: Array<Uint32Array | null>; // IndexIntoInvertedNonRootCallNodeTable -> (Uint32Array | null)
depth: number[]; // IndexIntoInvertedNonRootCallNodeTable -> number
length: number;
};
// Compute the InvertedRootCallNodeTable.
// We compute this information upfront for all roots. The root count is fixed -
// the number of roots is the same as the number of functions in the funcTable.
function _createInvertedRootCallNodeTable(
callNodeTable: CallNodeTable,
rootSuffixOrderIndexRangeEndCol: Uint32Array,
suffixOrderedCallNodes: Uint32Array,
defaultCategory: IndexIntoCategoryList
): InvertedRootCallNodeTable {
const funcCount = rootSuffixOrderIndexRangeEndCol.length;
const category = new Int32Array(funcCount);
const subcategory = new Int32Array(funcCount);
const innerWindowID = new Float64Array(funcCount);
const sourceFramesInlinedIntoSymbol = new Int32Array(funcCount);
let previousRootSuffixOrderIndexRangeEnd = 0;
for (let funcIndex = 0; funcIndex < funcCount; funcIndex++) {
const callNodeSuffixOrderIndexRangeStart =
previousRootSuffixOrderIndexRangeEnd;
const callNodeSuffixOrderIndexRangeEnd =
rootSuffixOrderIndexRangeEndCol[funcIndex];
previousRootSuffixOrderIndexRangeEnd = callNodeSuffixOrderIndexRangeEnd;
if (
callNodeSuffixOrderIndexRangeStart === callNodeSuffixOrderIndexRangeEnd
) {
// This root is never actually displayed in the inverted tree. It
// corresponds to a func which has no self time - no non-inverted node has
// this func as its self func. This root only exists for simplicity, so
// that there is one root per func.
// Set a dummy value for this unused root.
sourceFramesInlinedIntoSymbol[funcIndex] = -2; // "no symbol"
// (the other columns are already initialized to zero because they're
// typed arrays)
continue;
}
// Fill the remaining fields with the conflict-resolved versions of the values
// in the non-inverted call node table.
const firstNonInvertedCallNodeIndex =
suffixOrderedCallNodes[callNodeSuffixOrderIndexRangeStart];
let resolvedCategory =
callNodeTable.category[firstNonInvertedCallNodeIndex];
let resolvedSubcategory =
callNodeTable.subcategory[firstNonInvertedCallNodeIndex];
const resolvedInnerWindowID =
callNodeTable.innerWindowID[firstNonInvertedCallNodeIndex];
let resolvedSourceFramesInlinedIntoSymbol =
callNodeTable.sourceFramesInlinedIntoSymbol[
firstNonInvertedCallNodeIndex
];
// Resolve conflicts in the same way as for the non-inverted call node table.
for (
let orderingIndex = callNodeSuffixOrderIndexRangeStart + 1;
orderingIndex < callNodeSuffixOrderIndexRangeEnd;
orderingIndex++
) {
const currentNonInvertedCallNodeIndex =
suffixOrderedCallNodes[orderingIndex];
// Resolve category conflicts, by resetting a conflicting subcategory or
// category to the default category.
if (
resolvedCategory !==
callNodeTable.category[currentNonInvertedCallNodeIndex]
) {
// Conflicting origin stack categories -> default category + subcategory.
resolvedCategory = defaultCategory;
resolvedSubcategory = 0;
} else if (
resolvedSubcategory !==
callNodeTable.subcategory[currentNonInvertedCallNodeIndex]
) {
// Conflicting origin stack subcategories -> "Other" subcategory.
resolvedSubcategory = 0;
}
// Resolve "inlined into" conflicts. This can happen if you have two
// function calls A -> B where only one of the B calls is inlined, or
// if you use call tree transforms in such a way that a function B which
// was inlined into two different callers (A -> B, C -> B) gets collapsed
// into one call node.
if (
resolvedSourceFramesInlinedIntoSymbol !==
callNodeTable.sourceFramesInlinedIntoSymbol[
currentNonInvertedCallNodeIndex
]
) {
// Conflicting inlining: -1.
resolvedSourceFramesInlinedIntoSymbol = -1;
}
// FIXME: Resolve conflicts of InnerWindowID
}
category[funcIndex] = resolvedCategory;
subcategory[funcIndex] = resolvedSubcategory;
innerWindowID[funcIndex] = resolvedInnerWindowID;
sourceFramesInlinedIntoSymbol[funcIndex] =
resolvedSourceFramesInlinedIntoSymbol;
}
return {
category,
subcategory,
innerWindowID,
sourceFramesInlinedIntoSymbol,
suffixOrderIndexRangeEnd: rootSuffixOrderIndexRangeEndCol,
length: funcCount,
};
}
function _createEmptyInvertedNonRootCallNodeTable(): InvertedNonRootCallNodeTable {
return {
prefix: [],
func: [],
pathHash: [],
category: [],
subcategory: [],
innerWindowID: [],
sourceFramesInlinedIntoSymbol: [],
suffixOrderIndexRangeStart: [],
suffixOrderIndexRangeEnd: [],
deepNodes: [],
depth: [],
length: 0,
};
}
// The return type of _computeSuffixOrderForInvertedRoots.
//
// This is not the fully-refined suffix order; you could say that it's
// refined up to depth zero. It is refined enough so that every root has a
// contiguous range in the suffix order, where each range contains the root's
// corresponding non-inverted nodes.
type SuffixOrderForInvertedRoots = {
suffixOrderedCallNodes: Uint32Array;
suffixOrderIndexes: Uint32Array;
rootSuffixOrderIndexRangeEndCol: Uint32Array;
};
/**
* Computes an ordering for the non-inverted call node table where all
* non-inverted call nodes are ordered by their self func.
*
* This function is very performance sensitive. The number of non-inverted call
* nodes can be very high, e.g. ~3 million for https://share.firefox.dev/3N56qMu
*/
function _computeSuffixOrderForInvertedRoots(
callNodeTable: 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 = callNodeTable.length;
const callNodeTableFuncCol = callNodeTable.func;
for (let i = 0; i < callNodeCount; i++) {
const func = callNodeTableFuncCol[i];
nodeCountPerFunc[func]++;
}
// Pass 2: Compute cumulative start index based on the counts.
const startIndexPerFunc = nodeCountPerFunc; // Warning: we are reusing the same array
let nextFuncStartIndex = 0;
for (let func = 0; func < startIndexPerFunc.length; func++) {
const count = nodeCountPerFunc[func];
startIndexPerFunc[func] = nextFuncStartIndex;
nextFuncStartIndex += count;
}
// Pass 3: Compute the new ordering based on the reserved slices in startIndexPerFunc.
const nextIndexPerFunc = startIndexPerFunc;
const suffixOrderedCallNodes = new Uint32Array(callNodeCount);
const suffixOrderIndexes = new Uint32Array(callNodeCount);
for (let callNode = 0; callNode < callNodeCount; callNode++) {
const func = callNodeTableFuncCol[callNode];
const orderIndex = nextIndexPerFunc[func]++;
suffixOrderedCallNodes[orderIndex] = callNode;
suffixOrderIndexes[callNode] = orderIndex;
}
// The indexes in nextIndexPerFunc have now been advanced such that they point
// at the end of each partition.
const rootSuffixOrderIndexRangeEndCol = startIndexPerFunc;
return {
suffixOrderedCallNodes,
suffixOrderIndexes,
rootSuffixOrderIndexRangeEndCol,
};
}
// Information used to create the children of a node in the inverted tree.
type ChildrenInfo = {
// The func for each child. Duplicate-free and sorted by func.
funcPerChild: Uint32Array; // IndexIntoFuncTable[]
// The number of deep nodes for each child. Every entry is non-zero.
deepNodeCountPerChild: Uint32Array;
// The subset of the parent's self nodes which are not part of childrenSelfNodes.
selfNodesWhichEndAtParent: IndexIntoCallNodeTable[];
// The self nodes and their corresponding deep nodes for all children, each
// flattened into a single array.
// The length of these arrays is the sum of the values in deepNodeCountPerChild.
childrenSelfNodes: Uint32Array;
childrenDeepNodes: Uint32Array;
// The suffixOrderIndexRangeStart of the first child.
childrenSuffixOrderIndexRangeStart: number;
};
// An index into SuffixOrderedCallNodes.
export type SuffixOrderIndex = number;
/**
* The CallNodeInfo implementation for the inverted tree, with additional
* functionality for the inverted call tree.
*
* # The Suffix Order
*
* We define an alternative ordering of the *non-inverted* call nodes, called the
* "suffix order", which is useful when interacting with the *inverted* tree.
* The suffix order is stored by two Uint32Array side tables, returned by
* getSuffixOrderedCallNodes() and getSuffixOrderIndexes().
* getSuffixOrderedCallNodes() maps a suffix order index to a non-inverted call
* node, and getSuffixOrderIndexes() is the reverse, mapping a non-inverted call
* node to its suffix order index.
*
* ## Background
*
* Many operations we do in the profiler require the ability to do an efficient
* "ancestor" check:
*
* - For a call node X in the call tree, what's its "total"?
* - When call node X in the call tree is selected, which samples should be
* highlighted in the activity graph, and which samples should contribute to
* the category breakdown in the sidebar?
* - For how many samples has the clicked call node X been observed in a certain
* line of code / in a certain instruction?
*
* We answer these questions by iterating over samples, getting the sample's
* call node Y, and checking whether the selected / clicked node X is an ancestor
* of Y.
*
* In the non-inverted call tree, the ordering in the call node table gives us a
* quick way to do these checks: For a call node X, all its descendant call nodes
* are in a contiguous range between X and callNodeTable.subtreeRangeEnd[X].
*
* We want to have a similar ability for the *inverted* call tree, but without
* computing a full inverted call node table. The suffix order gives us this
* ability. It's based on the following insights:
*
* 1. Non-inverted call nodes are "enough" for many purposes even in inverted mode:
*
* When doing the per-sample checks listed above, we don't need an *inverted*
* call node for each sample. We just need an inverted call node for the
* clicked / selected node, and then we can check if the sample's
* *non-inverted* call node contributes to the selected / clicked *inverted*
* call node.
* A non-inverted call node is just a representation of a call path. You can
* read that call path from front to back, or you can read it from back to
* front. If you read it from back to front that's the inverted call path.
*
* 2. We can store multiple different orderings of the non-inverted call node
* table.
*
* The non-inverted call node table remains ordered in depth-first traversal
* order of the non-inverted tree, as described in the "Call node ordering"
* section on the CallNodeTable type. The suffix order is an additional,
* alternative ordering that we store on the side.
*
* ## Definition
*
* We define the suffix order as the lexicographic order of the inverted call path.
* Or as the lexicographic order of the non-inverted call paths "when reading back to front".
*
* D -> B comes before A -> C, because B comes before C.
* D -> B comes after A -> B, because B == B and D comes after A.
* D -> B comes before A -> D -> B, because B == B, D == D, and "end of path" comes before A.
*
* ## Example
*
* ### Non-inverted call tree:
*
* Legend:
*
* cnX: Non-inverted call node index X
* soX: Suffix order index X
*
* ```
* Tree Left aligned Right aligned Reordered by suffix
* - [cn0] A = A = A [so0] [so0] [cn0] A
* - [cn1] B = A -> B = A -> B [so3] [so1] [cn4] A <- A
* - [cn2] A = A -> B -> A = A -> B -> A [so2] ↘↗ [so2] [cn2] A <- B <- A
* - [cn3] C = A -> B -> C = A -> B -> C [so6] ↗↘ [so3] [cn1] B <- A
* - [cn4] A = A -> A = A -> A [so1] [so4] [cn5] B <- A <- A
* - [cn5] B = A -> A -> B = A -> A -> B [so4] [so5] [cn6] C <- A
* - [cn6] C = A -> C = A -> C [so5] [so6] [cn3] C <- B <- A
* ```
*
* ### Inverted call tree:
*
* Legend, continued:
*
* inX: Inverted call node index X (this index is somewhat arbitrary because
* it's based on the order in which callNodeInfoInverted.getChildren is
* called)
* so:X..Y: Suffix order index range soX..soY (soY excluded)
*
* ```
* Represents call paths ending in
* - [in0] A (so:0..3) = A = ... A (cn0, cn4, cn2)
* - [in3] A (so:1..2) = A <- A = ... A -> A (cn4)
* - [in4] B (so:2..3) = A <- B = ... B -> A (cn2)
* - [in6] A (so:2..3) = A <- B <- A = ... A -> B -> A (cn2)
* - [in1] B (so:3..5) = B = ... B (cn1, cn5)
* - [in5] A (so:3..5) = B <- A = ... A -> B (cn1, cn5)
* - [in10] A (so:4..5) = B <- A <- A = ... A -> A -> B (cn5)
* - [in2] C (so:5..7) = C = ... C (cn6, cn3)
* - [in7] A (so:5..6) = C <- A = ... A -> C (cn6)
* - [in8] B (so:6..7) = C <- B = ... B -> C (cn3)
* - [in9] A (so:6..7) = C <- B <- A = ... A -> B -> C (cn3)
* ```
*
* In the suffix order, call paths become grouped in such a way that call paths
* which belong to the same *inverted* tree node (i.e. which share a suffix) end
* up ordered next to each other. This makes it so that a node in the inverted
* tree can refer to all its represented call paths with a single contiguous range.
*
* In this example, inverted tree node `in5` represents all call paths which end
* in A -> B. Both `cn1` and `cn5` do so; `cn1` is A -> B and `cn5` is A -> A -> B.
* In the suffix order, `cn1` and `cn5` end up next to each other, at positions
* `so3` and `so4`. This means that the two paths can be referred to via the suffix
* order index range 3..5.
*
* Suffix ordered call nodes: [0, 4, 2, 1, 5, 6, 3] (soX -> cnY)
* Suffix order indexes: [0, 3, 2, 6, 1, 4, 5] (cnX -> soY)
*
* ## Incremental order refinement
*
* Sorting all non-inverted nodes upfront would take a long time on large profiles.
* So we don't do that. Instead, we refine the order as new inverted tree nodes
* are materialized on demand.
*
* The ground rules are:
* - For any inverted call node X, getSuffixOrderIndexRangeForCallNode(X) must
* always return the same range.
* - For any inverted call node X, the *set* of suffix ordered call nodes in the
* range returned by getSuffixOrderIndexRangeForCallNode(X) must always be the
* same. Notably, the order in the range does *not* necessarily need to remain
* the same.
*
* This means that, whenever you have a handle X of an inverted call node, you
* can be confident that your checks of the form "is non-inverted call node Y
* part of X's range" will work correctly.
*
* # On-demand node creation
*
* Inverted nodes are created in this order:
*
* 1. All root nodes have been created upfront. There is one root per func.
* 2. The first _createChildren call will be for a root node. We create non-root
* nodes for the root's children, and add them to _invertedNonRootCallNodeTable.
* 3. The next call to _createChildren can be for a non-root node. Again we
* create nodes for the children and add them to _invertedNonRootCallNodeTable.
*
* Example:
*
* ```
* Non-inverted tree:
*
* Tree Left aligned Right aligned
* - [cn0] A = A = A [so0]
* - [cn1] B = A -> B = A -> B [so3]
* - [cn2] A = A -> B -> A = A -> B -> A [so2]
* - [cn3] C = A -> B -> C = A -> B -> C [so6]
* - [cn4] A = A -> A = A -> A [so1]
* - [cn5] B = A -> A -> B = A -> A -> B [so4]
* - [cn6] C = A -> C = A -> C [so5]
*
* Inverted tree:
* Represents call paths ending in
* - [in0] A (so:0..3) = A = ... A (cn0, cn4, cn2)
* - [in3] A (so:1..2) = A <- A = ... A -> A (cn4)
* - [in4] B (so:2..3) = A <- B = ... B -> A (cn2)
* - [in6] A (so:2..3) = A <- B <- A = ... A -> B -> A (cn2)
* - [in1] B (so:3..5) = B = ... B (cn1, cn5)
* - [in5] A (so:3..5) = B <- A = ... A -> B (cn1, cn5)
* - [in10] A (so:4..5) = B <- A <- A = ... A -> A -> B (cn5)
* - [in2] C (so:5..7) = C = ... C (cn6, cn3)
* - [in7] A (so:5..6) = C <- A = ... A -> C (cn6)
* - [in8] B (so:6..7) = C <- B = ... B -> C (cn3)
* - [in9] A (so:6..7) = C <- B <- A = ... A -> B -> C (cn3)
* ```
*
* This inverted tree was built up as follows:
*
* Iteration 0: We create all roots. New nodes: in0, in1, in2
* Iteration 1: _createChildren(in0) is called. New nodes: in3, in4
* Iteration 2: _createChildren(in1) is called. New nodes: in5
* Iteration 3: _createChildren(in4) is called. New nodes: in6
* Iteration 4: _createChildren(in2) is called. New nodes: in7, in8
* Iteration 5: _createChildren(in8) is called. New nodes: in9
* Iteration 6: _createChildren(in5) is called. New nodes: in10
*
* The order of the _createChildren calls depends on how the user interacts with the
* call tree. The user could uncollapse call tree nodes in a different order,
* which would cause _createChildren to be called in a different order, and the
* inverted nodes we create would have different indexes.
*
* There are two invariants about "which inverted nodes exist" at any given time:
*
* 1. For any inverted tree node inX, _invertedNonRootCallNodeTable either contains
* all or none of inX's children.
* 2. For any inverted non-root node `inQ` with parent node `inP`,
* _createChildren(inP) is called before _createChildren(inQ) is called. That's
* somewhat obvious: inQ is *created* by the _createChildren(inP) call; without
* _createChildren(inP) we would not have an inQ to pass to _createChildren(inQ).
*
* ## Computation of the children
*
* How do we know which children to create? We look at the parents in the
* *non-inverted* tree.
*
* First, let's create the children for the root `in0` (func: A, depth 0).
* in0 has three "self nodes": cn0 (func: A), cn4 (func: A), and cn2 (func: A).
*
* To create the children of in0, we need to look at the parents of cn0, cn4, and cn2.
*
* cn0 has no parent.
* cn4's parent is cn0 (func: A).
* cn2's parent is cn1 (func: B).
*
* This means that in0 has two children: One for func A and one for func B.
*
* Let's create the two children:
* - in3: func A, parent in0, depth 1, self nodes [cn4]
* - in4: func B, parent in0, depth 1, self nodes [cn2]
*
* ### Why do we keep a "deepNodes" property in the inverted table?
*
* In the next few paragraphs, we'll explain why we need to constantly iterate
* over the non-inverted parents, and that the "deepNodes" property is a cache
* to make it faster. Keep reading!
*
* Let's create the children of the non-root node in4 (func: B, depth 1).
* in4 represents the call path suffix "... -> B -> A".
*
* in4 has one self node: cn2 (func: A). cn2 is the only non-inverted node
* whose call path ends in "... -> B -> A".
*
* cn2's 0th parent (i.e. itself) is cn2 (func: A).
* cn2's 1st parent is cn1 (func: B).
* cn2's 2nd parent (i.e. its grandparent) is cn0 (func: A). <-- func A
*
* So in4 has one child, with func A. Let's create it:
* - in6: func A, parent in4, depth 2, self nodes [cn2]
*
* This example shows that, if we create inverted children of depth 2,
* we need to look at the grandparent ("2nd parent") of each self node.
*
* ---
*
* Let's try to go one level deeper and create the children of in6 (func A, depth 2):
*
* in6 has one self node: cn2.
* in6 has depth 2, its children would have depth 3.
*
* cn2's 0th parent is cn2 (func: A).
* cn2's 1st parent is cn1 (func: B).
* cn2's 2nd parent is cn0 (func: A).
* cn2's 3rd parent is ... it does not have one!
*
* So in6 has no children.
*
* ---
*
* More generically, we've shown that, in order to create inverted children of
* depth k, we need to look at the k'th parent of all self nodes for the inverted
* node whose children we're creating.
*
* If we had to get those k'th parent nodes from the self node all the time, we
* would spend a lot of time walking up the non-inverted tree! For example, if
* we wanted to create the children of an inverted node with depth 20, if that
* node had 500 self nodes, we would need to find the 21st parent node of each of
* those 500 self nodes!
*
* Walking up 21 steps is a bit silly, because we already walked up 20 steps for
* the same nodes when we created the inverted parent. Can we just store the result
* of the 20-step walk, and reuse it? Yes we can!
*
* So that's what why we have the "deepNodes" cache: On each inverted node, we
* don't only store its self nodes, we also store its "deep nodes", i.e. the k'th
* parent of each self node, with k being the depth of the inverted node.
* Then we only need to look at the immediate parent of each deep node in order
* to know which children to create for the inverted node.
*
* For an inverted root such as in0, k is 0, and the deep node for each self node
* is just the self node itself. (The 0'th parent of a node is that node itself.)
*
* in0:
* |-----------|-------------------------|
* | self node | corresponding deep node |
* |-----------|-------------------------|
* | cn0 | cn0 |
* | cn4 | cn4 |
* | cn2 | cn2 |
* |-----------|-------------------------|
*
* For in4, k is 1, and the deep node for each self node is the self node's
* immediate parent.
*
* in4:
* |-----------|-------------------------|
* | self node | corresponding deep node |
* |-----------|-------------------------|
* | cn2 | cn1 |
* |-----------|-------------------------|
*
* in6 (depth 2):
* |-----------|-------------------------|
* | self node | corresponding deep node |
* |-----------|-------------------------|
* | cn2 | cn0 |
* |-----------|-------------------------|
*
* So whenever we create the children of an inverted node, we start with its
* deep nodes and get their immediate parents. These parents become the deep
* nodes of the newly-created children. We store them on each new child. And
* this saves time because we don't have to walk up the parent chain by more
* than one step.
*
* Once we've created the children of an inverted node, we can discard its own
* deep nodes. They're not needed anymore. So _takeDeepNodesForInvertedNode
* nulls out the stored deepNodes for an inverted node when it's called.
*/
export class CallNodeInfoInverted implements CallNodeInfo {
// The non-inverted call node table.
_callNodeTable: CallNodeTable;
// The part of the inverted call node table for the roots of the inverted tree.
_invertedRootCallNodeTable: InvertedRootCallNodeTable;
// The dynamically growing part of the inverted call node table for just the
// non-root nodes. Entries are added to this table as needed, whenever a caller
// asks us for children of a node for which we haven't needed children before,
// or when a caller asks us to translate an inverted call path that we haven't
// seend before to an inverted call node index.
_invertedNonRootCallNodeTable: InvertedNonRootCallNodeTable;
// The mapping of non-inverted stack index to non-inverted call node index.
_stackIndexToNonInvertedCallNodeIndex: Int32Array;
// The number of roots, which is also the number of functions. Each root of
// the inverted tree represents a "self" function, i.e. all call paths which
// end in a certain function.
// We have roots even for functions which aren't used as "self" functions in
// any sampled stacks, for simplicity. The actual displayed number of roots
// in the call tree will usually be lower because roots with a zero total sample
// count will be filtered out. But any data in this class is fully independent
// from sample counts.
_rootCount: number;
// This is a Map<SuffixOrderIndex, IndexIntoCallNodeTable>.
// 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<IndexIntoCallNodeTable, SuffixOrderIndex>.
_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<CallNodePathHash, InvertedCallNodeHandle>. This map speeds up
// the look-up process by caching every CallNodePath we handle which avoids
// repeatedly looking up parents.
_cache: Map<string, InvertedCallNodeHandle> = 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<InvertedCallNodeHandle, InvertedCallNodeHandle[]> = new Map();
constructor(
callNodeTable: CallNodeTable,
stackIndexToNonInvertedCallNodeIndex: Int32Array,
defaultCategory: IndexIntoCategoryList,
funcCount: number
) {
this._callNodeTable = callNodeTable;
this._stackIndexToNonInvertedCallNodeIndex =
stackIndexToNonInvertedCallNodeIndex;
const {
suffixOrderedCallNodes,
suffixOrderIndexes,
rootSuffixOrderIndexRangeEndCol,
} = _computeSuffixOrderForInvertedRoots(callNodeTable, funcCount);
this._suffixOrderedCallNodes = suffixOrderedCallNodes;
this._suffixOrderIndexes = suffixOrderIndexes;
this._defaultCategory = defaultCategory;
this._rootCount = funcCount;
const invertedRootCallNodeTable = _createInvertedRootCallNodeTable(
callNodeTable,
rootSuffixOrderIndexRangeEndCol,
suffixOrderedCallNodes,
defaultCategory
);
this._invertedRootCallNodeTable = invertedRootCallNodeTable;
this._invertedNonRootCallNodeTable =
_createEmptyInvertedNonRootCallNodeTable();
}
isInverted(): boolean {
return true;
}
asInverted(): CallNodeInfoInverted | null {
return this;
}
getCallNodeTable(): CallNodeTable {
return this._callNodeTable;
}
getStackIndexToNonInvertedCallNodeIndex(): Int32Array {
return this._stackIndexToNonInvertedCallNodeIndex;
}