+ {unitValue} | {ticksVal.text}
+ {ticksVal.suffix} samples ({unitTitle})
+
+ );
+
+ if (sandwichedLabel) {
+ parts.push(
+
-
{data.getLabel(item.itemIndex)}
+
{data.getLabel(item.itemIndexes[0])}
{tooltipData.unitTitle}
@@ -55,8 +55,8 @@ type TooltipData = {
};
export const getTooltipData = (data: FlameGraphDataContainer, item: LevelItem, totalTicks: number): TooltipData => {
- const displayValue = data.getValueDisplay(item.itemIndex);
- const displaySelf = data.getSelfDisplay(item.itemIndex);
+ const displayValue = data.valueDisplayProcessor(item.value);
+ const displaySelf = data.getSelfDisplay(item.itemIndexes);
const percentValue = Math.round(10000 * (displayValue.numeric / totalTicks)) / 100;
const percentSelf = Math.round(10000 * (displaySelf.numeric / totalTicks)) / 100;
@@ -76,7 +76,7 @@ export const getTooltipData = (data: FlameGraphDataContainer, item: LevelItem, t
}
return {
- name: data.getLabel(item.itemIndex),
+ name: data.getLabel(item.itemIndexes[0]),
percentValue,
percentSelf,
unitTitle,
diff --git a/public/app/plugins/panel/flamegraph/components/FlameGraph/__snapshots__/FlameGraph.test.tsx.snap b/public/app/plugins/panel/flamegraph/components/FlameGraph/__snapshots__/FlameGraph.test.tsx.snap
index ff595f45137..49295e1c4a9 100644
--- a/public/app/plugins/panel/flamegraph/components/FlameGraph/__snapshots__/FlameGraph.test.tsx.snap
+++ b/public/app/plugins/panel/flamegraph/components/FlameGraph/__snapshots__/FlameGraph.test.tsx.snap
@@ -2,6 +2,23 @@
exports[`FlameGraph should render correctly 1`] = `
[
+ {
+ "props": {
+ "height": 1100,
+ "width": 1600,
+ "x": 0,
+ "y": 0,
+ },
+ "transform": [
+ 1,
+ 0,
+ 0,
+ 1,
+ 0,
+ 0,
+ ],
+ "type": "clearRect",
+ },
{
"props": {
"path": [
diff --git a/public/app/plugins/panel/flamegraph/flamegraphV2/components/FlameGraph/colors.test.ts b/public/app/plugins/panel/flamegraph/components/FlameGraph/colors.test.ts
similarity index 100%
rename from public/app/plugins/panel/flamegraph/flamegraphV2/components/FlameGraph/colors.test.ts
rename to public/app/plugins/panel/flamegraph/components/FlameGraph/colors.test.ts
diff --git a/public/app/plugins/panel/flamegraph/flamegraphV2/components/FlameGraph/colors.ts b/public/app/plugins/panel/flamegraph/components/FlameGraph/colors.ts
similarity index 100%
rename from public/app/plugins/panel/flamegraph/flamegraphV2/components/FlameGraph/colors.ts
rename to public/app/plugins/panel/flamegraph/components/FlameGraph/colors.ts
diff --git a/public/app/plugins/panel/flamegraph/components/FlameGraph/dataTransform.test.ts b/public/app/plugins/panel/flamegraph/components/FlameGraph/dataTransform.test.ts
index b9a68eea93a..d9e28bb2403 100644
--- a/public/app/plugins/panel/flamegraph/components/FlameGraph/dataTransform.test.ts
+++ b/public/app/plugins/panel/flamegraph/components/FlameGraph/dataTransform.test.ts
@@ -1,9 +1,14 @@
import { createDataFrame } from '@grafana/data';
-import { FlameGraphDataContainer, nestedSetToLevels } from './dataTransform';
+import { FlameGraphDataContainer, LevelItem, nestedSetToLevels } from './dataTransform';
describe('nestedSetToLevels', () => {
it('converts nested set data frame to levels', () => {
+ // [1------]
+ // [2---][6]
+ // [3][5][7]
+ // [4] [8]
+ // [9]
const frame = createDataFrame({
fields: [
{ name: 'level', values: [0, 1, 2, 3, 2, 1, 2, 3, 4] },
@@ -12,24 +17,32 @@ describe('nestedSetToLevels', () => {
{ name: 'self', values: [0, 0, 0, 0, 0, 0, 0, 0, 0] },
],
});
- const levels = nestedSetToLevels(new FlameGraphDataContainer(frame));
- expect(levels).toEqual([
- [{ start: 0, itemIndex: 0 }],
- [
- { start: 0, itemIndex: 1 },
- { start: 5, itemIndex: 5 },
- ],
- [
- { start: 0, itemIndex: 2 },
- { start: 3, itemIndex: 4 },
- { start: 5, itemIndex: 6 },
- ],
- [
- { start: 0, itemIndex: 3 },
- { start: 5, itemIndex: 7 },
- ],
- [{ start: 5, itemIndex: 8 }],
- ]);
+ const [levels] = nestedSetToLevels(new FlameGraphDataContainer(frame));
+
+ const n9: LevelItem = { itemIndexes: [8], start: 5, children: [], value: 1 };
+ const n8: LevelItem = { itemIndexes: [7], start: 5, children: [n9], value: 2 };
+ const n7: LevelItem = { itemIndexes: [6], start: 5, children: [n8], value: 3 };
+ const n6: LevelItem = { itemIndexes: [5], start: 5, children: [n7], value: 4 };
+ const n5: LevelItem = { itemIndexes: [4], start: 3, children: [], value: 1 };
+ const n4: LevelItem = { itemIndexes: [3], start: 0, children: [], value: 1 };
+ const n3: LevelItem = { itemIndexes: [2], start: 0, children: [n4], value: 3 };
+ const n2: LevelItem = { itemIndexes: [1], start: 0, children: [n3, n5], value: 5 };
+ const n1: LevelItem = { itemIndexes: [0], start: 0, children: [n2, n6], value: 10 };
+
+ n2.parents = [n1];
+ n6.parents = [n1];
+ n3.parents = [n2];
+ n5.parents = [n2];
+ n4.parents = [n3];
+ n7.parents = [n6];
+ n8.parents = [n7];
+ n9.parents = [n8];
+
+ expect(levels[0]).toEqual([n1]);
+ expect(levels[1]).toEqual([n2, n6]);
+ expect(levels[2]).toEqual([n3, n5, n7]);
+ expect(levels[3]).toEqual([n4, n8]);
+ expect(levels[4]).toEqual([n9]);
});
it('converts nested set data if multiple same level items', () => {
@@ -41,14 +54,18 @@ describe('nestedSetToLevels', () => {
{ name: 'self', values: [10, 5, 3, 1] },
],
});
- const levels = nestedSetToLevels(new FlameGraphDataContainer(frame));
- expect(levels).toEqual([
- [{ start: 0, itemIndex: 0 }],
- [
- { start: 0, itemIndex: 1 },
- { start: 5, itemIndex: 2 },
- { start: 8, itemIndex: 3 },
- ],
- ]);
+ const [levels] = nestedSetToLevels(new FlameGraphDataContainer(frame));
+
+ const n4: LevelItem = { itemIndexes: [3], start: 8, children: [], value: 1 };
+ const n3: LevelItem = { itemIndexes: [2], start: 5, children: [], value: 3 };
+ const n2: LevelItem = { itemIndexes: [1], start: 0, children: [], value: 5 };
+ const n1: LevelItem = { itemIndexes: [0], start: 0, children: [n2, n3, n4], value: 10 };
+
+ n2.parents = [n1];
+ n3.parents = [n1];
+ n4.parents = [n1];
+
+ expect(levels[0]).toEqual([n1]);
+ expect(levels[1]).toEqual([n2, n3, n4]);
});
});
diff --git a/public/app/plugins/panel/flamegraph/components/FlameGraph/dataTransform.ts b/public/app/plugins/panel/flamegraph/components/FlameGraph/dataTransform.ts
index 598bdfb6b51..0ae5ba48f08 100644
--- a/public/app/plugins/panel/flamegraph/components/FlameGraph/dataTransform.ts
+++ b/public/app/plugins/panel/flamegraph/components/FlameGraph/dataTransform.ts
@@ -10,35 +10,71 @@ import {
import { SampleUnit } from '../types';
-export type LevelItem = { start: number; itemIndex: number };
+import { mergeParentSubtrees, mergeSubtrees } from './treeTransforms';
+
+export type LevelItem = {
+ // Offset from the start of the level.
+ start: number;
+ // Value here can be different from a value of items in the data frame as for callers tree in sandwich view we have
+ // to trim the value to correspond only to the part used by the children in the subtree.
+ value: number;
+ // Index into the data frame. It is an array because for sandwich views we may be merging multiple items into single
+ // node.
+ itemIndexes: number[];
+ children: LevelItem[];
+ parents?: LevelItem[];
+};
/**
* 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): LevelItem[][] {
+export function nestedSetToLevels(container: FlameGraphDataContainer): [LevelItem[][], Record] {
const levels: LevelItem[][] = [];
let offset = 0;
+ let parent: LevelItem | undefined = undefined;
+ const uniqueLabels: Record = {};
+
for (let i = 0; i < container.data.length; i++) {
const currentLevel = container.getLevel(i);
const prevLevel = i > 0 ? container.getLevel(i - 1) : undefined;
levels[currentLevel] = levels[currentLevel] || [];
+
if (prevLevel && prevLevel >= currentLevel) {
// We are going down a level or staying at the same level, so we are adding a sibling to the last item in a level.
// So we have to compute the correct offset based on the last sibling.
- const lastItem = levels[currentLevel][levels[currentLevel].length - 1];
- offset = lastItem.start + container.getValue(lastItem.itemIndex);
+ const lastSibling = levels[currentLevel][levels[currentLevel].length - 1];
+ offset = lastSibling.start + container.getValue(lastSibling.itemIndexes[0]);
+ // we assume there is always a single root node so lastSibling should always have a parent.
+ // Also it has to have the same parent because of how the items are ordered.
+ parent = lastSibling.parents![0];
}
+
const newItem: LevelItem = {
- itemIndex: i,
+ itemIndexes: [i],
+ value: container.getValue(i),
start: offset,
+ parents: parent && [parent],
+ children: [],
};
+ if (uniqueLabels[container.getLabel(i)]) {
+ uniqueLabels[container.getLabel(i)].push(newItem);
+ } else {
+ uniqueLabels[container.getLabel(i)] = [newItem];
+ }
+
+ if (parent) {
+ parent.children.push(newItem);
+ }
+ parent = newItem;
+
levels[currentLevel].push(newItem);
}
- return levels;
+
+ return [levels, uniqueLabels];
}
export class FlameGraphDataContainer {
@@ -52,6 +88,9 @@ export class FlameGraphDataContainer {
valueDisplayProcessor: DisplayProcessor;
uniqueLabels: string[];
+ private levels: LevelItem[][] | undefined;
+ private uniqueLabelsMap: Record | undefined;
+
constructor(data: DataFrame, theme: GrafanaTheme2 = createTheme()) {
this.data = data;
this.labelField = data.fields.find((f) => f.name === 'label')!;
@@ -92,20 +131,26 @@ export class FlameGraphDataContainer {
return this.levelField.values[index];
}
- getValue(index: number) {
- return this.valueField.values[index];
+ getValue(index: number | number[]) {
+ let indexArray: number[] = typeof index === 'number' ? [index] : index;
+ return indexArray.reduce((acc, index) => {
+ return acc + this.valueField.values[index];
+ }, 0);
}
- getValueDisplay(index: number) {
- return this.valueDisplayProcessor(this.valueField.values[index]);
+ getValueDisplay(index: number | number[]) {
+ return this.valueDisplayProcessor(this.getValue(index));
}
- getSelf(index: number) {
- return this.selfField.values[index];
+ getSelf(index: number | number[]) {
+ let indexArray: number[] = typeof index === 'number' ? [index] : index;
+ return indexArray.reduce((acc, index) => {
+ return acc + this.selfField.values[index];
+ }, 0);
}
- getSelfDisplay(index: number) {
- return this.valueDisplayProcessor(this.selfField.values[index]);
+ getSelfDisplay(index: number | number[]) {
+ return this.valueDisplayProcessor(this.getSelf(index));
}
getUniqueLabels() {
@@ -122,4 +167,35 @@ export class FlameGraphDataContainer {
return 'Count';
}
+
+ getLevels() {
+ this.initLevels();
+ return this.levels!;
+ }
+
+ getSandwichLevels(label: string) {
+ const nodes = this.getNodesWithLabel(label);
+
+ if (!nodes?.length) {
+ return [];
+ }
+
+ const callers = mergeParentSubtrees(nodes, this);
+ const callees = mergeSubtrees(nodes, this);
+
+ return [callers, callees];
+ }
+
+ getNodesWithLabel(label: string) {
+ this.initLevels();
+ return this.uniqueLabelsMap![label];
+ }
+
+ private initLevels() {
+ if (!this.levels) {
+ const [levels, uniqueLabelsMap] = nestedSetToLevels(this);
+ this.levels = levels;
+ this.uniqueLabelsMap = uniqueLabelsMap;
+ }
+ }
}
diff --git a/public/app/plugins/panel/flamegraph/flamegraphV2/components/FlameGraph/murmur3.ts b/public/app/plugins/panel/flamegraph/components/FlameGraph/murmur3.ts
similarity index 100%
rename from public/app/plugins/panel/flamegraph/flamegraphV2/components/FlameGraph/murmur3.ts
rename to public/app/plugins/panel/flamegraph/components/FlameGraph/murmur3.ts
diff --git a/public/app/plugins/panel/flamegraph/components/FlameGraph/rendering.test.ts b/public/app/plugins/panel/flamegraph/components/FlameGraph/rendering.test.ts
index 2b4c09d36ff..729dfad1d89 100644
--- a/public/app/plugins/panel/flamegraph/components/FlameGraph/rendering.test.ts
+++ b/public/app/plugins/panel/flamegraph/components/FlameGraph/rendering.test.ts
@@ -14,7 +14,7 @@ function makeDataFrame(fields: Record>) {
describe('getRectDimensionsForLevel', () => {
it('should render a single item', () => {
- const level: LevelItem[] = [{ start: 0, itemIndex: 0 }];
+ const level: LevelItem[] = [{ start: 0, itemIndexes: [0], children: [], value: 100 }];
const container = new FlameGraphDataContainer(makeDataFrame({ value: [100], level: [1], label: ['1'], self: [0] }));
const result = getRectDimensionsForLevel(container, level, 1, 100, 0, 10);
expect(result).toEqual([
@@ -34,9 +34,9 @@ describe('getRectDimensionsForLevel', () => {
it('should render a multiple items', () => {
const level: LevelItem[] = [
- { start: 0, itemIndex: 0 },
- { start: 100, itemIndex: 1 },
- { start: 150, itemIndex: 2 },
+ { start: 0, itemIndexes: [0], children: [], value: 100 },
+ { start: 100, itemIndexes: [1], children: [], value: 50 },
+ { start: 150, itemIndexes: [2], children: [], value: 50 },
];
const container = new FlameGraphDataContainer(
makeDataFrame({ value: [100, 50, 50], level: [2, 2, 2], label: ['1', '2', '3'], self: [0, 0, 0] })
@@ -71,9 +71,9 @@ describe('getRectDimensionsForLevel', () => {
it('should render a collapsed items', () => {
const level: LevelItem[] = [
- { start: 0, itemIndex: 0 },
- { start: 100, itemIndex: 1 },
- { start: 102, itemIndex: 2 },
+ { start: 0, itemIndexes: [0], children: [], value: 100 },
+ { start: 100, itemIndexes: [1], children: [], value: 2 },
+ { start: 102, itemIndexes: [2], children: [], value: 1 },
];
const container = new FlameGraphDataContainer(
makeDataFrame({ value: [100, 2, 1], level: [2, 2, 2], label: ['1', '2', '3'], self: [0, 0, 0] })
diff --git a/public/app/plugins/panel/flamegraph/components/FlameGraph/rendering.ts b/public/app/plugins/panel/flamegraph/components/FlameGraph/rendering.ts
index 7a805409193..3e782d761bd 100644
--- a/public/app/plugins/panel/flamegraph/components/FlameGraph/rendering.ts
+++ b/public/app/plugins/panel/flamegraph/components/FlameGraph/rendering.ts
@@ -1,4 +1,8 @@
-import { colors } from '@grafana/ui';
+import uFuzzy from '@leeoniya/ufuzzy';
+import { RefObject, useEffect, useMemo, useState } from 'react';
+
+import { GrafanaTheme2 } from '@grafana/data';
+import { colors, useTheme2 } from '@grafana/ui';
import {
BAR_BORDER_WIDTH,
@@ -8,10 +12,116 @@ import {
LABEL_THRESHOLD,
PIXELS_PER_LEVEL,
} from '../../constants';
-import { TextAlign } from '../types';
+import { ClickedItemData, ColorScheme, TextAlign } from '../types';
+import { getBarColorByPackage, getBarColorByValue } from './colors';
import { FlameGraphDataContainer, LevelItem } from './dataTransform';
+const ufuzzy = new uFuzzy();
+
+export function useFlameRender(
+ canvasRef: RefObject,
+ data: FlameGraphDataContainer,
+ levels: LevelItem[][],
+ wrapperWidth: number,
+ rangeMin: number,
+ rangeMax: number,
+ search: string,
+ textAlign: TextAlign,
+ totalTicks: number,
+ colorScheme: ColorScheme,
+ focusedItemData?: ClickedItemData
+) {
+ const foundLabels = useMemo(() => {
+ if (search) {
+ const foundLabels = new Set();
+ let idxs = ufuzzy.filter(data.getUniqueLabels(), search);
+
+ if (idxs) {
+ for (let idx of idxs) {
+ foundLabels.add(data.getUniqueLabels()[idx]);
+ }
+ }
+
+ return foundLabels;
+ }
+ // In this case undefined means there was no search so no attempt to highlighting anything should be made.
+ return undefined;
+ }, [search, data]);
+
+ const ctx = useSetupCanvas(canvasRef, wrapperWidth, levels.length);
+ const theme = useTheme2();
+
+ useEffect(() => {
+ if (!ctx) {
+ return;
+ }
+ ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
+ const pixelsPerTick = (wrapperWidth * window.devicePixelRatio) / totalTicks / (rangeMax - rangeMin);
+
+ for (let levelIndex = 0; levelIndex < levels.length; levelIndex++) {
+ const level = levels[levelIndex];
+ // Get all the dimensions of the rectangles for the level. We do this by level instead of per rectangle, because
+ // sometimes we collapse multiple bars into single rect.
+ const dimensions = getRectDimensionsForLevel(data, level, levelIndex, totalTicks, rangeMin, pixelsPerTick);
+ for (const rect of dimensions) {
+ const focusedLevel = focusedItemData ? focusedItemData.level : 0;
+ // Render each rectangle based on the computed dimensions
+ renderRect(
+ ctx,
+ rect,
+ totalTicks,
+ rangeMin,
+ rangeMax,
+ levelIndex,
+ focusedLevel,
+ foundLabels,
+ textAlign,
+ colorScheme,
+ theme
+ );
+ }
+ }
+ }, [
+ ctx,
+ data,
+ levels,
+ wrapperWidth,
+ rangeMin,
+ rangeMax,
+ search,
+ focusedItemData,
+ foundLabels,
+ textAlign,
+ totalTicks,
+ colorScheme,
+ theme,
+ ]);
+}
+
+function useSetupCanvas(canvasRef: RefObject, wrapperWidth: number, numberOfLevels: number) {
+ const [ctx, setCtx] = useState();
+
+ useEffect(() => {
+ if (!(numberOfLevels && canvasRef.current)) {
+ return;
+ }
+ const ctx = canvasRef.current.getContext('2d')!;
+
+ const height = PIXELS_PER_LEVEL * numberOfLevels;
+ canvasRef.current.width = Math.round(wrapperWidth * window.devicePixelRatio);
+ canvasRef.current.height = Math.round(height);
+ canvasRef.current.style.width = `${wrapperWidth}px`;
+ canvasRef.current.style.height = `${height / window.devicePixelRatio}px`;
+
+ ctx.textBaseline = 'middle';
+ ctx.font = 12 * window.devicePixelRatio + 'px monospace';
+ ctx.strokeStyle = 'white';
+ setCtx(ctx);
+ }, [canvasRef, setCtx, wrapperWidth, numberOfLevels]);
+ return ctx;
+}
+
type RectData = {
width: number;
height: number;
@@ -40,7 +150,7 @@ export function getRectDimensionsForLevel(
for (let barIndex = 0; barIndex < level.length; barIndex += 1) {
const item = level[barIndex];
const barX = getBarX(item.start, totalTicks, rangeMin, pixelsPerTick);
- let curBarTicks = data.getValue(item.itemIndex);
+ let curBarTicks = item.value;
// merge very small blocks into big "collapsed" ones for performance
const collapsed = curBarTicks * pixelsPerTick <= COLLAPSE_THRESHOLD;
@@ -48,14 +158,14 @@ export function getRectDimensionsForLevel(
while (
barIndex < level.length - 1 &&
item.start + curBarTicks === level[barIndex + 1].start &&
- data.getValue(level[barIndex + 1].itemIndex) * pixelsPerTick <= COLLAPSE_THRESHOLD
+ level[barIndex + 1].value * pixelsPerTick <= COLLAPSE_THRESHOLD
) {
barIndex += 1;
- curBarTicks += data.getValue(level[barIndex].itemIndex);
+ curBarTicks += level[barIndex].value;
}
}
- const displayValue = data.getValueDisplay(item.itemIndex);
+ const displayValue = data.valueDisplayProcessor(item.value);
let unit = displayValue.suffix ? displayValue.text + displayValue.suffix : displayValue.text;
const width = curBarTicks * pixelsPerTick - (collapsed ? 0 : BAR_BORDER_WIDTH * 2);
@@ -66,9 +176,9 @@ export function getRectDimensionsForLevel(
y: levelIndex * PIXELS_PER_LEVEL,
collapsed,
ticks: curBarTicks,
- label: data.getLabel(item.itemIndex),
+ label: data.getLabel(item.itemIndexes[0]),
unitLabel: unit,
- itemIndex: item.itemIndex,
+ itemIndex: item.itemIndexes[0],
});
}
return coordinatesLevel;
@@ -80,11 +190,12 @@ export function renderRect(
totalTicks: number,
rangeMin: number,
rangeMax: number,
- query: string,
levelIndex: number,
topLevelIndex: number,
- foundNames: Set,
- textAlign: TextAlign
+ foundNames: Set | undefined,
+ textAlign: TextAlign,
+ colorScheme: ColorScheme,
+ theme: GrafanaTheme2
) {
if (rect.width < HIDE_THRESHOLD) {
return;
@@ -93,28 +204,36 @@ export function renderRect(
ctx.beginPath();
ctx.rect(rect.x + (rect.collapsed ? 0 : BAR_BORDER_WIDTH), rect.y, rect.width, rect.height);
- // / (rangeMax - rangeMin) here so when you click a bar it will adjust the top (clicked)bar to the most 'intense' color
- const intensity = Math.min(1, rect.ticks / totalTicks / (rangeMax - rangeMin));
- const h = 50 - 50 * intensity;
- const l = 65 + 7 * intensity;
+ const color =
+ colorScheme === ColorScheme.ValueBased
+ ? getBarColorByValue(rect.ticks, totalTicks, rangeMin, rangeMax)
+ : getBarColorByPackage(rect.label, theme);
- const name = rect.label;
-
- if (!rect.collapsed) {
- ctx.stroke();
-
- if (query) {
- ctx.fillStyle = foundNames.has(name) ? getBarColor(h, l) : colors[55];
- } else {
- ctx.fillStyle = levelIndex > topLevelIndex - 1 ? getBarColor(h, l) : getBarColor(h, l + 15);
- }
+ if (foundNames) {
+ // Means we are searching, we use color for matches and gray the rest
+ ctx.fillStyle = foundNames.has(rect.label) ? color.toHslString() : colors[55];
} else {
- ctx.fillStyle = foundNames.has(name) ? getBarColor(h, l) : colors[55];
+ // No search
+ if (rect.collapsed) {
+ // Collapsed are always grayed
+ ctx.fillStyle = colors[55];
+ } else {
+ // Mute if we are above the focused symbol
+ ctx.fillStyle = levelIndex > topLevelIndex - 1 ? color.toHslString() : color.lighten(15).toHslString();
+ }
}
+
+ if (rect.collapsed) {
+ // Only fill the collapsed rects
+ ctx.fill();
+ return;
+ }
+
+ ctx.stroke();
ctx.fill();
- if (!rect.collapsed && rect.width >= LABEL_THRESHOLD) {
- renderLabel(ctx, name, rect, textAlign);
+ if (rect.width >= LABEL_THRESHOLD) {
+ renderLabel(ctx, rect.label, rect, textAlign);
}
}
@@ -159,7 +278,3 @@ function renderLabel(ctx: CanvasRenderingContext2D, name: string, rect: RectData
export function getBarX(offset: number, totalTicks: number, rangeMin: number, pixelsPerTick: number) {
return (offset - totalTicks * rangeMin) * pixelsPerTick;
}
-
-function getBarColor(h: number, l: number) {
- return `hsl(${h}, 100%, ${l}%)`;
-}
diff --git a/public/app/plugins/panel/flamegraph/flamegraphV2/components/FlameGraph/testHelpers.test.ts b/public/app/plugins/panel/flamegraph/components/FlameGraph/testHelpers.test.ts
similarity index 100%
rename from public/app/plugins/panel/flamegraph/flamegraphV2/components/FlameGraph/testHelpers.test.ts
rename to public/app/plugins/panel/flamegraph/components/FlameGraph/testHelpers.test.ts
diff --git a/public/app/plugins/panel/flamegraph/flamegraphV2/components/FlameGraph/testHelpers.ts b/public/app/plugins/panel/flamegraph/components/FlameGraph/testHelpers.ts
similarity index 100%
rename from public/app/plugins/panel/flamegraph/flamegraphV2/components/FlameGraph/testHelpers.ts
rename to public/app/plugins/panel/flamegraph/components/FlameGraph/testHelpers.ts
diff --git a/public/app/plugins/panel/flamegraph/flamegraphV2/components/FlameGraph/treeTransforms.test.ts b/public/app/plugins/panel/flamegraph/components/FlameGraph/treeTransforms.test.ts
similarity index 100%
rename from public/app/plugins/panel/flamegraph/flamegraphV2/components/FlameGraph/treeTransforms.test.ts
rename to public/app/plugins/panel/flamegraph/components/FlameGraph/treeTransforms.test.ts
diff --git a/public/app/plugins/panel/flamegraph/flamegraphV2/components/FlameGraph/treeTransforms.ts b/public/app/plugins/panel/flamegraph/components/FlameGraph/treeTransforms.ts
similarity index 100%
rename from public/app/plugins/panel/flamegraph/flamegraphV2/components/FlameGraph/treeTransforms.ts
rename to public/app/plugins/panel/flamegraph/components/FlameGraph/treeTransforms.ts
diff --git a/public/app/plugins/panel/flamegraph/components/FlameGraphContainer.tsx b/public/app/plugins/panel/flamegraph/components/FlameGraphContainer.tsx
index 97dbc0dc0f4..d87b8157b8c 100644
--- a/public/app/plugins/panel/flamegraph/components/FlameGraphContainer.tsx
+++ b/public/app/plugins/panel/flamegraph/components/FlameGraphContainer.tsx
@@ -9,10 +9,10 @@ import { useStyles2, useTheme2 } from '@grafana/ui';
import { MIN_WIDTH_TO_SHOW_BOTH_TOPTABLE_AND_FLAMEGRAPH } from '../constants';
import FlameGraph from './FlameGraph/FlameGraph';
-import { FlameGraphDataContainer, LevelItem, nestedSetToLevels } from './FlameGraph/dataTransform';
+import { FlameGraphDataContainer } from './FlameGraph/dataTransform';
import FlameGraphHeader from './FlameGraphHeader';
import FlameGraphTopTableContainer from './TopTable/FlameGraphTopTableContainer';
-import { SelectedView, TextAlign } from './types';
+import { ClickedItemData, ColorScheme, SelectedView, TextAlign } from './types';
type Props = {
data?: DataFrame;
@@ -20,7 +20,7 @@ type Props = {
};
const FlameGraphContainer = (props: Props) => {
- const [focusedItemIndex, setFocusedItemIndex] = useState();
+ const [focusedItemData, setFocusedItemData] = useState();
const [rangeMin, setRangeMin] = useState(0);
const [rangeMax, setRangeMax] = useState(1);
@@ -28,19 +28,17 @@ const FlameGraphContainer = (props: Props) => {
const [selectedView, setSelectedView] = useState(SelectedView.Both);
const [sizeRef, { width: containerWidth }] = useMeasure();
const [textAlign, setTextAlign] = useState('left');
+ // This is a label of the item because in sandwich view we group all items by label and present a merged graph
+ const [sandwichItem, setSandwichItem] = useState();
+ const [colorScheme, setColorScheme] = useState(ColorScheme.ValueBased);
const theme = useTheme2();
- const [dataContainer, levels] = useMemo((): [FlameGraphDataContainer, LevelItem[][]] | [undefined, undefined] => {
+ const dataContainer = useMemo((): FlameGraphDataContainer | undefined => {
if (!props.data) {
- return [undefined, undefined];
+ return;
}
- const container = new FlameGraphDataContainer(props.data, theme);
-
- // Transform dataFrame with nested set format to array of levels. Each level contains all the bars for a particular
- // level of the flame graph. We do this temporary as in the end we should be able to render directly by iterating
- // over the dataFrame rows.
- return [container, nestedSetToLevels(container)];
+ return new FlameGraphDataContainer(props.data, theme);
}, [props.data, theme]);
const styles = useStyles2(getStyles);
@@ -56,10 +54,19 @@ const FlameGraphContainer = (props: Props) => {
}
}, [selectedView, setSelectedView, containerWidth]);
- useEffect(() => {
- setFocusedItemIndex(undefined);
+ function resetFocus() {
+ setFocusedItemData(undefined);
setRangeMin(0);
setRangeMax(1);
+ }
+
+ function resetSandwich() {
+ setSandwichItem(undefined);
+ }
+
+ useEffect(() => {
+ resetFocus();
+ resetSandwich();
}, [props.data]);
return (
@@ -74,12 +81,14 @@ const FlameGraphContainer = (props: Props) => {
setSelectedView={setSelectedView}
containerWidth={containerWidth}
onReset={() => {
- setRangeMin(0);
- setRangeMax(1);
- setFocusedItemIndex(undefined);
+ resetFocus();
+ resetSandwich();
}}
textAlign={textAlign}
onTextAlignChange={setTextAlign}
+ showResetButton={Boolean(focusedItemData || sandwichItem)}
+ colorScheme={colorScheme}
+ onColorSchemeChange={setColorScheme}
/>
@@ -87,7 +96,6 @@ const FlameGraphContainer = (props: Props) => {
{
if (search === symbol) {
setSearch('');
@@ -97,27 +105,31 @@ const FlameGraphContainer = (props: Props) => {
grafana_version: config.buildInfo.version,
});
setSearch(symbol);
- // Reset selected level in flamegraph when selecting row in top table
- setRangeMin(0);
- setRangeMax(1);
}
}}
+ height={selectedView === SelectedView.TopTable ? 600 : undefined}
/>
)}
{selectedView !== SelectedView.TopTable && (
setFocusedItemIndex(itemIndex)}
- focusedItemIndex={focusedItemIndex}
+ onItemFocused={(data) => setFocusedItemData(data)}
+ focusedItemData={focusedItemData}
textAlign={textAlign}
+ sandwichItem={sandwichItem}
+ onSandwich={(label: string) => {
+ resetFocus();
+ setSandwichItem(label);
+ }}
+ onFocusPillClick={resetFocus}
+ onSandwichPillClick={resetSandwich}
+ colorScheme={colorScheme}
/>
)}
diff --git a/public/app/plugins/panel/flamegraph/components/FlameGraphHeader.test.tsx b/public/app/plugins/panel/flamegraph/components/FlameGraphHeader.test.tsx
index a576a1dc063..520ff81b54b 100644
--- a/public/app/plugins/panel/flamegraph/components/FlameGraphHeader.test.tsx
+++ b/public/app/plugins/panel/flamegraph/components/FlameGraphHeader.test.tsx
@@ -1,40 +1,75 @@
import '@testing-library/jest-dom';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
-import React, { useState } from 'react';
+import React from 'react';
import { CoreApp } from '@grafana/data';
import FlameGraphHeader from './FlameGraphHeader';
-import { SelectedView } from './types';
+import { ColorScheme, SelectedView } from './types';
describe('FlameGraphHeader', () => {
- const FlameGraphHeaderWithProps = () => {
- const [search, setSearch] = useState('');
- const [selectedView, setSelectedView] = useState(SelectedView.Both);
+ function setup(props: Partial> = {}) {
+ const setSearch = jest.fn();
+ const setSelectedView = jest.fn();
+ const onReset = jest.fn();
+ const onSchemeChange = jest.fn();
- return (
+ const renderResult = render(
{
- setSearch('');
- }}
+ onReset={onReset}
onTextAlignChange={jest.fn()}
textAlign={'left'}
+ showResetButton={true}
+ colorScheme={ColorScheme.ValueBased}
+ onColorSchemeChange={onSchemeChange}
+ {...props}
/>
);
- };
- it('reset button should remove search text', async () => {
- render();
- await userEvent.type(screen.getByPlaceholderText('Search..'), 'abc');
- expect(screen.getByDisplayValue('abc')).toBeInTheDocument();
- await userEvent.click(screen.getByRole('button', { name: /Reset/i }));
- expect(screen.queryByDisplayValue('abc')).not.toBeInTheDocument();
+ return {
+ renderResult,
+ handlers: {
+ setSearch,
+ setSelectedView,
+ onReset,
+ onSchemeChange,
+ },
+ };
+ }
+
+ it('show reset button when needed', async () => {
+ setup({ showResetButton: false });
+ expect(screen.queryByLabelText(/Reset focus/)).toBeNull();
+
+ setup();
+ expect(screen.getByLabelText(/Reset focus/)).toBeInTheDocument();
+ });
+
+ it('calls on reset when reset button is clicked', async () => {
+ const { handlers } = setup();
+ const resetButton = screen.getByLabelText(/Reset focus/);
+ expect(resetButton).toBeInTheDocument();
+ await userEvent.click(resetButton);
+ expect(handlers.onReset).toHaveBeenCalledTimes(1);
+ });
+
+ it('calls on color scheme change when clicked', async () => {
+ const { handlers } = setup();
+ const changeButton = screen.getByLabelText(/Change color scheme/);
+ expect(changeButton).toBeInTheDocument();
+ await userEvent.click(changeButton);
+
+ const byPackageButton = screen.getByText(/By package name/);
+ expect(byPackageButton).toBeInTheDocument();
+ await userEvent.click(byPackageButton);
+
+ expect(handlers.onSchemeChange).toHaveBeenCalledTimes(1);
});
});
diff --git a/public/app/plugins/panel/flamegraph/components/FlameGraphHeader.tsx b/public/app/plugins/panel/flamegraph/components/FlameGraphHeader.tsx
index 3dd0d068260..0167d46cb51 100644
--- a/public/app/plugins/panel/flamegraph/components/FlameGraphHeader.tsx
+++ b/public/app/plugins/panel/flamegraph/components/FlameGraphHeader.tsx
@@ -1,16 +1,16 @@
-import { css } from '@emotion/css';
+import { css, cx } from '@emotion/css';
import React, { useEffect, useState } from 'react';
import useDebounce from 'react-use/lib/useDebounce';
import usePrevious from 'react-use/lib/usePrevious';
import { CoreApp, GrafanaTheme2, SelectableValue } from '@grafana/data';
-import { reportInteraction } from '@grafana/runtime';
-import { Button, Input, RadioButtonGroup, useStyles2 } from '@grafana/ui';
+import { reportInteraction, config } from '@grafana/runtime';
+import { Button, Dropdown, Input, Menu, RadioButtonGroup, useStyles2 } from '@grafana/ui';
-import { config } from '../../../../core/config';
import { MIN_WIDTH_TO_SHOW_BOTH_TOPTABLE_AND_FLAMEGRAPH } from '../constants';
-import { SelectedView, TextAlign } from './types';
+import { byPackageGradient, byValueGradient } from './FlameGraph/colors';
+import { ColorScheme, SelectedView, TextAlign } from './types';
type Props = {
app: CoreApp;
@@ -22,6 +22,9 @@ type Props = {
onReset: () => void;
textAlign: TextAlign;
onTextAlignChange: (align: TextAlign) => void;
+ showResetButton: boolean;
+ colorScheme: ColorScheme;
+ onColorSchemeChange: (colorScheme: ColorScheme) => void;
};
const FlameGraphHeader = ({
@@ -34,6 +37,9 @@ const FlameGraphHeader = ({
onReset,
textAlign,
onTextAlignChange,
+ showResetButton,
+ colorScheme,
+ onColorSchemeChange,
}: Props) => {
const styles = useStyles2((theme) => getStyles(theme, app));
function interaction(name: string, context: Record) {
@@ -46,34 +52,52 @@ const FlameGraphHeader = ({
const [localSearch, setLocalSearch] = useSearchInput(search, setSearch);
+ const suffix =
+ localSearch !== '' ? (
+
+ ) : null;
+
return (
-
-
- {
- setLocalSearch(v.currentTarget.value);
- }}
- placeholder={'Search..'}
- width={44}
- />
-
-