forked from grafana/grafana
-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathtreeTransforms.ts
131 lines (115 loc) · 4.45 KB
/
treeTransforms.ts
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
import { groupBy } from 'lodash';
import { LevelItem } from './dataTransform';
type DataInterface = {
getLabel: (index: number) => string;
};
// Merge parent subtree of the roots for the callers tree in the sandwich view of the flame graph.
export function mergeParentSubtrees(roots: LevelItem[], data: DataInterface): LevelItem[][] {
const newRoots = getParentSubtrees(roots);
return mergeSubtrees(newRoots, data, 'parents');
}
// Returns a subtrees per root that will have the parents resized to the same value as the root. When doing callers
// tree we need to keep proper sizes of the parents, before we merge them, so we correctly attribute to the parents
// only the value it contributed to the root.
// So if we have something like:
// [0/////////////]
// [1//][4/////][6]
// [2] [5/////]
// [6] [6/][8/]
// [7]
// Taking all the node with '6' will create:
// [0][0/]
// [1][4/]
// [2][5/][0]
// [6][6/][6]
// Which we can later merge.
function getParentSubtrees(roots: LevelItem[]) {
return roots.map((r) => {
if (!r.parents?.length) {
return r;
}
const newRoot = {
...r,
children: [],
};
const stack: Array<{ child: undefined | LevelItem; parent: LevelItem }> = [
{ child: newRoot, parent: r.parents[0] },
];
while (stack.length) {
const args = stack.shift()!;
const newNode = {
...args.parent,
children: args.child ? [args.child] : [],
parents: [],
};
if (args.child) {
newNode.value = args.child.value;
args.child.parents = [newNode];
}
if (args.parent.parents?.length) {
stack.push({ child: newNode, parent: args.parent.parents[0] });
}
}
return newRoot;
});
}
// Merge subtrees into a single tree. Returns an array of levels for easy rendering. It assumes roots are mergeable,
// meaning they represent the same unit of work (same label). Then we walk the tree in a specified direction,
// merging nodes with the same label and same parent/child into single bigger node. This copies the tree (and all nodes)
// as we are creating new merged nodes and modifying the parents/children.
export function mergeSubtrees(
roots: LevelItem[],
data: DataInterface,
direction: 'parents' | 'children' = 'children'
): LevelItem[][] {
const oppositeDirection = direction === 'parents' ? 'children' : 'parents';
const levels: LevelItem[][] = [];
// Loop instead of recursion to be sure we don't blow stack size limit and save some memory. Each stack item is
// basically a list of arrays you would pass to each level of recursion.
const stack: Array<{ previous: undefined | LevelItem; items: LevelItem[]; level: number }> = [
{ previous: undefined, items: roots, level: 0 },
];
while (stack.length) {
const args = stack.shift()!;
const indexes = args.items.flatMap((i) => i.itemIndexes);
const newItem: LevelItem = {
// We use the items value instead of value from the data frame, cause we could have changed it in the process
value: args.items.reduce((acc, i) => acc + i.value, 0),
itemIndexes: indexes,
// these will change later
children: [],
parents: [],
start: 0,
level: args.level,
};
levels[args.level] = levels[args.level] || [];
levels[args.level].push(newItem);
if (args.previous) {
// Not the first level, so we need to make sure we update previous items to keep the child/parent relationships
// and compute correct new start offset for the item.
newItem[oppositeDirection] = [args.previous];
const prevSiblingsVal =
args.previous[direction]?.reduce((acc, node) => {
return acc + node.value;
}, 0) || 0;
newItem.start = args.previous.start + prevSiblingsVal;
args.previous[direction]!.push(newItem);
}
const nextItems = args.items.flatMap((i) => i[direction] || []);
// Group by label which for now is the only identifier by which we decide if node represents the same unit of work.
const nextGroups = groupBy(nextItems, (c) => data.getLabel(c.itemIndexes[0]));
for (const g of Object.values(nextGroups)) {
stack.push({ previous: newItem, items: g, level: args.level + 1 });
}
}
// Reverse the levels if we are doing callers tree, so we return levels in the correct order.
if (direction === 'parents') {
levels.reverse();
levels.forEach((level, index) => {
level.forEach((item) => {
item.level = index;
});
});
}
return levels;
}