mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
FlameGraph: Refactor handling of the labels (#65491)
This commit is contained in:
parent
609a771874
commit
db6694994f
@ -2,12 +2,12 @@ import { fireEvent, screen } from '@testing-library/dom';
|
|||||||
import { render } from '@testing-library/react';
|
import { render } from '@testing-library/react';
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
|
|
||||||
import { CoreApp, DataFrameView, MutableDataFrame } from '@grafana/data';
|
import { CoreApp, MutableDataFrame } from '@grafana/data';
|
||||||
|
|
||||||
import { SelectedView } from '../types';
|
import { SelectedView } from '../types';
|
||||||
|
|
||||||
import FlameGraph from './FlameGraph';
|
import FlameGraph from './FlameGraph';
|
||||||
import { Item, nestedSetToLevels } from './dataTransform';
|
import { FlameGraphDataContainer, nestedSetToLevels } from './dataTransform';
|
||||||
import { data } from './testData/dataNestedSet';
|
import { data } from './testData/dataNestedSet';
|
||||||
|
|
||||||
import 'jest-canvas-mock';
|
import 'jest-canvas-mock';
|
||||||
@ -29,12 +29,12 @@ describe('FlameGraph', () => {
|
|||||||
const [selectedView, _] = useState(SelectedView.Both);
|
const [selectedView, _] = useState(SelectedView.Both);
|
||||||
|
|
||||||
const flameGraphData = new MutableDataFrame(data);
|
const flameGraphData = new MutableDataFrame(data);
|
||||||
const dataView = new DataFrameView<Item>(flameGraphData);
|
const container = new FlameGraphDataContainer(flameGraphData);
|
||||||
const levels = nestedSetToLevels(dataView);
|
const levels = nestedSetToLevels(container);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FlameGraph
|
<FlameGraph
|
||||||
data={flameGraphData}
|
data={container}
|
||||||
app={CoreApp.Explore}
|
app={CoreApp.Explore}
|
||||||
levels={levels}
|
levels={levels}
|
||||||
topLevelIndex={topLevelIndex}
|
topLevelIndex={topLevelIndex}
|
||||||
@ -47,7 +47,6 @@ describe('FlameGraph', () => {
|
|||||||
setRangeMin={setRangeMin}
|
setRangeMin={setRangeMin}
|
||||||
setRangeMax={setRangeMax}
|
setRangeMax={setRangeMax}
|
||||||
selectedView={selectedView}
|
selectedView={selectedView}
|
||||||
getLabelValue={(val) => val.toString()}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -21,22 +21,22 @@ import uFuzzy from '@leeoniya/ufuzzy';
|
|||||||
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { useMeasure } from 'react-use';
|
import { useMeasure } from 'react-use';
|
||||||
|
|
||||||
import { CoreApp, createTheme, DataFrame, FieldType, getDisplayProcessor } from '@grafana/data';
|
import { CoreApp } from '@grafana/data';
|
||||||
|
|
||||||
import { PIXELS_PER_LEVEL } from '../../constants';
|
import { PIXELS_PER_LEVEL } from '../../constants';
|
||||||
import { TooltipData, SelectedView, ContextMenuData } from '../types';
|
import { SelectedView, ContextMenuData } from '../types';
|
||||||
|
|
||||||
import FlameGraphContextMenu from './FlameGraphContextMenu';
|
import FlameGraphContextMenu from './FlameGraphContextMenu';
|
||||||
import FlameGraphMetadata from './FlameGraphMetadata';
|
import FlameGraphMetadata from './FlameGraphMetadata';
|
||||||
import FlameGraphTooltip, { getTooltipData } from './FlameGraphTooltip';
|
import FlameGraphTooltip from './FlameGraphTooltip';
|
||||||
import { ItemWithStart } from './dataTransform';
|
import { FlameGraphDataContainer, LevelItem } from './dataTransform';
|
||||||
import { getBarX, getRectDimensionsForLevel, renderRect } from './rendering';
|
import { getBarX, getRectDimensionsForLevel, renderRect } from './rendering';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
data: DataFrame;
|
data: FlameGraphDataContainer;
|
||||||
app: CoreApp;
|
app: CoreApp;
|
||||||
flameGraphHeight?: number;
|
flameGraphHeight?: number;
|
||||||
levels: ItemWithStart[][];
|
levels: LevelItem[][];
|
||||||
topLevelIndex: number;
|
topLevelIndex: number;
|
||||||
selectedBarIndex: number;
|
selectedBarIndex: number;
|
||||||
rangeMin: number;
|
rangeMin: number;
|
||||||
@ -48,7 +48,6 @@ type Props = {
|
|||||||
setRangeMax: (range: number) => void;
|
setRangeMax: (range: number) => void;
|
||||||
selectedView: SelectedView;
|
selectedView: SelectedView;
|
||||||
style?: React.CSSProperties;
|
style?: React.CSSProperties;
|
||||||
getLabelValue: (label: string | number) => string;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const FlameGraph = ({
|
const FlameGraph = ({
|
||||||
@ -66,52 +65,35 @@ const FlameGraph = ({
|
|||||||
setRangeMin,
|
setRangeMin,
|
||||||
setRangeMax,
|
setRangeMax,
|
||||||
selectedView,
|
selectedView,
|
||||||
getLabelValue,
|
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const styles = getStyles(selectedView, app, flameGraphHeight);
|
const styles = getStyles(selectedView, app, flameGraphHeight);
|
||||||
const totalTicks = data.fields[1].values.get(0);
|
const totalTicks = data.getValue(0);
|
||||||
const valueField =
|
|
||||||
data.fields.find((f) => f.name === 'value') ?? data.fields.find((f) => f.type === FieldType.number);
|
|
||||||
|
|
||||||
if (!valueField) {
|
|
||||||
throw new Error('Malformed dataFrame: value field of type number is not in the query response');
|
|
||||||
}
|
|
||||||
|
|
||||||
const [sizeRef, { width: wrapperWidth }] = useMeasure<HTMLDivElement>();
|
const [sizeRef, { width: wrapperWidth }] = useMeasure<HTMLDivElement>();
|
||||||
const graphRef = useRef<HTMLCanvasElement>(null);
|
const graphRef = useRef<HTMLCanvasElement>(null);
|
||||||
const tooltipRef = useRef<HTMLDivElement>(null);
|
const tooltipRef = useRef<HTMLDivElement>(null);
|
||||||
const [tooltipData, setTooltipData] = useState<TooltipData>();
|
const [tooltipItem, setTooltipItem] = useState<LevelItem>();
|
||||||
const [contextMenuData, setContextMenuData] = useState<ContextMenuData>();
|
const [contextMenuData, setContextMenuData] = useState<ContextMenuData>();
|
||||||
|
|
||||||
const [ufuzzy] = useState(() => {
|
const [ufuzzy] = useState(() => {
|
||||||
return new uFuzzy();
|
return new uFuzzy();
|
||||||
});
|
});
|
||||||
|
|
||||||
const uniqueLabels = useMemo(() => {
|
|
||||||
const labelField = data.fields.find((f) => f.name === 'label');
|
|
||||||
const enumConfig = labelField?.config?.type?.enum;
|
|
||||||
if (enumConfig) {
|
|
||||||
return enumConfig.text || [];
|
|
||||||
} else {
|
|
||||||
return [...new Set<string>(labelField?.values.toArray())];
|
|
||||||
}
|
|
||||||
}, [data]);
|
|
||||||
|
|
||||||
const foundLabels = useMemo(() => {
|
const foundLabels = useMemo(() => {
|
||||||
const foundLabels = new Set<string>();
|
const foundLabels = new Set<string>();
|
||||||
|
|
||||||
if (search) {
|
if (search) {
|
||||||
let idxs = ufuzzy.filter(uniqueLabels, search);
|
let idxs = ufuzzy.filter(data.getUniqueLabels(), search);
|
||||||
|
|
||||||
if (idxs) {
|
if (idxs) {
|
||||||
for (let idx of idxs) {
|
for (let idx of idxs) {
|
||||||
foundLabels.add(uniqueLabels[idx]);
|
foundLabels.add(data.getUniqueLabels()[idx]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return foundLabels;
|
return foundLabels;
|
||||||
}, [ufuzzy, search, uniqueLabels]);
|
}, [ufuzzy, search, data]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!levels.length) {
|
if (!levels.length) {
|
||||||
@ -131,48 +113,25 @@ const FlameGraph = ({
|
|||||||
ctx.font = 12 * window.devicePixelRatio + 'px monospace';
|
ctx.font = 12 * window.devicePixelRatio + 'px monospace';
|
||||||
ctx.strokeStyle = 'white';
|
ctx.strokeStyle = 'white';
|
||||||
|
|
||||||
const processor = getDisplayProcessor({
|
|
||||||
field: valueField,
|
|
||||||
theme: createTheme() /* theme does not matter for us here */,
|
|
||||||
});
|
|
||||||
|
|
||||||
for (let levelIndex = 0; levelIndex < levels.length; levelIndex++) {
|
for (let levelIndex = 0; levelIndex < levels.length; levelIndex++) {
|
||||||
const level = levels[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
|
// 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.
|
// sometimes we collapse multiple bars into single rect.
|
||||||
const dimensions = getRectDimensionsForLevel(
|
const dimensions = getRectDimensionsForLevel(data, level, levelIndex, totalTicks, rangeMin, pixelsPerTick);
|
||||||
level,
|
|
||||||
levelIndex,
|
|
||||||
totalTicks,
|
|
||||||
rangeMin,
|
|
||||||
pixelsPerTick,
|
|
||||||
processor,
|
|
||||||
getLabelValue
|
|
||||||
);
|
|
||||||
for (const rect of dimensions) {
|
for (const rect of dimensions) {
|
||||||
// Render each rectangle based on the computed dimensions
|
// Render each rectangle based on the computed dimensions
|
||||||
renderRect(ctx, rect, totalTicks, rangeMin, rangeMax, search, levelIndex, topLevelIndex, foundLabels);
|
renderRect(ctx, rect, totalTicks, rangeMin, rangeMax, search, levelIndex, topLevelIndex, foundLabels);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [
|
}, [data, levels, wrapperWidth, totalTicks, rangeMin, rangeMax, search, topLevelIndex, foundLabels]);
|
||||||
levels,
|
|
||||||
wrapperWidth,
|
|
||||||
valueField,
|
|
||||||
totalTicks,
|
|
||||||
rangeMin,
|
|
||||||
rangeMax,
|
|
||||||
search,
|
|
||||||
topLevelIndex,
|
|
||||||
foundLabels,
|
|
||||||
getLabelValue,
|
|
||||||
]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (graphRef.current) {
|
if (graphRef.current) {
|
||||||
graphRef.current.onclick = (e) => {
|
graphRef.current.onclick = (e) => {
|
||||||
setTooltipData(undefined);
|
setTooltipItem(undefined);
|
||||||
const pixelsPerTick = graphRef.current!.clientWidth / totalTicks / (rangeMax - rangeMin);
|
const pixelsPerTick = graphRef.current!.clientWidth / totalTicks / (rangeMax - rangeMin);
|
||||||
const { levelIndex, barIndex } = convertPixelCoordinatesToBarCoordinates(
|
const { levelIndex, barIndex } = convertPixelCoordinatesToBarCoordinates(
|
||||||
|
data,
|
||||||
e,
|
e,
|
||||||
pixelsPerTick,
|
pixelsPerTick,
|
||||||
levels,
|
levels,
|
||||||
@ -191,9 +150,10 @@ const FlameGraph = ({
|
|||||||
|
|
||||||
graphRef.current!.onmousemove = (e) => {
|
graphRef.current!.onmousemove = (e) => {
|
||||||
if (tooltipRef.current && contextMenuData === undefined) {
|
if (tooltipRef.current && contextMenuData === undefined) {
|
||||||
setTooltipData(undefined);
|
setTooltipItem(undefined);
|
||||||
const pixelsPerTick = graphRef.current!.clientWidth / totalTicks / (rangeMax - rangeMin);
|
const pixelsPerTick = graphRef.current!.clientWidth / totalTicks / (rangeMax - rangeMin);
|
||||||
const { levelIndex, barIndex } = convertPixelCoordinatesToBarCoordinates(
|
const { levelIndex, barIndex } = convertPixelCoordinatesToBarCoordinates(
|
||||||
|
data,
|
||||||
e,
|
e,
|
||||||
pixelsPerTick,
|
pixelsPerTick,
|
||||||
levels,
|
levels,
|
||||||
@ -204,19 +164,17 @@ const FlameGraph = ({
|
|||||||
if (barIndex !== -1 && !isNaN(levelIndex) && !isNaN(barIndex)) {
|
if (barIndex !== -1 && !isNaN(levelIndex) && !isNaN(barIndex)) {
|
||||||
tooltipRef.current.style.left = e.clientX + 10 + 'px';
|
tooltipRef.current.style.left = e.clientX + 10 + 'px';
|
||||||
tooltipRef.current.style.top = e.clientY + 'px';
|
tooltipRef.current.style.top = e.clientY + 'px';
|
||||||
|
setTooltipItem(levels[levelIndex][barIndex]);
|
||||||
const bar = levels[levelIndex][barIndex];
|
|
||||||
const tooltipData = getTooltipData(valueField, bar.label, bar.value, bar.self, totalTicks);
|
|
||||||
setTooltipData(tooltipData);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
graphRef.current!.onmouseleave = () => {
|
graphRef.current!.onmouseleave = () => {
|
||||||
setTooltipData(undefined);
|
setTooltipItem(undefined);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
|
data,
|
||||||
levels,
|
levels,
|
||||||
rangeMin,
|
rangeMin,
|
||||||
rangeMax,
|
rangeMax,
|
||||||
@ -227,13 +185,12 @@ const FlameGraph = ({
|
|||||||
setRangeMin,
|
setRangeMin,
|
||||||
setRangeMax,
|
setRangeMax,
|
||||||
selectedView,
|
selectedView,
|
||||||
valueField,
|
|
||||||
setSelectedBarIndex,
|
setSelectedBarIndex,
|
||||||
setContextMenuData,
|
setContextMenuData,
|
||||||
contextMenuData,
|
contextMenuData,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// hide context menu if outside of the flame graph canvas is clicked
|
// hide context menu if outside the flame graph canvas is clicked
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleOnClick = (e: MouseEvent) => {
|
const handleOnClick = (e: MouseEvent) => {
|
||||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||||
@ -248,18 +205,19 @@ const FlameGraph = ({
|
|||||||
return (
|
return (
|
||||||
<div className={styles.graph} ref={sizeRef}>
|
<div className={styles.graph} ref={sizeRef}>
|
||||||
<FlameGraphMetadata
|
<FlameGraphMetadata
|
||||||
|
data={data}
|
||||||
levels={levels}
|
levels={levels}
|
||||||
topLevelIndex={topLevelIndex}
|
topLevelIndex={topLevelIndex}
|
||||||
selectedBarIndex={selectedBarIndex}
|
selectedBarIndex={selectedBarIndex}
|
||||||
valueField={valueField}
|
|
||||||
totalTicks={totalTicks}
|
totalTicks={totalTicks}
|
||||||
/>
|
/>
|
||||||
<div className={styles.canvasContainer} id="flameGraphCanvasContainer">
|
<div className={styles.canvasContainer} id="flameGraphCanvasContainer">
|
||||||
<canvas ref={graphRef} data-testid="flameGraph" />
|
<canvas ref={graphRef} data-testid="flameGraph" />
|
||||||
</div>
|
</div>
|
||||||
<FlameGraphTooltip tooltipRef={tooltipRef} tooltipData={tooltipData!} getLabelValue={getLabelValue} />
|
<FlameGraphTooltip tooltipRef={tooltipRef} item={tooltipItem} data={data} totalTicks={totalTicks} />
|
||||||
{contextMenuData && (
|
{contextMenuData && (
|
||||||
<FlameGraphContextMenu
|
<FlameGraphContextMenu
|
||||||
|
data={data}
|
||||||
contextMenuData={contextMenuData!}
|
contextMenuData={contextMenuData!}
|
||||||
levels={levels}
|
levels={levels}
|
||||||
totalTicks={totalTicks}
|
totalTicks={totalTicks}
|
||||||
@ -269,7 +227,6 @@ const FlameGraph = ({
|
|||||||
setSelectedBarIndex={setSelectedBarIndex}
|
setSelectedBarIndex={setSelectedBarIndex}
|
||||||
setRangeMin={setRangeMin}
|
setRangeMin={setRangeMin}
|
||||||
setRangeMax={setRangeMax}
|
setRangeMax={setRangeMax}
|
||||||
getLabelValue={getLabelValue}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -293,14 +250,15 @@ const getStyles = (selectedView: SelectedView, app: CoreApp, flameGraphHeight: n
|
|||||||
// Convert pixel coordinates to bar coordinates in the levels array so that we can add mouse events like clicks to
|
// Convert pixel coordinates to bar coordinates in the levels array so that we can add mouse events like clicks to
|
||||||
// the canvas.
|
// the canvas.
|
||||||
const convertPixelCoordinatesToBarCoordinates = (
|
const convertPixelCoordinatesToBarCoordinates = (
|
||||||
|
data: FlameGraphDataContainer,
|
||||||
e: MouseEvent,
|
e: MouseEvent,
|
||||||
pixelsPerTick: number,
|
pixelsPerTick: number,
|
||||||
levels: ItemWithStart[][],
|
levels: LevelItem[][],
|
||||||
totalTicks: number,
|
totalTicks: number,
|
||||||
rangeMin: number
|
rangeMin: number
|
||||||
) => {
|
) => {
|
||||||
const levelIndex = Math.floor(e.offsetY / (PIXELS_PER_LEVEL / window.devicePixelRatio));
|
const levelIndex = Math.floor(e.offsetY / (PIXELS_PER_LEVEL / window.devicePixelRatio));
|
||||||
const barIndex = getBarIndex(e.offsetX, levels[levelIndex], pixelsPerTick, totalTicks, rangeMin);
|
const barIndex = getBarIndex(e.offsetX, data, levels[levelIndex], pixelsPerTick, totalTicks, rangeMin);
|
||||||
return { levelIndex, barIndex };
|
return { levelIndex, barIndex };
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -310,7 +268,8 @@ const convertPixelCoordinatesToBarCoordinates = (
|
|||||||
*/
|
*/
|
||||||
const getBarIndex = (
|
const getBarIndex = (
|
||||||
x: number,
|
x: number,
|
||||||
level: ItemWithStart[],
|
data: FlameGraphDataContainer,
|
||||||
|
level: LevelItem[],
|
||||||
pixelsPerTick: number,
|
pixelsPerTick: number,
|
||||||
totalTicks: number,
|
totalTicks: number,
|
||||||
rangeMin: number
|
rangeMin: number
|
||||||
@ -323,7 +282,7 @@ const getBarIndex = (
|
|||||||
const midIndex = (start + end) >> 1;
|
const midIndex = (start + end) >> 1;
|
||||||
const startOfBar = getBarX(level[midIndex].start, totalTicks, rangeMin, pixelsPerTick);
|
const startOfBar = getBarX(level[midIndex].start, totalTicks, rangeMin, pixelsPerTick);
|
||||||
const startOfNextBar = getBarX(
|
const startOfNextBar = getBarX(
|
||||||
level[midIndex].start + level[midIndex].value,
|
level[midIndex].start + data.getValue(level[midIndex].itemIndex),
|
||||||
totalTicks,
|
totalTicks,
|
||||||
rangeMin,
|
rangeMin,
|
||||||
pixelsPerTick
|
pixelsPerTick
|
||||||
|
@ -4,11 +4,12 @@ import { MenuItem, ContextMenu } from '@grafana/ui';
|
|||||||
|
|
||||||
import { ContextMenuData } from '../types';
|
import { ContextMenuData } from '../types';
|
||||||
|
|
||||||
import { ItemWithStart } from './dataTransform';
|
import { FlameGraphDataContainer, LevelItem } from './dataTransform';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
contextMenuData: ContextMenuData;
|
contextMenuData: ContextMenuData;
|
||||||
levels: ItemWithStart[][];
|
data: FlameGraphDataContainer;
|
||||||
|
levels: LevelItem[][];
|
||||||
totalTicks: number;
|
totalTicks: number;
|
||||||
graphRef: React.RefObject<HTMLCanvasElement>;
|
graphRef: React.RefObject<HTMLCanvasElement>;
|
||||||
setContextMenuData: (event: ContextMenuData | undefined) => void;
|
setContextMenuData: (event: ContextMenuData | undefined) => void;
|
||||||
@ -16,7 +17,6 @@ type Props = {
|
|||||||
setSelectedBarIndex: (bar: number) => void;
|
setSelectedBarIndex: (bar: number) => void;
|
||||||
setRangeMin: (range: number) => void;
|
setRangeMin: (range: number) => void;
|
||||||
setRangeMax: (range: number) => void;
|
setRangeMax: (range: number) => void;
|
||||||
getLabelValue: (label: string | number) => string;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const FlameGraphContextMenu = ({
|
const FlameGraphContextMenu = ({
|
||||||
@ -29,8 +29,10 @@ const FlameGraphContextMenu = ({
|
|||||||
setSelectedBarIndex,
|
setSelectedBarIndex,
|
||||||
setRangeMin,
|
setRangeMin,
|
||||||
setRangeMax,
|
setRangeMax,
|
||||||
getLabelValue,
|
data,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
|
const clickedItem = levels[contextMenuData.levelIndex][contextMenuData.barIndex];
|
||||||
|
|
||||||
const renderMenuItems = () => {
|
const renderMenuItems = () => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -41,12 +43,8 @@ const FlameGraphContextMenu = ({
|
|||||||
if (graphRef.current && contextMenuData) {
|
if (graphRef.current && contextMenuData) {
|
||||||
setTopLevelIndex(contextMenuData.levelIndex);
|
setTopLevelIndex(contextMenuData.levelIndex);
|
||||||
setSelectedBarIndex(contextMenuData.barIndex);
|
setSelectedBarIndex(contextMenuData.barIndex);
|
||||||
setRangeMin(levels[contextMenuData.levelIndex][contextMenuData.barIndex].start / totalTicks);
|
setRangeMin(clickedItem.start / totalTicks);
|
||||||
setRangeMax(
|
setRangeMax((clickedItem.start + data.getValue(clickedItem.itemIndex)) / totalTicks);
|
||||||
(levels[contextMenuData.levelIndex][contextMenuData.barIndex].start +
|
|
||||||
levels[contextMenuData.levelIndex][contextMenuData.barIndex].value) /
|
|
||||||
totalTicks
|
|
||||||
);
|
|
||||||
setContextMenuData(undefined);
|
setContextMenuData(undefined);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
@ -56,8 +54,7 @@ const FlameGraphContextMenu = ({
|
|||||||
icon={'copy'}
|
icon={'copy'}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (graphRef.current && contextMenuData) {
|
if (graphRef.current && contextMenuData) {
|
||||||
const bar = levels[contextMenuData.levelIndex][contextMenuData.barIndex];
|
navigator.clipboard.writeText(data.getLabel(clickedItem.itemIndex)).then(() => {
|
||||||
navigator.clipboard.writeText(getLabelValue(bar.label)).then(() => {
|
|
||||||
setContextMenuData(undefined);
|
setContextMenuData(undefined);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,28 @@
|
|||||||
import { ArrayVector, Field, FieldType } from '@grafana/data';
|
import { MutableDataFrame } from '@grafana/data';
|
||||||
|
|
||||||
import { getMetadata } from './FlameGraphMetadata';
|
import { getMetadata } from './FlameGraphMetadata';
|
||||||
|
import { FlameGraphDataContainer } from './dataTransform';
|
||||||
|
|
||||||
|
function makeDataFrame(fields: Record<string, Array<number | string>>, unit?: string) {
|
||||||
|
return new MutableDataFrame({
|
||||||
|
fields: Object.keys(fields).map((key) => ({
|
||||||
|
name: key,
|
||||||
|
values: fields[key],
|
||||||
|
config: unit
|
||||||
|
? {
|
||||||
|
unit,
|
||||||
|
}
|
||||||
|
: {},
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
describe('should get metadata correctly', () => {
|
describe('should get metadata correctly', () => {
|
||||||
it('for bytes', () => {
|
it('for bytes', () => {
|
||||||
const metadata = getMetadata(makeField('bytes'), 1_624_078_250, 8_624_078_250);
|
const container = new FlameGraphDataContainer(
|
||||||
|
makeDataFrame({ value: [1_624_078_250], level: [1], label: ['1'], self: [0] }, 'bytes')
|
||||||
|
);
|
||||||
|
const metadata = getMetadata(container, { itemIndex: 0, start: 0 }, 8_624_078_250);
|
||||||
expect(metadata).toEqual({
|
expect(metadata).toEqual({
|
||||||
percentValue: 18.83,
|
percentValue: 18.83,
|
||||||
unitTitle: 'RAM',
|
unitTitle: 'RAM',
|
||||||
@ -14,7 +32,10 @@ describe('should get metadata correctly', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('with default unit', () => {
|
it('with default unit', () => {
|
||||||
const metadata = getMetadata(makeField('none'), 1_624_078_250, 8_624_078_250);
|
const container = new FlameGraphDataContainer(
|
||||||
|
makeDataFrame({ value: [1_624_078_250], level: [1], label: ['1'], self: [0] }, 'none')
|
||||||
|
);
|
||||||
|
const metadata = getMetadata(container, { itemIndex: 0, start: 0 }, 8_624_078_250);
|
||||||
expect(metadata).toEqual({
|
expect(metadata).toEqual({
|
||||||
percentValue: 18.83,
|
percentValue: 18.83,
|
||||||
unitTitle: 'Count',
|
unitTitle: 'Count',
|
||||||
@ -24,16 +45,10 @@ describe('should get metadata correctly', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('without unit', () => {
|
it('without unit', () => {
|
||||||
const metadata = getMetadata(
|
const container = new FlameGraphDataContainer(
|
||||||
{
|
makeDataFrame({ value: [1_624_078_250], level: [1], label: ['1'], self: [0] })
|
||||||
name: 'test',
|
|
||||||
type: FieldType.number,
|
|
||||||
values: new ArrayVector(),
|
|
||||||
config: {},
|
|
||||||
},
|
|
||||||
1_624_078_250,
|
|
||||||
8_624_078_250
|
|
||||||
);
|
);
|
||||||
|
const metadata = getMetadata(container, { itemIndex: 0, start: 0 }, 8_624_078_250);
|
||||||
expect(metadata).toEqual({
|
expect(metadata).toEqual({
|
||||||
percentValue: 18.83,
|
percentValue: 18.83,
|
||||||
unitTitle: 'Count',
|
unitTitle: 'Count',
|
||||||
@ -43,7 +58,10 @@ describe('should get metadata correctly', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('for objects', () => {
|
it('for objects', () => {
|
||||||
const metadata = getMetadata(makeField('short'), 1_624_078_250, 8_624_078_250);
|
const container = new FlameGraphDataContainer(
|
||||||
|
makeDataFrame({ value: [1_624_078_250], level: [1], label: ['1'], self: [0] }, 'short')
|
||||||
|
);
|
||||||
|
const metadata = getMetadata(container, { itemIndex: 0, start: 0 }, 8_624_078_250);
|
||||||
expect(metadata).toEqual({
|
expect(metadata).toEqual({
|
||||||
percentValue: 18.83,
|
percentValue: 18.83,
|
||||||
unitTitle: 'Count',
|
unitTitle: 'Count',
|
||||||
@ -53,7 +71,10 @@ describe('should get metadata correctly', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('for nanoseconds', () => {
|
it('for nanoseconds', () => {
|
||||||
const metadata = getMetadata(makeField('ns'), 1_624_078_250, 8_624_078_250);
|
const container = new FlameGraphDataContainer(
|
||||||
|
makeDataFrame({ value: [1_624_078_250], level: [1], label: ['1'], self: [0] }, 'ns')
|
||||||
|
);
|
||||||
|
const metadata = getMetadata(container, { itemIndex: 0, start: 0 }, 8_624_078_250);
|
||||||
expect(metadata).toEqual({
|
expect(metadata).toEqual({
|
||||||
percentValue: 18.83,
|
percentValue: 18.83,
|
||||||
unitTitle: 'Time',
|
unitTitle: 'Time',
|
||||||
@ -62,14 +83,3 @@ describe('should get metadata correctly', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
function makeField(unit: string): Field {
|
|
||||||
return {
|
|
||||||
name: 'test',
|
|
||||||
type: FieldType.number,
|
|
||||||
config: {
|
|
||||||
unit,
|
|
||||||
},
|
|
||||||
values: new ArrayVector(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
@ -1,53 +1,42 @@
|
|||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { createTheme, Field, getDisplayProcessor, Vector } from '@grafana/data';
|
|
||||||
import { useStyles2 } from '@grafana/ui';
|
import { useStyles2 } from '@grafana/ui';
|
||||||
|
|
||||||
import { Metadata, SampleUnit } from '../types';
|
import { Metadata } from '../types';
|
||||||
|
|
||||||
import { ItemWithStart } from './dataTransform';
|
import { FlameGraphDataContainer, LevelItem } from './dataTransform';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
levels: ItemWithStart[][];
|
data: FlameGraphDataContainer;
|
||||||
|
levels: LevelItem[][];
|
||||||
topLevelIndex: number;
|
topLevelIndex: number;
|
||||||
selectedBarIndex: number;
|
selectedBarIndex: number;
|
||||||
valueField: Field<number, Vector<number>>;
|
|
||||||
totalTicks: number;
|
totalTicks: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
const FlameGraphMetadata = React.memo(({ levels, topLevelIndex, selectedBarIndex, valueField, totalTicks }: Props) => {
|
const FlameGraphMetadata = React.memo(({ data, levels, topLevelIndex, selectedBarIndex, totalTicks }: Props) => {
|
||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
if (levels[topLevelIndex] && levels[topLevelIndex][selectedBarIndex]) {
|
if (levels[topLevelIndex] && levels[topLevelIndex][selectedBarIndex]) {
|
||||||
const bar = levels[topLevelIndex][selectedBarIndex];
|
const bar = levels[topLevelIndex][selectedBarIndex];
|
||||||
const metadata = getMetadata(valueField, bar.value, totalTicks);
|
const metadata = getMetadata(data, bar, totalTicks);
|
||||||
const metadataText = `${metadata?.unitValue} (${metadata?.percentValue}%) of ${metadata?.samples} total samples (${metadata?.unitTitle})`;
|
const metadataText = `${metadata?.unitValue} (${metadata?.percentValue}%) of ${metadata?.samples} total samples (${metadata?.unitTitle})`;
|
||||||
return <>{<div className={styles.metadata}>{metadataText}</div>}</>;
|
return <>{<div className={styles.metadata}>{metadataText}</div>}</>;
|
||||||
}
|
}
|
||||||
return <></>;
|
return <></>;
|
||||||
});
|
});
|
||||||
|
|
||||||
export const getMetadata = (field: Field, value: number, totalTicks: number): Metadata => {
|
export const getMetadata = (data: FlameGraphDataContainer, bar: LevelItem, totalTicks: number): Metadata => {
|
||||||
let unitTitle;
|
const displayValue = data.getValueDisplay(bar.itemIndex);
|
||||||
const processor = getDisplayProcessor({ field, theme: createTheme() /* theme does not matter for us here */ });
|
const percentValue = Math.round(10000 * (displayValue.numeric / totalTicks)) / 100;
|
||||||
const displayValue = processor(value);
|
|
||||||
const percentValue = Math.round(10000 * (value / totalTicks)) / 100;
|
|
||||||
let unitValue = displayValue.text + displayValue.suffix;
|
let unitValue = displayValue.text + displayValue.suffix;
|
||||||
|
|
||||||
switch (field.config.unit) {
|
const unitTitle = data.getUnitTitle();
|
||||||
case SampleUnit.Bytes:
|
if (unitTitle === 'Count') {
|
||||||
unitTitle = 'RAM';
|
if (!displayValue.suffix) {
|
||||||
break;
|
// Makes sure we don't show 123undefined or something like that if suffix isn't defined
|
||||||
case SampleUnit.Nanoseconds:
|
unitValue = displayValue.text;
|
||||||
unitTitle = 'Time';
|
}
|
||||||
break;
|
|
||||||
default:
|
|
||||||
unitTitle = 'Count';
|
|
||||||
if (!displayValue.suffix) {
|
|
||||||
// Makes sure we don't show 123undefined or something like that if suffix isn't defined
|
|
||||||
unitValue = displayValue.text;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -1,10 +1,23 @@
|
|||||||
import { ArrayVector, Field, FieldType } from '@grafana/data';
|
import { ArrayVector, Field, FieldType, MutableDataFrame } from '@grafana/data';
|
||||||
|
|
||||||
import { getTooltipData } from './FlameGraphTooltip';
|
import { getTooltipData } from './FlameGraphTooltip';
|
||||||
|
import { FlameGraphDataContainer } from './dataTransform';
|
||||||
|
|
||||||
describe('should get tooltip data correctly', () => {
|
function setupData(unit?: string) {
|
||||||
|
const flameGraphData = new MutableDataFrame({
|
||||||
|
fields: [
|
||||||
|
{ name: 'level', values: [0] },
|
||||||
|
unit ? makeField('value', unit, [8_624_078_250]) : { name: 'value', values: [8_624_078_250] },
|
||||||
|
{ name: 'self', values: [978_250] },
|
||||||
|
{ name: 'label', values: ['total'] },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
return new FlameGraphDataContainer(flameGraphData);
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('FlameGraphTooltip', () => {
|
||||||
it('for bytes', () => {
|
it('for bytes', () => {
|
||||||
const tooltipData = getTooltipData(makeField('bytes'), 'total', 8_624_078_250, 978_250, 8_624_078_250);
|
const tooltipData = getTooltipData(setupData('bytes'), { start: 0, itemIndex: 0 }, 8_624_078_250);
|
||||||
expect(tooltipData).toEqual({
|
expect(tooltipData).toEqual({
|
||||||
name: 'total',
|
name: 'total',
|
||||||
percentSelf: 0.01,
|
percentSelf: 0.01,
|
||||||
@ -17,7 +30,7 @@ describe('should get tooltip data correctly', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('with default unit', () => {
|
it('with default unit', () => {
|
||||||
const tooltipData = getTooltipData(makeField('none'), 'total', 8_624_078_250, 978_250, 8_624_078_250);
|
const tooltipData = getTooltipData(setupData('none'), { start: 0, itemIndex: 0 }, 8_624_078_250);
|
||||||
expect(tooltipData).toEqual({
|
expect(tooltipData).toEqual({
|
||||||
name: 'total',
|
name: 'total',
|
||||||
percentSelf: 0.01,
|
percentSelf: 0.01,
|
||||||
@ -30,18 +43,7 @@ describe('should get tooltip data correctly', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('without unit', () => {
|
it('without unit', () => {
|
||||||
const tooltipData = getTooltipData(
|
const tooltipData = getTooltipData(setupData('none'), { start: 0, itemIndex: 0 }, 8_624_078_250);
|
||||||
{
|
|
||||||
name: 'test',
|
|
||||||
type: FieldType.number,
|
|
||||||
values: new ArrayVector(),
|
|
||||||
config: {},
|
|
||||||
},
|
|
||||||
'total',
|
|
||||||
8_624_078_250,
|
|
||||||
978_250,
|
|
||||||
8_624_078_250
|
|
||||||
);
|
|
||||||
expect(tooltipData).toEqual({
|
expect(tooltipData).toEqual({
|
||||||
name: 'total',
|
name: 'total',
|
||||||
percentSelf: 0.01,
|
percentSelf: 0.01,
|
||||||
@ -54,7 +56,7 @@ describe('should get tooltip data correctly', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('for objects', () => {
|
it('for objects', () => {
|
||||||
const tooltipData = getTooltipData(makeField('short'), 'total', 8_624_078_250, 978_250, 8_624_078_250);
|
const tooltipData = getTooltipData(setupData('short'), { start: 0, itemIndex: 0 }, 8_624_078_250);
|
||||||
expect(tooltipData).toEqual({
|
expect(tooltipData).toEqual({
|
||||||
name: 'total',
|
name: 'total',
|
||||||
percentSelf: 0.01,
|
percentSelf: 0.01,
|
||||||
@ -67,7 +69,7 @@ describe('should get tooltip data correctly', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('for nanoseconds', () => {
|
it('for nanoseconds', () => {
|
||||||
const tooltipData = getTooltipData(makeField('ns'), 'total', 8_624_078_250, 978_250, 8_624_078_250);
|
const tooltipData = getTooltipData(setupData('ns'), { start: 0, itemIndex: 0 }, 8_624_078_250);
|
||||||
expect(tooltipData).toEqual({
|
expect(tooltipData).toEqual({
|
||||||
name: 'total',
|
name: 'total',
|
||||||
percentSelf: 0.01,
|
percentSelf: 0.01,
|
||||||
@ -80,13 +82,13 @@ describe('should get tooltip data correctly', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
function makeField(unit: string): Field {
|
function makeField(name: string, unit: string, values: number[]): Field {
|
||||||
return {
|
return {
|
||||||
name: 'test',
|
name,
|
||||||
type: FieldType.number,
|
type: FieldType.number,
|
||||||
config: {
|
config: {
|
||||||
unit,
|
unit,
|
||||||
},
|
},
|
||||||
values: new ArrayVector(),
|
values: new ArrayVector(values),
|
||||||
};
|
};
|
||||||
}
|
}
|
@ -1,94 +1,95 @@
|
|||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
import React, { LegacyRef } from 'react';
|
import React, { LegacyRef } from 'react';
|
||||||
|
|
||||||
import { createTheme, Field, getDisplayProcessor } from '@grafana/data';
|
|
||||||
import { useStyles2, Tooltip } from '@grafana/ui';
|
import { useStyles2, Tooltip } from '@grafana/ui';
|
||||||
|
|
||||||
import { TooltipData, SampleUnit } from '../types';
|
import { FlameGraphDataContainer, LevelItem } from './dataTransform';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
tooltipRef: LegacyRef<HTMLDivElement>;
|
data: FlameGraphDataContainer;
|
||||||
tooltipData: TooltipData;
|
totalTicks: number;
|
||||||
getLabelValue: (label: string | number) => string;
|
item?: LevelItem;
|
||||||
|
tooltipRef?: LegacyRef<HTMLDivElement>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const FlameGraphTooltip = ({ tooltipRef, tooltipData, getLabelValue }: Props) => {
|
const FlameGraphTooltip = ({ data, tooltipRef, item, totalTicks }: Props) => {
|
||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
|
|
||||||
|
let content = null;
|
||||||
|
if (item) {
|
||||||
|
const tooltipData = getTooltipData(data, item, totalTicks);
|
||||||
|
content = (
|
||||||
|
<Tooltip
|
||||||
|
content={
|
||||||
|
<div>
|
||||||
|
<p>{data.getLabel(item.itemIndex)}</p>
|
||||||
|
<p className={styles.lastParagraph}>
|
||||||
|
{tooltipData.unitTitle}
|
||||||
|
<br />
|
||||||
|
Total: <b>{tooltipData.unitValue}</b> ({tooltipData.percentValue}%)
|
||||||
|
<br />
|
||||||
|
Self: <b>{tooltipData.unitSelf}</b> ({tooltipData.percentSelf}%)
|
||||||
|
<br />
|
||||||
|
Samples: <b>{tooltipData.samples}</b>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
placement={'right'}
|
||||||
|
show={true}
|
||||||
|
>
|
||||||
|
<span></span>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Even if we don't show tooltip we need this div so the ref is consistently attached. Would need some refactor in
|
||||||
|
// FlameGraph.tsx to make it work without it.
|
||||||
return (
|
return (
|
||||||
<div ref={tooltipRef} className={styles.tooltip}>
|
<div ref={tooltipRef} className={styles.tooltip}>
|
||||||
{tooltipData && (
|
{content}
|
||||||
<Tooltip
|
|
||||||
content={
|
|
||||||
<div>
|
|
||||||
<p>{getLabelValue(tooltipData.name)}</p>
|
|
||||||
<p className={styles.lastParagraph}>
|
|
||||||
{tooltipData.unitTitle}
|
|
||||||
<br />
|
|
||||||
Total: <b>{tooltipData.unitValue}</b> ({tooltipData.percentValue}%)
|
|
||||||
<br />
|
|
||||||
Self: <b>{tooltipData.unitSelf}</b> ({tooltipData.percentSelf}%)
|
|
||||||
<br />
|
|
||||||
Samples: <b>{tooltipData.samples}</b>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
placement={'right'}
|
|
||||||
show={true}
|
|
||||||
>
|
|
||||||
<span></span>
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getTooltipData = (
|
type TooltipData = {
|
||||||
field: Field,
|
name: string;
|
||||||
label: string,
|
percentValue: number;
|
||||||
value: number,
|
percentSelf: number;
|
||||||
self: number,
|
unitTitle: string;
|
||||||
totalTicks: number
|
unitValue: string;
|
||||||
): TooltipData => {
|
unitSelf: string;
|
||||||
let unitTitle;
|
samples: string;
|
||||||
|
};
|
||||||
|
|
||||||
const processor = getDisplayProcessor({ field, theme: createTheme() /* theme does not matter for us here */ });
|
export const getTooltipData = (data: FlameGraphDataContainer, item: LevelItem, totalTicks: number): TooltipData => {
|
||||||
const displayValue = processor(value);
|
const displayValue = data.getValueDisplay(item.itemIndex);
|
||||||
const displaySelf = processor(self);
|
const displaySelf = data.getSelfDisplay(item.itemIndex);
|
||||||
|
|
||||||
const percentValue = Math.round(10000 * (value / totalTicks)) / 100;
|
const percentValue = Math.round(10000 * (displayValue.numeric / totalTicks)) / 100;
|
||||||
const percentSelf = Math.round(10000 * (self / totalTicks)) / 100;
|
const percentSelf = Math.round(10000 * (displaySelf.numeric / totalTicks)) / 100;
|
||||||
let unitValue = displayValue.text + displayValue.suffix;
|
let unitValue = displayValue.text + displayValue.suffix;
|
||||||
let unitSelf = displaySelf.text + displaySelf.suffix;
|
let unitSelf = displaySelf.text + displaySelf.suffix;
|
||||||
|
|
||||||
switch (field.config.unit) {
|
const unitTitle = data.getUnitTitle();
|
||||||
case SampleUnit.Bytes:
|
if (unitTitle === 'Count') {
|
||||||
unitTitle = 'RAM';
|
if (!displayValue.suffix) {
|
||||||
break;
|
// Makes sure we don't show 123undefined or something like that if suffix isn't defined
|
||||||
case SampleUnit.Nanoseconds:
|
unitValue = displayValue.text;
|
||||||
unitTitle = 'Time';
|
}
|
||||||
break;
|
if (!displaySelf.suffix) {
|
||||||
default:
|
// Makes sure we don't show 123undefined or something like that if suffix isn't defined
|
||||||
unitTitle = 'Count';
|
unitSelf = displaySelf.text;
|
||||||
if (!displayValue.suffix) {
|
}
|
||||||
// Makes sure we don't show 123undefined or something like that if suffix isn't defined
|
|
||||||
unitValue = displayValue.text;
|
|
||||||
}
|
|
||||||
if (!displaySelf.suffix) {
|
|
||||||
// Makes sure we don't show 123undefined or something like that if suffix isn't defined
|
|
||||||
unitSelf = displaySelf.text;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name: label,
|
name: data.getLabel(item.itemIndex),
|
||||||
percentValue,
|
percentValue,
|
||||||
percentSelf,
|
percentSelf,
|
||||||
unitTitle,
|
unitTitle,
|
||||||
unitValue,
|
unitValue,
|
||||||
unitSelf,
|
unitSelf,
|
||||||
samples: value.toLocaleString(),
|
samples: displayValue.numeric.toLocaleString(),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { DataFrameView, MutableDataFrame } from '@grafana/data';
|
import { MutableDataFrame } from '@grafana/data';
|
||||||
|
|
||||||
import { Item, nestedSetToLevels } from './dataTransform';
|
import { FlameGraphDataContainer, nestedSetToLevels } from './dataTransform';
|
||||||
|
|
||||||
describe('nestedSetToLevels', () => {
|
describe('nestedSetToLevels', () => {
|
||||||
it('converts nested set data frame to levels', () => {
|
it('converts nested set data frame to levels', () => {
|
||||||
@ -9,25 +9,26 @@ describe('nestedSetToLevels', () => {
|
|||||||
{ name: 'level', values: [0, 1, 2, 3, 2, 1, 2, 3, 4] },
|
{ name: 'level', values: [0, 1, 2, 3, 2, 1, 2, 3, 4] },
|
||||||
{ name: 'value', values: [10, 5, 3, 1, 1, 4, 3, 2, 1] },
|
{ name: 'value', values: [10, 5, 3, 1, 1, 4, 3, 2, 1] },
|
||||||
{ name: 'label', values: ['1', '2', '3', '4', '5', '6', '7', '8', '9'] },
|
{ name: 'label', values: ['1', '2', '3', '4', '5', '6', '7', '8', '9'] },
|
||||||
|
{ name: 'self', values: [0, 0, 0, 0, 0, 0, 0, 0, 0] },
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
const levels = nestedSetToLevels(new DataFrameView<Item>(frame));
|
const levels = nestedSetToLevels(new FlameGraphDataContainer(frame));
|
||||||
expect(levels).toEqual([
|
expect(levels).toEqual([
|
||||||
[{ level: 0, value: 10, start: 0, label: '1' }],
|
[{ start: 0, itemIndex: 0 }],
|
||||||
[
|
[
|
||||||
{ level: 1, value: 5, start: 0, label: '2' },
|
{ start: 0, itemIndex: 1 },
|
||||||
{ level: 1, value: 4, start: 5, label: '6' },
|
{ start: 5, itemIndex: 5 },
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
{ level: 2, value: 3, start: 0, label: '3' },
|
{ start: 0, itemIndex: 2 },
|
||||||
{ level: 2, value: 1, start: 3, label: '5' },
|
{ start: 3, itemIndex: 4 },
|
||||||
{ level: 2, value: 3, start: 5, label: '7' },
|
{ start: 5, itemIndex: 6 },
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
{ level: 3, value: 1, start: 0, label: '4' },
|
{ start: 0, itemIndex: 3 },
|
||||||
{ level: 3, value: 2, start: 5, label: '8' },
|
{ start: 5, itemIndex: 7 },
|
||||||
],
|
],
|
||||||
[{ level: 4, value: 1, start: 5, label: '9' }],
|
[{ start: 5, itemIndex: 8 }],
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -37,15 +38,16 @@ describe('nestedSetToLevels', () => {
|
|||||||
{ name: 'level', values: [0, 1, 1, 1] },
|
{ name: 'level', values: [0, 1, 1, 1] },
|
||||||
{ name: 'value', values: [10, 5, 3, 1] },
|
{ name: 'value', values: [10, 5, 3, 1] },
|
||||||
{ name: 'label', values: ['1', '2', '3', '4'] },
|
{ name: 'label', values: ['1', '2', '3', '4'] },
|
||||||
|
{ name: 'self', values: [10, 5, 3, 1] },
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
const levels = nestedSetToLevels(new DataFrameView<Item>(frame));
|
const levels = nestedSetToLevels(new FlameGraphDataContainer(frame));
|
||||||
expect(levels).toEqual([
|
expect(levels).toEqual([
|
||||||
[{ level: 0, value: 10, start: 0, label: '1' }],
|
[{ start: 0, itemIndex: 0 }],
|
||||||
[
|
[
|
||||||
{ level: 1, value: 5, start: 0, label: '2' },
|
{ start: 0, itemIndex: 1 },
|
||||||
{ level: 1, value: 3, start: 5, label: '3' },
|
{ start: 5, itemIndex: 2 },
|
||||||
{ level: 1, value: 1, start: 8, label: '4' },
|
{ start: 8, itemIndex: 3 },
|
||||||
],
|
],
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
@ -1,35 +1,125 @@
|
|||||||
import { DataFrameView } from '@grafana/data';
|
import {
|
||||||
|
createTheme,
|
||||||
|
DataFrame,
|
||||||
|
DisplayProcessor,
|
||||||
|
Field,
|
||||||
|
getDisplayProcessor,
|
||||||
|
getEnumDisplayProcessor,
|
||||||
|
GrafanaTheme2,
|
||||||
|
} from '@grafana/data';
|
||||||
|
|
||||||
export type Item = { level: number; value: number; label: string; self: number };
|
import { SampleUnit } from '../types';
|
||||||
export type ItemWithStart = Item & { start: number };
|
|
||||||
|
export type LevelItem = { start: number; itemIndex: number };
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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.
|
||||||
* @param dataView
|
|
||||||
*/
|
*/
|
||||||
export function nestedSetToLevels(dataView: DataFrameView<Item>): ItemWithStart[][] {
|
export function nestedSetToLevels(container: FlameGraphDataContainer): LevelItem[][] {
|
||||||
const levels: ItemWithStart[][] = [];
|
const levels: LevelItem[][] = [];
|
||||||
let offset = 0;
|
let offset = 0;
|
||||||
|
|
||||||
for (let i = 0; i < dataView.length; i++) {
|
for (let i = 0; i < container.data.length; i++) {
|
||||||
// We have to clone the items as .get(i) returns a changing pointer not the data themselves.
|
const currentLevel = container.getLevel(i);
|
||||||
const item = { ...dataView.get(i) };
|
const prevLevel = i > 0 ? container.getLevel(i - 1) : undefined;
|
||||||
const prevItem = i > 0 ? { ...dataView.get(i - 1) } : undefined;
|
|
||||||
|
|
||||||
levels[item.level] = levels[item.level] || [];
|
levels[currentLevel] = levels[currentLevel] || [];
|
||||||
if (prevItem && prevItem.level >= item.level) {
|
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.
|
// 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.
|
// So we have to compute the correct offset based on the last sibling.
|
||||||
const lastItem = levels[item.level][levels[item.level].length - 1];
|
const lastItem = levels[currentLevel][levels[currentLevel].length - 1];
|
||||||
offset = lastItem.start + lastItem.value;
|
offset = lastItem.start + container.getValue(lastItem.itemIndex);
|
||||||
}
|
}
|
||||||
const newItem: ItemWithStart = {
|
const newItem: LevelItem = {
|
||||||
...item,
|
itemIndex: i,
|
||||||
start: offset,
|
start: offset,
|
||||||
};
|
};
|
||||||
|
|
||||||
levels[item.level].push(newItem);
|
levels[currentLevel].push(newItem);
|
||||||
}
|
}
|
||||||
return levels;
|
return levels;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class FlameGraphDataContainer {
|
||||||
|
data: DataFrame;
|
||||||
|
labelField: Field;
|
||||||
|
levelField: Field;
|
||||||
|
valueField: Field;
|
||||||
|
selfField: Field;
|
||||||
|
|
||||||
|
labelDisplayProcessor: DisplayProcessor;
|
||||||
|
valueDisplayProcessor: DisplayProcessor;
|
||||||
|
uniqueLabels: string[];
|
||||||
|
|
||||||
|
constructor(data: DataFrame, theme: GrafanaTheme2 = createTheme()) {
|
||||||
|
this.data = data;
|
||||||
|
this.labelField = data.fields.find((f) => f.name === 'label')!;
|
||||||
|
this.levelField = data.fields.find((f) => f.name === 'level')!;
|
||||||
|
this.valueField = data.fields.find((f) => f.name === 'value')!;
|
||||||
|
this.selfField = data.fields.find((f) => f.name === 'self')!;
|
||||||
|
|
||||||
|
if (!(this.labelField && this.levelField && this.valueField && this.selfField)) {
|
||||||
|
throw new Error('Malformed dataFrame: value, level and label and self fields are required.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const enumConfig = this.labelField?.config?.type?.enum;
|
||||||
|
// Label can actually be an enum field so depending on that we have to access it through display processor. This is
|
||||||
|
// both a backward compatibility but also to allow using a simple dataFrame without enum config. This would allow
|
||||||
|
// users to use this panel with correct query from data sources that do not return profiles natively.
|
||||||
|
if (enumConfig) {
|
||||||
|
this.labelDisplayProcessor = getEnumDisplayProcessor(theme, enumConfig);
|
||||||
|
this.uniqueLabels = enumConfig.text || [];
|
||||||
|
} else {
|
||||||
|
this.labelDisplayProcessor = (value) => ({
|
||||||
|
text: value + '',
|
||||||
|
numeric: 0,
|
||||||
|
});
|
||||||
|
this.uniqueLabels = [...new Set<string>(this.labelField.values.toArray())];
|
||||||
|
}
|
||||||
|
|
||||||
|
this.valueDisplayProcessor = getDisplayProcessor({
|
||||||
|
field: this.valueField,
|
||||||
|
theme,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getLabel(index: number) {
|
||||||
|
return this.labelDisplayProcessor(this.labelField.values.get(index)).text;
|
||||||
|
}
|
||||||
|
|
||||||
|
getLevel(index: number) {
|
||||||
|
return this.levelField.values.get(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
getValue(index: number) {
|
||||||
|
return this.valueField.values.get(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
getValueDisplay(index: number) {
|
||||||
|
return this.valueDisplayProcessor(this.valueField.values.get(index));
|
||||||
|
}
|
||||||
|
|
||||||
|
getSelf(index: number) {
|
||||||
|
return this.selfField.values.get(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
getSelfDisplay(index: number) {
|
||||||
|
return this.valueDisplayProcessor(this.selfField.values.get(index));
|
||||||
|
}
|
||||||
|
|
||||||
|
getUniqueLabels() {
|
||||||
|
return this.uniqueLabels;
|
||||||
|
}
|
||||||
|
|
||||||
|
getUnitTitle() {
|
||||||
|
switch (this.valueField.config.unit) {
|
||||||
|
case SampleUnit.Bytes:
|
||||||
|
return 'RAM';
|
||||||
|
case SampleUnit.Nanoseconds:
|
||||||
|
return 'Time';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'Count';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,20 +1,22 @@
|
|||||||
import { createTheme, getDisplayProcessor } from '@grafana/data';
|
import { MutableDataFrame } from '@grafana/data';
|
||||||
|
|
||||||
import { ItemWithStart } from './dataTransform';
|
import { FlameGraphDataContainer, LevelItem } from './dataTransform';
|
||||||
import { getRectDimensionsForLevel } from './rendering';
|
import { getRectDimensionsForLevel } from './rendering';
|
||||||
|
|
||||||
|
function makeDataFrame(fields: Record<string, Array<number | string>>) {
|
||||||
|
return new MutableDataFrame({
|
||||||
|
fields: Object.keys(fields).map((key) => ({
|
||||||
|
name: key,
|
||||||
|
values: fields[key],
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
describe('getRectDimensionsForLevel', () => {
|
describe('getRectDimensionsForLevel', () => {
|
||||||
it('should render a single item', () => {
|
it('should render a single item', () => {
|
||||||
const level: ItemWithStart[] = [{ level: 1, start: 0, value: 100, label: '1', self: 0 }];
|
const level: LevelItem[] = [{ start: 0, itemIndex: 0 }];
|
||||||
const result = getRectDimensionsForLevel(
|
const container = new FlameGraphDataContainer(makeDataFrame({ value: [100], level: [1], label: ['1'], self: [0] }));
|
||||||
level,
|
const result = getRectDimensionsForLevel(container, level, 1, 100, 0, 10);
|
||||||
1,
|
|
||||||
100,
|
|
||||||
0,
|
|
||||||
10,
|
|
||||||
getDisplayProcessor({ field: { config: {} }, theme: createTheme() }),
|
|
||||||
(val) => val.toString()
|
|
||||||
);
|
|
||||||
expect(result).toEqual([
|
expect(result).toEqual([
|
||||||
{
|
{
|
||||||
width: 999,
|
width: 999,
|
||||||
@ -30,20 +32,15 @@ describe('getRectDimensionsForLevel', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should render a multiple items', () => {
|
it('should render a multiple items', () => {
|
||||||
const level: ItemWithStart[] = [
|
const level: LevelItem[] = [
|
||||||
{ level: 2, start: 0, value: 100, label: '1', self: 0 },
|
{ start: 0, itemIndex: 0 },
|
||||||
{ level: 2, start: 100, value: 50, label: '2', self: 0 },
|
{ start: 100, itemIndex: 1 },
|
||||||
{ level: 2, start: 150, value: 50, label: '3', self: 0 },
|
{ start: 150, itemIndex: 2 },
|
||||||
];
|
];
|
||||||
const result = getRectDimensionsForLevel(
|
const container = new FlameGraphDataContainer(
|
||||||
level,
|
makeDataFrame({ value: [100, 50, 50], level: [2, 2, 2], label: ['1', '2', '3'], self: [0, 0, 0] })
|
||||||
2,
|
|
||||||
100,
|
|
||||||
0,
|
|
||||||
10,
|
|
||||||
getDisplayProcessor({ field: { config: {} }, theme: createTheme() }),
|
|
||||||
(val) => val.toString()
|
|
||||||
);
|
);
|
||||||
|
const result = getRectDimensionsForLevel(container, level, 2, 100, 0, 10);
|
||||||
expect(result).toEqual([
|
expect(result).toEqual([
|
||||||
{ width: 999, height: 22, x: 0, y: 44, collapsed: false, ticks: 100, label: '1', unitLabel: '100' },
|
{ width: 999, height: 22, x: 0, y: 44, collapsed: false, ticks: 100, label: '1', unitLabel: '100' },
|
||||||
{ width: 499, height: 22, x: 1000, y: 44, collapsed: false, ticks: 50, label: '2', unitLabel: '50' },
|
{ width: 499, height: 22, x: 1000, y: 44, collapsed: false, ticks: 50, label: '2', unitLabel: '50' },
|
||||||
@ -52,20 +49,15 @@ describe('getRectDimensionsForLevel', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should render a collapsed items', () => {
|
it('should render a collapsed items', () => {
|
||||||
const level: ItemWithStart[] = [
|
const level: LevelItem[] = [
|
||||||
{ level: 2, start: 0, value: 100, label: '1', self: 0 },
|
{ start: 0, itemIndex: 0 },
|
||||||
{ level: 2, start: 100, value: 2, label: '2', self: 0 },
|
{ start: 100, itemIndex: 1 },
|
||||||
{ level: 2, start: 102, value: 1, label: '3', self: 0 },
|
{ start: 102, itemIndex: 2 },
|
||||||
];
|
];
|
||||||
const result = getRectDimensionsForLevel(
|
const container = new FlameGraphDataContainer(
|
||||||
level,
|
makeDataFrame({ value: [100, 2, 1], level: [2, 2, 2], label: ['1', '2', '3'], self: [0, 0, 0] })
|
||||||
2,
|
|
||||||
100,
|
|
||||||
0,
|
|
||||||
1,
|
|
||||||
getDisplayProcessor({ field: { config: {} }, theme: createTheme() }),
|
|
||||||
(val) => val.toString()
|
|
||||||
);
|
);
|
||||||
|
const result = getRectDimensionsForLevel(container, level, 2, 100, 0, 1);
|
||||||
expect(result).toEqual([
|
expect(result).toEqual([
|
||||||
{ width: 99, height: 22, x: 0, y: 44, collapsed: false, ticks: 100, label: '1', unitLabel: '100' },
|
{ width: 99, height: 22, x: 0, y: 44, collapsed: false, ticks: 100, label: '1', unitLabel: '100' },
|
||||||
{ width: 3, height: 22, x: 100, y: 44, collapsed: true, ticks: 3, label: '2', unitLabel: '2' },
|
{ width: 3, height: 22, x: 100, y: 44, collapsed: true, ticks: 3, label: '2', unitLabel: '2' },
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import { DisplayProcessor } from '@grafana/data';
|
|
||||||
import { colors } from '@grafana/ui';
|
import { colors } from '@grafana/ui';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -10,7 +9,7 @@ import {
|
|||||||
PIXELS_PER_LEVEL,
|
PIXELS_PER_LEVEL,
|
||||||
} from '../../constants';
|
} from '../../constants';
|
||||||
|
|
||||||
import { ItemWithStart } from './dataTransform';
|
import { FlameGraphDataContainer, LevelItem } from './dataTransform';
|
||||||
|
|
||||||
type RectData = {
|
type RectData = {
|
||||||
width: number;
|
width: number;
|
||||||
@ -28,19 +27,18 @@ type RectData = {
|
|||||||
* into bigger rects.
|
* into bigger rects.
|
||||||
*/
|
*/
|
||||||
export function getRectDimensionsForLevel(
|
export function getRectDimensionsForLevel(
|
||||||
level: ItemWithStart[],
|
data: FlameGraphDataContainer,
|
||||||
|
level: LevelItem[],
|
||||||
levelIndex: number,
|
levelIndex: number,
|
||||||
totalTicks: number,
|
totalTicks: number,
|
||||||
rangeMin: number,
|
rangeMin: number,
|
||||||
pixelsPerTick: number,
|
pixelsPerTick: number
|
||||||
processor: DisplayProcessor,
|
|
||||||
getLabelValue: (value: number | string) => string
|
|
||||||
): RectData[] {
|
): RectData[] {
|
||||||
const coordinatesLevel = [];
|
const coordinatesLevel = [];
|
||||||
for (let barIndex = 0; barIndex < level.length; barIndex += 1) {
|
for (let barIndex = 0; barIndex < level.length; barIndex += 1) {
|
||||||
const item = level[barIndex];
|
const item = level[barIndex];
|
||||||
const barX = getBarX(item.start, totalTicks, rangeMin, pixelsPerTick);
|
const barX = getBarX(item.start, totalTicks, rangeMin, pixelsPerTick);
|
||||||
let curBarTicks = item.value;
|
let curBarTicks = data.getValue(item.itemIndex);
|
||||||
|
|
||||||
// merge very small blocks into big "collapsed" ones for performance
|
// merge very small blocks into big "collapsed" ones for performance
|
||||||
const collapsed = curBarTicks * pixelsPerTick <= COLLAPSE_THRESHOLD;
|
const collapsed = curBarTicks * pixelsPerTick <= COLLAPSE_THRESHOLD;
|
||||||
@ -48,14 +46,14 @@ export function getRectDimensionsForLevel(
|
|||||||
while (
|
while (
|
||||||
barIndex < level.length - 1 &&
|
barIndex < level.length - 1 &&
|
||||||
item.start + curBarTicks === level[barIndex + 1].start &&
|
item.start + curBarTicks === level[barIndex + 1].start &&
|
||||||
level[barIndex + 1].value * pixelsPerTick <= COLLAPSE_THRESHOLD
|
data.getValue(level[barIndex + 1].itemIndex) * pixelsPerTick <= COLLAPSE_THRESHOLD
|
||||||
) {
|
) {
|
||||||
barIndex += 1;
|
barIndex += 1;
|
||||||
curBarTicks += level[barIndex].value;
|
curBarTicks += data.getValue(level[barIndex].itemIndex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const displayValue = processor(item.value);
|
const displayValue = data.getValueDisplay(item.itemIndex);
|
||||||
let unit = displayValue.suffix ? displayValue.text + displayValue.suffix : displayValue.text;
|
let unit = displayValue.suffix ? displayValue.text + displayValue.suffix : displayValue.text;
|
||||||
|
|
||||||
const width = curBarTicks * pixelsPerTick - (collapsed ? 0 : BAR_BORDER_WIDTH * 2);
|
const width = curBarTicks * pixelsPerTick - (collapsed ? 0 : BAR_BORDER_WIDTH * 2);
|
||||||
@ -66,7 +64,7 @@ export function getRectDimensionsForLevel(
|
|||||||
y: levelIndex * PIXELS_PER_LEVEL,
|
y: levelIndex * PIXELS_PER_LEVEL,
|
||||||
collapsed,
|
collapsed,
|
||||||
ticks: curBarTicks,
|
ticks: curBarTicks,
|
||||||
label: getLabelValue(item.label),
|
label: data.getLabel(item.itemIndex),
|
||||||
unitLabel: unit,
|
unitLabel: unit,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -1,14 +1,14 @@
|
|||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
import React, { useEffect, useMemo, useState } from 'react';
|
||||||
import { useMeasure } from 'react-use';
|
import { useMeasure } from 'react-use';
|
||||||
|
|
||||||
import { DataFrame, DataFrameView, CoreApp, getEnumDisplayProcessor } from '@grafana/data';
|
import { DataFrame, CoreApp } from '@grafana/data';
|
||||||
import { useStyles2, useTheme2 } from '@grafana/ui';
|
import { useStyles2, useTheme2 } from '@grafana/ui';
|
||||||
|
|
||||||
import { MIN_WIDTH_TO_SHOW_BOTH_TOPTABLE_AND_FLAMEGRAPH, PIXELS_PER_LEVEL } from '../constants';
|
import { MIN_WIDTH_TO_SHOW_BOTH_TOPTABLE_AND_FLAMEGRAPH, PIXELS_PER_LEVEL } from '../constants';
|
||||||
|
|
||||||
import FlameGraph from './FlameGraph/FlameGraph';
|
import FlameGraph from './FlameGraph/FlameGraph';
|
||||||
import { Item, nestedSetToLevels } from './FlameGraph/dataTransform';
|
import { FlameGraphDataContainer, LevelItem, nestedSetToLevels } from './FlameGraph/dataTransform';
|
||||||
import FlameGraphHeader from './FlameGraphHeader';
|
import FlameGraphHeader from './FlameGraphHeader';
|
||||||
import FlameGraphTopTableContainer from './TopTable/FlameGraphTopTableContainer';
|
import FlameGraphTopTableContainer from './TopTable/FlameGraphTopTableContainer';
|
||||||
import { SelectedView } from './types';
|
import { SelectedView } from './types';
|
||||||
@ -30,38 +30,21 @@ const FlameGraphContainer = (props: Props) => {
|
|||||||
const [search, setSearch] = useState('');
|
const [search, setSearch] = useState('');
|
||||||
const [selectedView, setSelectedView] = useState(SelectedView.Both);
|
const [selectedView, setSelectedView] = useState(SelectedView.Both);
|
||||||
const [sizeRef, { width: containerWidth }] = useMeasure<HTMLDivElement>();
|
const [sizeRef, { width: containerWidth }] = useMeasure<HTMLDivElement>();
|
||||||
|
|
||||||
const labelField = props.data?.fields.find((f) => f.name === 'label');
|
|
||||||
|
|
||||||
const theme = useTheme2();
|
const theme = useTheme2();
|
||||||
|
|
||||||
// Label can actually be an enum field so depending on that we have to access it through display processor. This is
|
const [dataContainer, levels] = useMemo((): [FlameGraphDataContainer, LevelItem[][]] | [undefined, undefined] => {
|
||||||
// both a backward compatibility but also to allow using a simple dataFrame without enum config. This would allow
|
|
||||||
// users to use this panel with correct query from data sources that do not return profiles natively.
|
|
||||||
const getLabelValue = useCallback(
|
|
||||||
(label: string | number) => {
|
|
||||||
const enumConfig = labelField?.config?.type?.enum;
|
|
||||||
if (enumConfig) {
|
|
||||||
return getEnumDisplayProcessor(theme, enumConfig)(label).text;
|
|
||||||
} else {
|
|
||||||
return label.toString();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[labelField, 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.
|
|
||||||
const levels = useMemo(() => {
|
|
||||||
if (!props.data) {
|
if (!props.data) {
|
||||||
return [];
|
return [undefined, undefined];
|
||||||
}
|
}
|
||||||
const dataView = new DataFrameView<Item>(props.data);
|
const container = new FlameGraphDataContainer(props.data, theme);
|
||||||
return nestedSetToLevels(dataView);
|
|
||||||
}, [props.data]);
|
|
||||||
|
|
||||||
const styles = useStyles2(() => getStyles(props.app, PIXELS_PER_LEVEL * levels.length));
|
// 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)];
|
||||||
|
}, [props.data, theme]);
|
||||||
|
|
||||||
|
const styles = useStyles2(() => getStyles(props.app, PIXELS_PER_LEVEL * (levels?.length ?? 0)));
|
||||||
|
|
||||||
// If user resizes window with both as the selected view
|
// If user resizes window with both as the selected view
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -83,7 +66,7 @@ const FlameGraphContainer = (props: Props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{props.data && (
|
{dataContainer && (
|
||||||
<div ref={sizeRef} className={styles.container}>
|
<div ref={sizeRef} className={styles.container}>
|
||||||
<FlameGraphHeader
|
<FlameGraphHeader
|
||||||
app={props.app}
|
app={props.app}
|
||||||
@ -100,7 +83,7 @@ const FlameGraphContainer = (props: Props) => {
|
|||||||
|
|
||||||
{selectedView !== SelectedView.FlameGraph && (
|
{selectedView !== SelectedView.FlameGraph && (
|
||||||
<FlameGraphTopTableContainer
|
<FlameGraphTopTableContainer
|
||||||
data={props.data}
|
data={dataContainer}
|
||||||
app={props.app}
|
app={props.app}
|
||||||
totalLevels={levels.length}
|
totalLevels={levels.length}
|
||||||
selectedView={selectedView}
|
selectedView={selectedView}
|
||||||
@ -110,13 +93,12 @@ const FlameGraphContainer = (props: Props) => {
|
|||||||
setSelectedBarIndex={setSelectedBarIndex}
|
setSelectedBarIndex={setSelectedBarIndex}
|
||||||
setRangeMin={setRangeMin}
|
setRangeMin={setRangeMin}
|
||||||
setRangeMax={setRangeMax}
|
setRangeMax={setRangeMax}
|
||||||
getLabelValue={getLabelValue}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{selectedView !== SelectedView.TopTable && (
|
{selectedView !== SelectedView.TopTable && (
|
||||||
<FlameGraph
|
<FlameGraph
|
||||||
data={props.data}
|
data={dataContainer}
|
||||||
app={props.app}
|
app={props.app}
|
||||||
flameGraphHeight={props.flameGraphHeight}
|
flameGraphHeight={props.flameGraphHeight}
|
||||||
levels={levels}
|
levels={levels}
|
||||||
@ -130,7 +112,6 @@ const FlameGraphContainer = (props: Props) => {
|
|||||||
setRangeMin={setRangeMin}
|
setRangeMin={setRangeMin}
|
||||||
setRangeMax={setRangeMax}
|
setRangeMax={setRangeMax}
|
||||||
selectedView={selectedView}
|
selectedView={selectedView}
|
||||||
getLabelValue={getLabelValue}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import { render, screen } from '@testing-library/react';
|
import { render, screen } from '@testing-library/react';
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
|
|
||||||
import { CoreApp, DataFrameView, MutableDataFrame } from '@grafana/data';
|
import { CoreApp, MutableDataFrame } from '@grafana/data';
|
||||||
|
|
||||||
import { Item, nestedSetToLevels } from '../FlameGraph/dataTransform';
|
import { FlameGraphDataContainer, nestedSetToLevels } from '../FlameGraph/dataTransform';
|
||||||
import { data } from '../FlameGraph/testData/dataNestedSet';
|
import { data } from '../FlameGraph/testData/dataNestedSet';
|
||||||
import { SelectedView } from '../types';
|
import { SelectedView } from '../types';
|
||||||
|
|
||||||
@ -15,12 +15,12 @@ describe('FlameGraphTopTableContainer', () => {
|
|||||||
const [selectedView, _] = useState(SelectedView.Both);
|
const [selectedView, _] = useState(SelectedView.Both);
|
||||||
|
|
||||||
const flameGraphData = new MutableDataFrame(data);
|
const flameGraphData = new MutableDataFrame(data);
|
||||||
const dataView = new DataFrameView<Item>(flameGraphData);
|
const container = new FlameGraphDataContainer(flameGraphData);
|
||||||
const levels = nestedSetToLevels(dataView);
|
const levels = nestedSetToLevels(container);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FlameGraphTopTableContainer
|
<FlameGraphTopTableContainer
|
||||||
data={flameGraphData}
|
data={container}
|
||||||
app={CoreApp.Explore}
|
app={CoreApp.Explore}
|
||||||
totalLevels={levels.length}
|
totalLevels={levels.length}
|
||||||
selectedView={selectedView}
|
selectedView={selectedView}
|
||||||
@ -30,7 +30,6 @@ describe('FlameGraphTopTableContainer', () => {
|
|||||||
setSelectedBarIndex={jest.fn()}
|
setSelectedBarIndex={jest.fn()}
|
||||||
setRangeMin={jest.fn()}
|
setRangeMin={jest.fn()}
|
||||||
setRangeMax={jest.fn()}
|
setRangeMax={jest.fn()}
|
||||||
getLabelValue={(val) => val.toString()}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,17 +1,18 @@
|
|||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
import React, { useCallback, useEffect, useState } from 'react';
|
import React, { useMemo } from 'react';
|
||||||
import AutoSizer from 'react-virtualized-auto-sizer';
|
import AutoSizer from 'react-virtualized-auto-sizer';
|
||||||
|
|
||||||
import { CoreApp, DataFrame, Field, FieldType, getDisplayProcessor } from '@grafana/data';
|
import { CoreApp, DisplayValue } from '@grafana/data';
|
||||||
import { useStyles2, useTheme2 } from '@grafana/ui';
|
import { useStyles2 } from '@grafana/ui';
|
||||||
|
|
||||||
import { PIXELS_PER_LEVEL } from '../../constants';
|
import { PIXELS_PER_LEVEL } from '../../constants';
|
||||||
import { SampleUnit, SelectedView, TableData, TopTableData } from '../types';
|
import { FlameGraphDataContainer } from '../FlameGraph/dataTransform';
|
||||||
|
import { SelectedView, TableData, TopTableData } from '../types';
|
||||||
|
|
||||||
import FlameGraphTopTable from './FlameGraphTopTable';
|
import FlameGraphTopTable from './FlameGraphTopTable';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
data: DataFrame;
|
data: FlameGraphDataContainer;
|
||||||
app: CoreApp;
|
app: CoreApp;
|
||||||
totalLevels: number;
|
totalLevels: number;
|
||||||
selectedView: SelectedView;
|
selectedView: SelectedView;
|
||||||
@ -21,7 +22,6 @@ type Props = {
|
|||||||
setSelectedBarIndex: (bar: number) => void;
|
setSelectedBarIndex: (bar: number) => void;
|
||||||
setRangeMin: (range: number) => void;
|
setRangeMin: (range: number) => void;
|
||||||
setRangeMax: (range: number) => void;
|
setRangeMax: (range: number) => void;
|
||||||
getLabelValue: (label: string | number) => string;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const FlameGraphTopTableContainer = ({
|
const FlameGraphTopTableContainer = ({
|
||||||
@ -35,70 +35,26 @@ const FlameGraphTopTableContainer = ({
|
|||||||
setSelectedBarIndex,
|
setSelectedBarIndex,
|
||||||
setRangeMin,
|
setRangeMin,
|
||||||
setRangeMax,
|
setRangeMax,
|
||||||
getLabelValue,
|
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const styles = useStyles2(() => getStyles(selectedView, app));
|
const styles = useStyles2(() => getStyles(selectedView, app));
|
||||||
const theme = useTheme2();
|
|
||||||
const [topTable, setTopTable] = useState<TopTableData[]>();
|
|
||||||
const valueField =
|
|
||||||
data.fields.find((f) => f.name === 'value') ?? data.fields.find((f) => f.type === FieldType.number);
|
|
||||||
|
|
||||||
const selfField = data.fields.find((f) => f.name === 'self') ?? data.fields.find((f) => f.type === FieldType.number);
|
const topTable = useMemo(() => {
|
||||||
const labelsField = data.fields.find((f) => f.name === 'label');
|
// Group the data by label
|
||||||
|
// TODO: should be by filename + funcName + linenumber?
|
||||||
const sortLevelsIntoTable = useCallback(() => {
|
|
||||||
let label, self, value;
|
|
||||||
let table: { [key: string]: TableData } = {};
|
let table: { [key: string]: TableData } = {};
|
||||||
|
for (let i = 0; i < data.data.length; i++) {
|
||||||
if (valueField && selfField && labelsField) {
|
const value = data.getValue(i);
|
||||||
const valueValues = valueField.values;
|
const self = data.getSelf(i);
|
||||||
const selfValues = selfField.values;
|
const label = data.getLabel(i);
|
||||||
const labelValues = labelsField.values;
|
table[label] = table[label] || {};
|
||||||
|
table[label].self = table[label].self ? table[label].self + self : self;
|
||||||
for (let i = 0; i < valueValues.length; i++) {
|
table[label].total = table[label].total ? table[label].total + value : value;
|
||||||
value = valueValues.get(i);
|
|
||||||
self = selfValues.get(i);
|
|
||||||
label = getLabelValue(labelValues.get(i));
|
|
||||||
table[label] = table[label] || {};
|
|
||||||
table[label].self = table[label].self ? table[label].self + self : self;
|
|
||||||
table[label].total = table[label].total ? table[label].total + value : value;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return table;
|
|
||||||
}, [getLabelValue, selfField, valueField, labelsField]);
|
|
||||||
|
|
||||||
const getTopTableData = useCallback(
|
|
||||||
(field: Field, value: number) => {
|
|
||||||
const processor = getDisplayProcessor({ field, theme });
|
|
||||||
const displayValue = processor(value);
|
|
||||||
let unitValue = displayValue.text + displayValue.suffix;
|
|
||||||
|
|
||||||
switch (field.config.unit) {
|
|
||||||
case SampleUnit.Bytes:
|
|
||||||
break;
|
|
||||||
case SampleUnit.Nanoseconds:
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
if (!displayValue.suffix) {
|
|
||||||
// Makes sure we don't show 123undefined or something like that if suffix isn't defined
|
|
||||||
unitValue = displayValue.text;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return unitValue;
|
|
||||||
},
|
|
||||||
[theme]
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const table = sortLevelsIntoTable();
|
|
||||||
|
|
||||||
let topTable: TopTableData[] = [];
|
let topTable: TopTableData[] = [];
|
||||||
for (let key in table) {
|
for (let key in table) {
|
||||||
const selfUnit = getTopTableData(selfField!, table[key].self);
|
const selfUnit = handleUnits(data.valueDisplayProcessor(table[key].self), data.getUnitTitle());
|
||||||
const valueUnit = getTopTableData(valueField!, table[key].total);
|
const valueUnit = handleUnits(data.valueDisplayProcessor(table[key].total), data.getUnitTitle());
|
||||||
|
|
||||||
topTable.push({
|
topTable.push({
|
||||||
symbol: key,
|
symbol: key,
|
||||||
@ -107,8 +63,8 @@ const FlameGraphTopTableContainer = ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
setTopTable(topTable);
|
return topTable;
|
||||||
}, [data.fields, selfField, sortLevelsIntoTable, valueField, getTopTableData]);
|
}, [data]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -135,6 +91,17 @@ const FlameGraphTopTableContainer = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function handleUnits(displayValue: DisplayValue, unit: string) {
|
||||||
|
let unitValue = displayValue.text + displayValue.suffix;
|
||||||
|
if (unit === 'Count') {
|
||||||
|
if (!displayValue.suffix) {
|
||||||
|
// Makes sure we don't show 123undefined or something like that if suffix isn't defined
|
||||||
|
unitValue = displayValue.text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return unitValue;
|
||||||
|
}
|
||||||
|
|
||||||
const getStyles = (selectedView: SelectedView, app: CoreApp) => {
|
const getStyles = (selectedView: SelectedView, app: CoreApp) => {
|
||||||
const marginRight = '20px';
|
const marginRight = '20px';
|
||||||
|
|
||||||
|
@ -1,13 +1,3 @@
|
|||||||
export type TooltipData = {
|
|
||||||
name: string;
|
|
||||||
percentValue: number;
|
|
||||||
percentSelf: number;
|
|
||||||
unitTitle: string;
|
|
||||||
unitValue: string;
|
|
||||||
unitSelf: string;
|
|
||||||
samples: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ContextMenuData = {
|
export type ContextMenuData = {
|
||||||
e: MouseEvent;
|
e: MouseEvent;
|
||||||
levelIndex: number;
|
levelIndex: number;
|
||||||
|
Loading…
Reference in New Issue
Block a user