Files
grafana/public/app/plugins/panel/flamegraph/components/FlameGraph/treeTransforms.ts
2023-08-01 16:08:46 +02:00

135 lines
4.6 KiB
TypeScript

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;
newNode.valueRight = args.child.valueRight;
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),
// valueRight may not exist at all if this is not a diff profile
valueRight: args.items.reduce<number | undefined>((acc, i) => {
if (i.valueRight !== undefined) {
return (acc ?? 0) + i.valueRight;
} else {
return acc;
}
}, undefined),
itemIndexes: indexes,
// these will change later
children: [],
parents: [],
start: 0,
};
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();
}
return levels;
}