|
|
|
@ -34,24 +34,19 @@ export type CollapseConfig = { |
|
|
|
|
collapsed: boolean; |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
export type CollapsedMap = Map<LevelItem, CollapseConfig>; |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* Convert data frame with nested set format into array of level. This is mainly done for compatibility with current |
|
|
|
|
* rendering code. |
|
|
|
|
*/ |
|
|
|
|
export function nestedSetToLevels( |
|
|
|
|
container: FlameGraphDataContainer, |
|
|
|
|
options?: { |
|
|
|
|
collapsing: boolean; |
|
|
|
|
} |
|
|
|
|
options?: Options |
|
|
|
|
): [LevelItem[][], Record<string, LevelItem[]>, CollapsedMap] { |
|
|
|
|
const levels: LevelItem[][] = []; |
|
|
|
|
let offset = 0; |
|
|
|
|
|
|
|
|
|
let parent: LevelItem | undefined = undefined; |
|
|
|
|
const uniqueLabels: Record<string, LevelItem[]> = {}; |
|
|
|
|
const collapsedMap: CollapsedMap = new Map(); |
|
|
|
|
|
|
|
|
|
for (let i = 0; i < container.data.length; i++) { |
|
|
|
|
const currentLevel = container.getLevel(i); |
|
|
|
@ -82,22 +77,6 @@ export function nestedSetToLevels( |
|
|
|
|
level: currentLevel, |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
if (options?.collapsing) { |
|
|
|
|
// We collapse similar items here, where it seems like parent and child are the same thing and so the distinction
|
|
|
|
|
// isn't that important. We create a map of items that should be collapsed together.
|
|
|
|
|
if (parent && newItem.value === parent.value) { |
|
|
|
|
if (collapsedMap.has(parent)) { |
|
|
|
|
const config = collapsedMap.get(parent)!; |
|
|
|
|
collapsedMap.set(newItem, config); |
|
|
|
|
config.items.push(newItem); |
|
|
|
|
} else { |
|
|
|
|
const config = { items: [parent, newItem], collapsed: true }; |
|
|
|
|
collapsedMap.set(parent, config); |
|
|
|
|
collapsedMap.set(newItem, config); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
if (uniqueLabels[container.getLabel(i)]) { |
|
|
|
|
uniqueLabels[container.getLabel(i)].push(newItem); |
|
|
|
|
} else { |
|
|
|
@ -107,12 +86,68 @@ export function nestedSetToLevels( |
|
|
|
|
if (parent) { |
|
|
|
|
parent.children.push(newItem); |
|
|
|
|
} |
|
|
|
|
parent = newItem; |
|
|
|
|
|
|
|
|
|
parent = newItem; |
|
|
|
|
levels[currentLevel].push(newItem); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
return [levels, uniqueLabels, collapsedMap]; |
|
|
|
|
const collapsedMapContainer = new CollapsedMapContainer(options?.collapsingThreshold); |
|
|
|
|
if (options?.collapsing) { |
|
|
|
|
// We collapse similar items here, where it seems like parent and child are the same thing and so the distinction
|
|
|
|
|
// isn't that important. We create a map of items that should be collapsed together. We need to do it with complete
|
|
|
|
|
// tree as we need to know how many children an item has to know if we can collapse it.
|
|
|
|
|
collapsedMapContainer.addTree(levels[0][0]); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
return [levels, uniqueLabels, collapsedMapContainer.getMap()]; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
export type CollapsedMap = Map<LevelItem, CollapseConfig>; |
|
|
|
|
export class CollapsedMapContainer { |
|
|
|
|
private map = new Map(); |
|
|
|
|
private threshold = 0.99; |
|
|
|
|
|
|
|
|
|
constructor(threshold?: number) { |
|
|
|
|
if (threshold !== undefined) { |
|
|
|
|
this.threshold = threshold; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
addTree(root: LevelItem) { |
|
|
|
|
const stack = [root]; |
|
|
|
|
while (stack.length) { |
|
|
|
|
const current = stack.shift()!; |
|
|
|
|
|
|
|
|
|
if (current.parents?.length) { |
|
|
|
|
this.addItem(current, current.parents[0]); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
if (current.children.length) { |
|
|
|
|
stack.unshift(...current.children); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
addItem(item: LevelItem, parent?: LevelItem) { |
|
|
|
|
// The heuristics here is pretty simple right now. Just check if it's single child and if we are within threshold.
|
|
|
|
|
// We assume items with small self just aren't too important while we cannot really collapse items with siblings
|
|
|
|
|
// as it's not clear what to do with said sibling.
|
|
|
|
|
if (parent && item.value > parent.value * this.threshold && parent.children.length === 1) { |
|
|
|
|
if (this.map.has(parent)) { |
|
|
|
|
const config = this.map.get(parent)!; |
|
|
|
|
this.map.set(item, config); |
|
|
|
|
config.items.push(item); |
|
|
|
|
} else { |
|
|
|
|
const config = { items: [parent, item], collapsed: true }; |
|
|
|
|
this.map.set(parent, config); |
|
|
|
|
this.map.set(item, config); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
getMap() { |
|
|
|
|
return new Map(this.map); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
export function getMessageCheckFieldsResult(wrongFields: CheckFieldsResult) { |
|
|
|
@ -166,9 +201,14 @@ export function checkFields(data: DataFrame): CheckFieldsResult | undefined { |
|
|
|
|
return undefined; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
export type Options = { |
|
|
|
|
collapsing: boolean; |
|
|
|
|
collapsingThreshold?: number; |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
export class FlameGraphDataContainer { |
|
|
|
|
data: DataFrame; |
|
|
|
|
options: { collapsing: boolean }; |
|
|
|
|
options: Options; |
|
|
|
|
|
|
|
|
|
labelField: Field; |
|
|
|
|
levelField: Field; |
|
|
|
@ -187,7 +227,7 @@ export class FlameGraphDataContainer { |
|
|
|
|
private uniqueLabelsMap: Record<string, LevelItem[]> | undefined; |
|
|
|
|
private collapsedMap: Map<LevelItem, CollapseConfig> | undefined; |
|
|
|
|
|
|
|
|
|
constructor(data: DataFrame, options: { collapsing: boolean }, theme: GrafanaTheme2 = createTheme()) { |
|
|
|
|
constructor(data: DataFrame, options: Options, theme: GrafanaTheme2 = createTheme()) { |
|
|
|
|
this.data = data; |
|
|
|
|
this.options = options; |
|
|
|
|
|
|
|
|
|