mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Flamegraph: Update threshold for collapsing and fix flickering (#78206)
This commit is contained in:
parent
7cec741bae
commit
9777da5502
@ -17,7 +17,7 @@
|
|||||||
// TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
|
// TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
|
||||||
// THIS SOFTWARE.
|
// THIS SOFTWARE.
|
||||||
import { css, cx } from '@emotion/css';
|
import { css, cx } from '@emotion/css';
|
||||||
import React, { useEffect, useMemo, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
|
|
||||||
import { Icon } from '@grafana/ui';
|
import { Icon } from '@grafana/ui';
|
||||||
|
|
||||||
@ -26,7 +26,7 @@ import { ClickedItemData, ColorScheme, ColorSchemeDiff, TextAlign } from '../typ
|
|||||||
|
|
||||||
import FlameGraphCanvas from './FlameGraphCanvas';
|
import FlameGraphCanvas from './FlameGraphCanvas';
|
||||||
import FlameGraphMetadata from './FlameGraphMetadata';
|
import FlameGraphMetadata from './FlameGraphMetadata';
|
||||||
import { CollapsedMap, FlameGraphDataContainer } from './dataTransform';
|
import { CollapsedMap, FlameGraphDataContainer, LevelItem } from './dataTransform';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
data: FlameGraphDataContainer;
|
data: FlameGraphDataContainer;
|
||||||
@ -66,30 +66,42 @@ const FlameGraph = ({
|
|||||||
}: Props) => {
|
}: Props) => {
|
||||||
const styles = getStyles();
|
const styles = getStyles();
|
||||||
|
|
||||||
const [collapsedMap, setCollapsedMap] = useState<CollapsedMap>(data ? data.getCollapsedMap() : new Map());
|
const [collapsedMap, setCollapsedMap] = useState<CollapsedMap>(new Map());
|
||||||
|
const [levels, setLevels] = useState<LevelItem[][]>();
|
||||||
|
const [levelsCallers, setLevelsCallers] = useState<LevelItem[][]>();
|
||||||
|
const [totalProfileTicks, setTotalProfileTicks] = useState<number>(0);
|
||||||
|
const [totalProfileTicksRight, setTotalProfileTicksRight] = useState<number>();
|
||||||
|
const [totalViewTicks, setTotalViewTicks] = useState<number>(0);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (data) {
|
if (data) {
|
||||||
setCollapsedMap(data.getCollapsedMap());
|
setCollapsedMap(data.getCollapsedMap());
|
||||||
}
|
|
||||||
}, [data]);
|
|
||||||
|
|
||||||
const [levels, levelsCallers, totalProfileTicks, totalProfileTicksRight, totalViewTicks] = useMemo(() => {
|
let levels = data.getLevels();
|
||||||
let levels = data.getLevels();
|
let totalProfileTicks = levels.length ? levels[0][0].value : 0;
|
||||||
let totalProfileTicks = levels.length ? levels[0][0].value : 0;
|
let totalProfileTicksRight = levels.length ? levels[0][0].valueRight : undefined;
|
||||||
let totalProfileTicksRight = levels.length ? levels[0][0].valueRight : undefined;
|
let totalViewTicks = totalProfileTicks;
|
||||||
let totalViewTicks = totalProfileTicks;
|
let levelsCallers = undefined;
|
||||||
let levelsCallers = undefined;
|
|
||||||
|
|
||||||
if (sandwichItem) {
|
if (sandwichItem) {
|
||||||
const [callers, callees] = data.getSandwichLevels(sandwichItem);
|
const [callers, callees] = data.getSandwichLevels(sandwichItem);
|
||||||
levels = callees;
|
levels = callees;
|
||||||
levelsCallers = callers;
|
levelsCallers = callers;
|
||||||
// We need this separate as in case of diff profile we want to compute diff colors based on the original ticks.
|
// We need this separate as in case of diff profile we want to compute diff colors based on the original ticks.
|
||||||
totalViewTicks = callees[0]?.[0]?.value ?? 0;
|
totalViewTicks = callees[0]?.[0]?.value ?? 0;
|
||||||
|
}
|
||||||
|
setLevels(levels);
|
||||||
|
setLevelsCallers(levelsCallers);
|
||||||
|
setTotalProfileTicks(totalProfileTicks);
|
||||||
|
setTotalProfileTicksRight(totalProfileTicksRight);
|
||||||
|
setTotalViewTicks(totalViewTicks);
|
||||||
}
|
}
|
||||||
return [levels, levelsCallers, totalProfileTicks, totalProfileTicksRight, totalViewTicks];
|
|
||||||
}, [data, sandwichItem]);
|
}, [data, sandwichItem]);
|
||||||
|
|
||||||
|
if (!levels) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const commonCanvasProps = {
|
const commonCanvasProps = {
|
||||||
data,
|
data,
|
||||||
rangeMin,
|
rangeMin,
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -1,6 +1,6 @@
|
|||||||
import { createDataFrame, FieldType } from '@grafana/data';
|
import { createDataFrame, FieldType } from '@grafana/data';
|
||||||
|
|
||||||
import { FlameGraphDataContainer, LevelItem, nestedSetToLevels } from './dataTransform';
|
import { CollapsedMapContainer, FlameGraphDataContainer, LevelItem, nestedSetToLevels } from './dataTransform';
|
||||||
import { textToDataContainer } from './testHelpers';
|
import { textToDataContainer } from './testHelpers';
|
||||||
|
|
||||||
describe('nestedSetToLevels', () => {
|
describe('nestedSetToLevels', () => {
|
||||||
@ -90,6 +90,20 @@ describe('FlameGraphDataContainer', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('creates correct collapse map 2', () => {
|
||||||
|
// Should not create any groups because even though the 1 is within threshold it has a sibling
|
||||||
|
const container = textToDataContainer(
|
||||||
|
`
|
||||||
|
[0////////////////////////////////]
|
||||||
|
[1/////////////////////////////][2]
|
||||||
|
`,
|
||||||
|
{ collapsing: true, collapsingThreshold: 0.5 }
|
||||||
|
)!;
|
||||||
|
|
||||||
|
const collapsedMap = container.getCollapsedMap();
|
||||||
|
expect(Array.from(collapsedMap.keys()).length).toEqual(0);
|
||||||
|
});
|
||||||
|
|
||||||
it('creates empty collapse map if no items are similar', () => {
|
it('creates empty collapse map if no items are similar', () => {
|
||||||
const container = textToDataContainer(`
|
const container = textToDataContainer(`
|
||||||
[0//////////////]
|
[0//////////////]
|
||||||
@ -101,3 +115,81 @@ describe('FlameGraphDataContainer', () => {
|
|||||||
expect(Array.from(collapsedMap.keys()).length).toEqual(0);
|
expect(Array.from(collapsedMap.keys()).length).toEqual(0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('CollapsedMapContainer', () => {
|
||||||
|
const defaultItem: LevelItem = {
|
||||||
|
itemIndexes: [0],
|
||||||
|
value: 100,
|
||||||
|
level: 0,
|
||||||
|
children: [],
|
||||||
|
start: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
it('groups items if they are within value threshold', () => {
|
||||||
|
const container = new CollapsedMapContainer();
|
||||||
|
|
||||||
|
const child2: LevelItem = {
|
||||||
|
...defaultItem,
|
||||||
|
itemIndexes: [2],
|
||||||
|
value: 99.1,
|
||||||
|
};
|
||||||
|
|
||||||
|
const child1: LevelItem = {
|
||||||
|
...defaultItem,
|
||||||
|
itemIndexes: [1],
|
||||||
|
children: [child2],
|
||||||
|
};
|
||||||
|
|
||||||
|
const parent: LevelItem = {
|
||||||
|
...defaultItem,
|
||||||
|
children: [child1],
|
||||||
|
};
|
||||||
|
|
||||||
|
container.addItem(child1, parent);
|
||||||
|
container.addItem(child2, child1);
|
||||||
|
expect(container.getMap().get(child1)).toMatchObject({ collapsed: true, items: [parent, child1, child2] });
|
||||||
|
expect(container.getMap().get(child2)).toMatchObject({ collapsed: true, items: [parent, child1, child2] });
|
||||||
|
expect(container.getMap().get(parent)).toMatchObject({ collapsed: true, items: [parent, child1, child2] });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("doesn't group items if they are outside value threshold", () => {
|
||||||
|
const container = new CollapsedMapContainer();
|
||||||
|
|
||||||
|
const parent: LevelItem = {
|
||||||
|
...defaultItem,
|
||||||
|
};
|
||||||
|
|
||||||
|
const child: LevelItem = {
|
||||||
|
...defaultItem,
|
||||||
|
itemIndexes: [1],
|
||||||
|
value: 98,
|
||||||
|
};
|
||||||
|
|
||||||
|
container.addItem(child, parent);
|
||||||
|
expect(container.getMap().size).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("doesn't group items if parent has multiple children", () => {
|
||||||
|
const container = new CollapsedMapContainer();
|
||||||
|
|
||||||
|
const child1: LevelItem = {
|
||||||
|
...defaultItem,
|
||||||
|
itemIndexes: [1],
|
||||||
|
value: 99.1,
|
||||||
|
};
|
||||||
|
|
||||||
|
const child2: LevelItem = {
|
||||||
|
...defaultItem,
|
||||||
|
itemIndexes: [2],
|
||||||
|
value: 0.09,
|
||||||
|
};
|
||||||
|
|
||||||
|
const parent: LevelItem = {
|
||||||
|
...defaultItem,
|
||||||
|
children: [child1, child2],
|
||||||
|
};
|
||||||
|
|
||||||
|
container.addItem(child1, parent);
|
||||||
|
expect(container.getMap().size).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
@ -34,24 +34,19 @@ export type CollapseConfig = {
|
|||||||
collapsed: boolean;
|
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
|
* Convert data frame with nested set format into array of level. This is mainly done for compatibility with current
|
||||||
* rendering code.
|
* rendering code.
|
||||||
*/
|
*/
|
||||||
export function nestedSetToLevels(
|
export function nestedSetToLevels(
|
||||||
container: FlameGraphDataContainer,
|
container: FlameGraphDataContainer,
|
||||||
options?: {
|
options?: Options
|
||||||
collapsing: boolean;
|
|
||||||
}
|
|
||||||
): [LevelItem[][], Record<string, LevelItem[]>, CollapsedMap] {
|
): [LevelItem[][], Record<string, LevelItem[]>, CollapsedMap] {
|
||||||
const levels: LevelItem[][] = [];
|
const levels: LevelItem[][] = [];
|
||||||
let offset = 0;
|
let offset = 0;
|
||||||
|
|
||||||
let parent: LevelItem | undefined = undefined;
|
let parent: LevelItem | undefined = undefined;
|
||||||
const uniqueLabels: Record<string, LevelItem[]> = {};
|
const uniqueLabels: Record<string, LevelItem[]> = {};
|
||||||
const collapsedMap: CollapsedMap = new Map();
|
|
||||||
|
|
||||||
for (let i = 0; i < container.data.length; i++) {
|
for (let i = 0; i < container.data.length; i++) {
|
||||||
const currentLevel = container.getLevel(i);
|
const currentLevel = container.getLevel(i);
|
||||||
@ -82,22 +77,6 @@ export function nestedSetToLevels(
|
|||||||
level: currentLevel,
|
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)]) {
|
if (uniqueLabels[container.getLabel(i)]) {
|
||||||
uniqueLabels[container.getLabel(i)].push(newItem);
|
uniqueLabels[container.getLabel(i)].push(newItem);
|
||||||
} else {
|
} else {
|
||||||
@ -107,12 +86,68 @@ export function nestedSetToLevels(
|
|||||||
if (parent) {
|
if (parent) {
|
||||||
parent.children.push(newItem);
|
parent.children.push(newItem);
|
||||||
}
|
}
|
||||||
parent = newItem;
|
|
||||||
|
|
||||||
|
parent = newItem;
|
||||||
levels[currentLevel].push(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) {
|
export function getMessageCheckFieldsResult(wrongFields: CheckFieldsResult) {
|
||||||
@ -166,9 +201,14 @@ export function checkFields(data: DataFrame): CheckFieldsResult | undefined {
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type Options = {
|
||||||
|
collapsing: boolean;
|
||||||
|
collapsingThreshold?: number;
|
||||||
|
};
|
||||||
|
|
||||||
export class FlameGraphDataContainer {
|
export class FlameGraphDataContainer {
|
||||||
data: DataFrame;
|
data: DataFrame;
|
||||||
options: { collapsing: boolean };
|
options: Options;
|
||||||
|
|
||||||
labelField: Field;
|
labelField: Field;
|
||||||
levelField: Field;
|
levelField: Field;
|
||||||
@ -187,7 +227,7 @@ export class FlameGraphDataContainer {
|
|||||||
private uniqueLabelsMap: Record<string, LevelItem[]> | undefined;
|
private uniqueLabelsMap: Record<string, LevelItem[]> | undefined;
|
||||||
private collapsedMap: Map<LevelItem, CollapseConfig> | 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.data = data;
|
||||||
this.options = options;
|
this.options = options;
|
||||||
|
|
||||||
|
@ -28,7 +28,7 @@ describe('walkTree', () => {
|
|||||||
it('correctly compute sizes for a single item', () => {
|
it('correctly compute sizes for a single item', () => {
|
||||||
const root: LevelItem = { start: 0, itemIndexes: [0], children: [], value: 100, level: 0 };
|
const root: LevelItem = { start: 0, itemIndexes: [0], children: [], value: 100, level: 0 };
|
||||||
const container = new FlameGraphDataContainer(
|
const container = new FlameGraphDataContainer(
|
||||||
makeDataFrame({ value: [100], level: [1], label: ['1'], self: [0] }),
|
makeDataFrame({ value: [100], level: [0], label: ['1'], self: [0] }),
|
||||||
{ collapsing: true }
|
{ collapsing: true }
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { arrayToDataFrame, FieldType } from '@grafana/data';
|
import { arrayToDataFrame, FieldType } from '@grafana/data';
|
||||||
|
|
||||||
import { FlameGraphDataContainer, LevelItem } from './dataTransform';
|
import { FlameGraphDataContainer, LevelItem, Options } from './dataTransform';
|
||||||
|
|
||||||
// Convert text to a FlameGraphDataContainer for testing. The format representing the flamegraph for example:
|
// Convert text to a FlameGraphDataContainer for testing. The format representing the flamegraph for example:
|
||||||
// [0///////]
|
// [0///////]
|
||||||
@ -9,7 +9,7 @@ import { FlameGraphDataContainer, LevelItem } from './dataTransform';
|
|||||||
// [3] [6]
|
// [3] [6]
|
||||||
// [7]
|
// [7]
|
||||||
// Each node starts with [ ends with ], single digit is used for label and the length of a node is it's value.
|
// Each node starts with [ ends with ], single digit is used for label and the length of a node is it's value.
|
||||||
export function textToDataContainer(text: string) {
|
export function textToDataContainer(text: string, options?: Options) {
|
||||||
const levels = text.split('\n');
|
const levels = text.split('\n');
|
||||||
|
|
||||||
if (levels.length === 0) {
|
if (levels.length === 0) {
|
||||||
@ -80,7 +80,7 @@ export function textToDataContainer(text: string) {
|
|||||||
const df = arrayToDataFrame(dfSorted);
|
const df = arrayToDataFrame(dfSorted);
|
||||||
const labelField = df.fields.find((f) => f.name === 'label')!;
|
const labelField = df.fields.find((f) => f.name === 'label')!;
|
||||||
labelField.type = FieldType.string;
|
labelField.type = FieldType.string;
|
||||||
return new FlameGraphDataContainer(df, { collapsing: true });
|
return new FlameGraphDataContainer(df, options || { collapsing: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
export function trimLevelsString(s: string) {
|
export function trimLevelsString(s: string) {
|
||||||
|
Loading…
Reference in New Issue
Block a user