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 React, { useState } from 'react';
|
||||
|
||||
import { CoreApp, DataFrameView, MutableDataFrame } from '@grafana/data';
|
||||
import { CoreApp, MutableDataFrame } from '@grafana/data';
|
||||
|
||||
import { SelectedView } from '../types';
|
||||
|
||||
import FlameGraph from './FlameGraph';
|
||||
import { Item, nestedSetToLevels } from './dataTransform';
|
||||
import { FlameGraphDataContainer, nestedSetToLevels } from './dataTransform';
|
||||
import { data } from './testData/dataNestedSet';
|
||||
|
||||
import 'jest-canvas-mock';
|
||||
@ -29,12 +29,12 @@ describe('FlameGraph', () => {
|
||||
const [selectedView, _] = useState(SelectedView.Both);
|
||||
|
||||
const flameGraphData = new MutableDataFrame(data);
|
||||
const dataView = new DataFrameView<Item>(flameGraphData);
|
||||
const levels = nestedSetToLevels(dataView);
|
||||
const container = new FlameGraphDataContainer(flameGraphData);
|
||||
const levels = nestedSetToLevels(container);
|
||||
|
||||
return (
|
||||
<FlameGraph
|
||||
data={flameGraphData}
|
||||
data={container}
|
||||
app={CoreApp.Explore}
|
||||
levels={levels}
|
||||
topLevelIndex={topLevelIndex}
|
||||
@ -47,7 +47,6 @@ describe('FlameGraph', () => {
|
||||
setRangeMin={setRangeMin}
|
||||
setRangeMax={setRangeMax}
|
||||
selectedView={selectedView}
|
||||
getLabelValue={(val) => val.toString()}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
@ -21,22 +21,22 @@ import uFuzzy from '@leeoniya/ufuzzy';
|
||||
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||
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 { TooltipData, SelectedView, ContextMenuData } from '../types';
|
||||
import { SelectedView, ContextMenuData } from '../types';
|
||||
|
||||
import FlameGraphContextMenu from './FlameGraphContextMenu';
|
||||
import FlameGraphMetadata from './FlameGraphMetadata';
|
||||
import FlameGraphTooltip, { getTooltipData } from './FlameGraphTooltip';
|
||||
import { ItemWithStart } from './dataTransform';
|
||||
import FlameGraphTooltip from './FlameGraphTooltip';
|
||||
import { FlameGraphDataContainer, LevelItem } from './dataTransform';
|
||||
import { getBarX, getRectDimensionsForLevel, renderRect } from './rendering';
|
||||
|
||||
type Props = {
|
||||
data: DataFrame;
|
||||
data: FlameGraphDataContainer;
|
||||
app: CoreApp;
|
||||
flameGraphHeight?: number;
|
||||
levels: ItemWithStart[][];
|
||||
levels: LevelItem[][];
|
||||
topLevelIndex: number;
|
||||
selectedBarIndex: number;
|
||||
rangeMin: number;
|
||||
@ -48,7 +48,6 @@ type Props = {
|
||||
setRangeMax: (range: number) => void;
|
||||
selectedView: SelectedView;
|
||||
style?: React.CSSProperties;
|
||||
getLabelValue: (label: string | number) => string;
|
||||
};
|
||||
|
||||
const FlameGraph = ({
|
||||
@ -66,52 +65,35 @@ const FlameGraph = ({
|
||||
setRangeMin,
|
||||
setRangeMax,
|
||||
selectedView,
|
||||
getLabelValue,
|
||||
}: Props) => {
|
||||
const styles = getStyles(selectedView, app, flameGraphHeight);
|
||||
const totalTicks = data.fields[1].values.get(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 totalTicks = data.getValue(0);
|
||||
|
||||
const [sizeRef, { width: wrapperWidth }] = useMeasure<HTMLDivElement>();
|
||||
const graphRef = useRef<HTMLCanvasElement>(null);
|
||||
const tooltipRef = useRef<HTMLDivElement>(null);
|
||||
const [tooltipData, setTooltipData] = useState<TooltipData>();
|
||||
const [tooltipItem, setTooltipItem] = useState<LevelItem>();
|
||||
const [contextMenuData, setContextMenuData] = useState<ContextMenuData>();
|
||||
|
||||
const [ufuzzy] = useState(() => {
|
||||
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 = new Set<string>();
|
||||
|
||||
if (search) {
|
||||
let idxs = ufuzzy.filter(uniqueLabels, search);
|
||||
let idxs = ufuzzy.filter(data.getUniqueLabels(), search);
|
||||
|
||||
if (idxs) {
|
||||
for (let idx of idxs) {
|
||||
foundLabels.add(uniqueLabels[idx]);
|
||||
foundLabels.add(data.getUniqueLabels()[idx]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return foundLabels;
|
||||
}, [ufuzzy, search, uniqueLabels]);
|
||||
}, [ufuzzy, search, data]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!levels.length) {
|
||||
@ -131,48 +113,25 @@ const FlameGraph = ({
|
||||
ctx.font = 12 * window.devicePixelRatio + 'px monospace';
|
||||
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++) {
|
||||
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(
|
||||
level,
|
||||
levelIndex,
|
||||
totalTicks,
|
||||
rangeMin,
|
||||
pixelsPerTick,
|
||||
processor,
|
||||
getLabelValue
|
||||
);
|
||||
const dimensions = getRectDimensionsForLevel(data, level, levelIndex, totalTicks, rangeMin, pixelsPerTick);
|
||||
for (const rect of dimensions) {
|
||||
// Render each rectangle based on the computed dimensions
|
||||
renderRect(ctx, rect, totalTicks, rangeMin, rangeMax, search, levelIndex, topLevelIndex, foundLabels);
|
||||
}
|
||||
}
|
||||
}, [
|
||||
levels,
|
||||
wrapperWidth,
|
||||
valueField,
|
||||
totalTicks,
|
||||
rangeMin,
|
||||
rangeMax,
|
||||
search,
|
||||
topLevelIndex,
|
||||
foundLabels,
|
||||
getLabelValue,
|
||||
]);
|
||||
}, [data, levels, wrapperWidth, totalTicks, rangeMin, rangeMax, search, topLevelIndex, foundLabels]);
|
||||
|
||||
useEffect(() => {
|
||||
if (graphRef.current) {
|
||||
graphRef.current.onclick = (e) => {
|
||||
setTooltipData(undefined);
|
||||
setTooltipItem(undefined);
|
||||
const pixelsPerTick = graphRef.current!.clientWidth / totalTicks / (rangeMax - rangeMin);
|
||||
const { levelIndex, barIndex } = convertPixelCoordinatesToBarCoordinates(
|
||||
data,
|
||||
e,
|
||||
pixelsPerTick,
|
||||
levels,
|
||||
@ -191,9 +150,10 @@ const FlameGraph = ({
|
||||
|
||||
graphRef.current!.onmousemove = (e) => {
|
||||
if (tooltipRef.current && contextMenuData === undefined) {
|
||||
setTooltipData(undefined);
|
||||
setTooltipItem(undefined);
|
||||
const pixelsPerTick = graphRef.current!.clientWidth / totalTicks / (rangeMax - rangeMin);
|
||||
const { levelIndex, barIndex } = convertPixelCoordinatesToBarCoordinates(
|
||||
data,
|
||||
e,
|
||||
pixelsPerTick,
|
||||
levels,
|
||||
@ -204,19 +164,17 @@ const FlameGraph = ({
|
||||
if (barIndex !== -1 && !isNaN(levelIndex) && !isNaN(barIndex)) {
|
||||
tooltipRef.current.style.left = e.clientX + 10 + 'px';
|
||||
tooltipRef.current.style.top = e.clientY + 'px';
|
||||
|
||||
const bar = levels[levelIndex][barIndex];
|
||||
const tooltipData = getTooltipData(valueField, bar.label, bar.value, bar.self, totalTicks);
|
||||
setTooltipData(tooltipData);
|
||||
setTooltipItem(levels[levelIndex][barIndex]);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
graphRef.current!.onmouseleave = () => {
|
||||
setTooltipData(undefined);
|
||||
setTooltipItem(undefined);
|
||||
};
|
||||
}
|
||||
}, [
|
||||
data,
|
||||
levels,
|
||||
rangeMin,
|
||||
rangeMax,
|
||||
@ -227,13 +185,12 @@ const FlameGraph = ({
|
||||
setRangeMin,
|
||||
setRangeMax,
|
||||
selectedView,
|
||||
valueField,
|
||||
setSelectedBarIndex,
|
||||
setContextMenuData,
|
||||
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(() => {
|
||||
const handleOnClick = (e: MouseEvent) => {
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
@ -248,18 +205,19 @@ const FlameGraph = ({
|
||||
return (
|
||||
<div className={styles.graph} ref={sizeRef}>
|
||||
<FlameGraphMetadata
|
||||
data={data}
|
||||
levels={levels}
|
||||
topLevelIndex={topLevelIndex}
|
||||
selectedBarIndex={selectedBarIndex}
|
||||
valueField={valueField}
|
||||
totalTicks={totalTicks}
|
||||
/>
|
||||
<div className={styles.canvasContainer} id="flameGraphCanvasContainer">
|
||||
<canvas ref={graphRef} data-testid="flameGraph" />
|
||||
</div>
|
||||
<FlameGraphTooltip tooltipRef={tooltipRef} tooltipData={tooltipData!} getLabelValue={getLabelValue} />
|
||||
<FlameGraphTooltip tooltipRef={tooltipRef} item={tooltipItem} data={data} totalTicks={totalTicks} />
|
||||
{contextMenuData && (
|
||||
<FlameGraphContextMenu
|
||||
data={data}
|
||||
contextMenuData={contextMenuData!}
|
||||
levels={levels}
|
||||
totalTicks={totalTicks}
|
||||
@ -269,7 +227,6 @@ const FlameGraph = ({
|
||||
setSelectedBarIndex={setSelectedBarIndex}
|
||||
setRangeMin={setRangeMin}
|
||||
setRangeMax={setRangeMax}
|
||||
getLabelValue={getLabelValue}
|
||||
/>
|
||||
)}
|
||||
</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
|
||||
// the canvas.
|
||||
const convertPixelCoordinatesToBarCoordinates = (
|
||||
data: FlameGraphDataContainer,
|
||||
e: MouseEvent,
|
||||
pixelsPerTick: number,
|
||||
levels: ItemWithStart[][],
|
||||
levels: LevelItem[][],
|
||||
totalTicks: number,
|
||||
rangeMin: number
|
||||
) => {
|
||||
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 };
|
||||
};
|
||||
|
||||
@ -310,7 +268,8 @@ const convertPixelCoordinatesToBarCoordinates = (
|
||||
*/
|
||||
const getBarIndex = (
|
||||
x: number,
|
||||
level: ItemWithStart[],
|
||||
data: FlameGraphDataContainer,
|
||||
level: LevelItem[],
|
||||
pixelsPerTick: number,
|
||||
totalTicks: number,
|
||||
rangeMin: number
|
||||
@ -323,7 +282,7 @@ const getBarIndex = (
|
||||
const midIndex = (start + end) >> 1;
|
||||
const startOfBar = getBarX(level[midIndex].start, totalTicks, rangeMin, pixelsPerTick);
|
||||
const startOfNextBar = getBarX(
|
||||
level[midIndex].start + level[midIndex].value,
|
||||
level[midIndex].start + data.getValue(level[midIndex].itemIndex),
|
||||
totalTicks,
|
||||
rangeMin,
|
||||
pixelsPerTick
|
||||
|
@ -4,11 +4,12 @@ import { MenuItem, ContextMenu } from '@grafana/ui';
|
||||
|
||||
import { ContextMenuData } from '../types';
|
||||
|
||||
import { ItemWithStart } from './dataTransform';
|
||||
import { FlameGraphDataContainer, LevelItem } from './dataTransform';
|
||||
|
||||
type Props = {
|
||||
contextMenuData: ContextMenuData;
|
||||
levels: ItemWithStart[][];
|
||||
data: FlameGraphDataContainer;
|
||||
levels: LevelItem[][];
|
||||
totalTicks: number;
|
||||
graphRef: React.RefObject<HTMLCanvasElement>;
|
||||
setContextMenuData: (event: ContextMenuData | undefined) => void;
|
||||
@ -16,7 +17,6 @@ type Props = {
|
||||
setSelectedBarIndex: (bar: number) => void;
|
||||
setRangeMin: (range: number) => void;
|
||||
setRangeMax: (range: number) => void;
|
||||
getLabelValue: (label: string | number) => string;
|
||||
};
|
||||
|
||||
const FlameGraphContextMenu = ({
|
||||
@ -29,8 +29,10 @@ const FlameGraphContextMenu = ({
|
||||
setSelectedBarIndex,
|
||||
setRangeMin,
|
||||
setRangeMax,
|
||||
getLabelValue,
|
||||
data,
|
||||
}: Props) => {
|
||||
const clickedItem = levels[contextMenuData.levelIndex][contextMenuData.barIndex];
|
||||
|
||||
const renderMenuItems = () => {
|
||||
return (
|
||||
<>
|
||||
@ -41,12 +43,8 @@ const FlameGraphContextMenu = ({
|
||||
if (graphRef.current && contextMenuData) {
|
||||
setTopLevelIndex(contextMenuData.levelIndex);
|
||||
setSelectedBarIndex(contextMenuData.barIndex);
|
||||
setRangeMin(levels[contextMenuData.levelIndex][contextMenuData.barIndex].start / totalTicks);
|
||||
setRangeMax(
|
||||
(levels[contextMenuData.levelIndex][contextMenuData.barIndex].start +
|
||||
levels[contextMenuData.levelIndex][contextMenuData.barIndex].value) /
|
||||
totalTicks
|
||||
);
|
||||
setRangeMin(clickedItem.start / totalTicks);
|
||||
setRangeMax((clickedItem.start + data.getValue(clickedItem.itemIndex)) / totalTicks);
|
||||
setContextMenuData(undefined);
|
||||
}
|
||||
}}
|
||||
@ -56,8 +54,7 @@ const FlameGraphContextMenu = ({
|
||||
icon={'copy'}
|
||||
onClick={() => {
|
||||
if (graphRef.current && contextMenuData) {
|
||||
const bar = levels[contextMenuData.levelIndex][contextMenuData.barIndex];
|
||||
navigator.clipboard.writeText(getLabelValue(bar.label)).then(() => {
|
||||
navigator.clipboard.writeText(data.getLabel(clickedItem.itemIndex)).then(() => {
|
||||
setContextMenuData(undefined);
|
||||
});
|
||||
}
|
||||
|
@ -1,10 +1,28 @@
|
||||
import { ArrayVector, Field, FieldType } from '@grafana/data';
|
||||
import { MutableDataFrame } from '@grafana/data';
|
||||
|
||||
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', () => {
|
||||
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({
|
||||
percentValue: 18.83,
|
||||
unitTitle: 'RAM',
|
||||
@ -14,7 +32,10 @@ describe('should get metadata correctly', () => {
|
||||
});
|
||||
|
||||
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({
|
||||
percentValue: 18.83,
|
||||
unitTitle: 'Count',
|
||||
@ -24,16 +45,10 @@ describe('should get metadata correctly', () => {
|
||||
});
|
||||
|
||||
it('without unit', () => {
|
||||
const metadata = getMetadata(
|
||||
{
|
||||
name: 'test',
|
||||
type: FieldType.number,
|
||||
values: new ArrayVector(),
|
||||
config: {},
|
||||
},
|
||||
1_624_078_250,
|
||||
8_624_078_250
|
||||
const container = new FlameGraphDataContainer(
|
||||
makeDataFrame({ value: [1_624_078_250], level: [1], label: ['1'], self: [0] })
|
||||
);
|
||||
const metadata = getMetadata(container, { itemIndex: 0, start: 0 }, 8_624_078_250);
|
||||
expect(metadata).toEqual({
|
||||
percentValue: 18.83,
|
||||
unitTitle: 'Count',
|
||||
@ -43,7 +58,10 @@ describe('should get metadata correctly', () => {
|
||||
});
|
||||
|
||||
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({
|
||||
percentValue: 18.83,
|
||||
unitTitle: 'Count',
|
||||
@ -53,7 +71,10 @@ describe('should get metadata correctly', () => {
|
||||
});
|
||||
|
||||
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({
|
||||
percentValue: 18.83,
|
||||
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 React from 'react';
|
||||
|
||||
import { createTheme, Field, getDisplayProcessor, Vector } from '@grafana/data';
|
||||
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 = {
|
||||
levels: ItemWithStart[][];
|
||||
data: FlameGraphDataContainer;
|
||||
levels: LevelItem[][];
|
||||
topLevelIndex: number;
|
||||
selectedBarIndex: number;
|
||||
valueField: Field<number, Vector<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);
|
||||
if (levels[topLevelIndex] && 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})`;
|
||||
return <>{<div className={styles.metadata}>{metadataText}</div>}</>;
|
||||
}
|
||||
return <></>;
|
||||
});
|
||||
|
||||
export const getMetadata = (field: Field, value: number, totalTicks: number): Metadata => {
|
||||
let unitTitle;
|
||||
const processor = getDisplayProcessor({ field, theme: createTheme() /* theme does not matter for us here */ });
|
||||
const displayValue = processor(value);
|
||||
const percentValue = Math.round(10000 * (value / totalTicks)) / 100;
|
||||
export const getMetadata = (data: FlameGraphDataContainer, bar: LevelItem, totalTicks: number): Metadata => {
|
||||
const displayValue = data.getValueDisplay(bar.itemIndex);
|
||||
const percentValue = Math.round(10000 * (displayValue.numeric / totalTicks)) / 100;
|
||||
let unitValue = displayValue.text + displayValue.suffix;
|
||||
|
||||
switch (field.config.unit) {
|
||||
case SampleUnit.Bytes:
|
||||
unitTitle = 'RAM';
|
||||
break;
|
||||
case SampleUnit.Nanoseconds:
|
||||
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;
|
||||
const unitTitle = data.getUnitTitle();
|
||||
if (unitTitle === 'Count') {
|
||||
if (!displayValue.suffix) {
|
||||
// Makes sure we don't show 123undefined or something like that if suffix isn't defined
|
||||
unitValue = displayValue.text;
|
||||
}
|
||||
}
|
||||
|
||||
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 { 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', () => {
|
||||
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({
|
||||
name: 'total',
|
||||
percentSelf: 0.01,
|
||||
@ -17,7 +30,7 @@ describe('should get tooltip data correctly', () => {
|
||||
});
|
||||
|
||||
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({
|
||||
name: 'total',
|
||||
percentSelf: 0.01,
|
||||
@ -30,18 +43,7 @@ describe('should get tooltip data correctly', () => {
|
||||
});
|
||||
|
||||
it('without unit', () => {
|
||||
const tooltipData = getTooltipData(
|
||||
{
|
||||
name: 'test',
|
||||
type: FieldType.number,
|
||||
values: new ArrayVector(),
|
||||
config: {},
|
||||
},
|
||||
'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({
|
||||
name: 'total',
|
||||
percentSelf: 0.01,
|
||||
@ -54,7 +56,7 @@ describe('should get tooltip data correctly', () => {
|
||||
});
|
||||
|
||||
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({
|
||||
name: 'total',
|
||||
percentSelf: 0.01,
|
||||
@ -67,7 +69,7 @@ describe('should get tooltip data correctly', () => {
|
||||
});
|
||||
|
||||
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({
|
||||
name: 'total',
|
||||
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 {
|
||||
name: 'test',
|
||||
name,
|
||||
type: FieldType.number,
|
||||
config: {
|
||||
unit,
|
||||
},
|
||||
values: new ArrayVector(),
|
||||
values: new ArrayVector(values),
|
||||
};
|
||||
}
|
@ -1,94 +1,95 @@
|
||||
import { css } from '@emotion/css';
|
||||
import React, { LegacyRef } from 'react';
|
||||
|
||||
import { createTheme, Field, getDisplayProcessor } from '@grafana/data';
|
||||
import { useStyles2, Tooltip } from '@grafana/ui';
|
||||
|
||||
import { TooltipData, SampleUnit } from '../types';
|
||||
import { FlameGraphDataContainer, LevelItem } from './dataTransform';
|
||||
|
||||
type Props = {
|
||||
tooltipRef: LegacyRef<HTMLDivElement>;
|
||||
tooltipData: TooltipData;
|
||||
getLabelValue: (label: string | number) => string;
|
||||
data: FlameGraphDataContainer;
|
||||
totalTicks: number;
|
||||
item?: LevelItem;
|
||||
tooltipRef?: LegacyRef<HTMLDivElement>;
|
||||
};
|
||||
|
||||
const FlameGraphTooltip = ({ tooltipRef, tooltipData, getLabelValue }: Props) => {
|
||||
const FlameGraphTooltip = ({ data, tooltipRef, item, totalTicks }: Props) => {
|
||||
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 (
|
||||
<div ref={tooltipRef} className={styles.tooltip}>
|
||||
{tooltipData && (
|
||||
<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>
|
||||
)}
|
||||
{content}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const getTooltipData = (
|
||||
field: Field,
|
||||
label: string,
|
||||
value: number,
|
||||
self: number,
|
||||
totalTicks: number
|
||||
): TooltipData => {
|
||||
let unitTitle;
|
||||
type TooltipData = {
|
||||
name: string;
|
||||
percentValue: number;
|
||||
percentSelf: number;
|
||||
unitTitle: string;
|
||||
unitValue: string;
|
||||
unitSelf: string;
|
||||
samples: string;
|
||||
};
|
||||
|
||||
const processor = getDisplayProcessor({ field, theme: createTheme() /* theme does not matter for us here */ });
|
||||
const displayValue = processor(value);
|
||||
const displaySelf = processor(self);
|
||||
export const getTooltipData = (data: FlameGraphDataContainer, item: LevelItem, totalTicks: number): TooltipData => {
|
||||
const displayValue = data.getValueDisplay(item.itemIndex);
|
||||
const displaySelf = data.getSelfDisplay(item.itemIndex);
|
||||
|
||||
const percentValue = Math.round(10000 * (value / totalTicks)) / 100;
|
||||
const percentSelf = Math.round(10000 * (self / totalTicks)) / 100;
|
||||
const percentValue = Math.round(10000 * (displayValue.numeric / totalTicks)) / 100;
|
||||
const percentSelf = Math.round(10000 * (displaySelf.numeric / totalTicks)) / 100;
|
||||
let unitValue = displayValue.text + displayValue.suffix;
|
||||
let unitSelf = displaySelf.text + displaySelf.suffix;
|
||||
|
||||
switch (field.config.unit) {
|
||||
case SampleUnit.Bytes:
|
||||
unitTitle = 'RAM';
|
||||
break;
|
||||
case SampleUnit.Nanoseconds:
|
||||
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;
|
||||
}
|
||||
if (!displaySelf.suffix) {
|
||||
// Makes sure we don't show 123undefined or something like that if suffix isn't defined
|
||||
unitSelf = displaySelf.text;
|
||||
}
|
||||
break;
|
||||
const unitTitle = data.getUnitTitle();
|
||||
if (unitTitle === 'Count') {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
name: label,
|
||||
name: data.getLabel(item.itemIndex),
|
||||
percentValue,
|
||||
percentSelf,
|
||||
unitTitle,
|
||||
unitValue,
|
||||
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', () => {
|
||||
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: 'value', values: [10, 5, 3, 1, 1, 4, 3, 2, 1] },
|
||||
{ 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([
|
||||
[{ level: 0, value: 10, start: 0, label: '1' }],
|
||||
[{ start: 0, itemIndex: 0 }],
|
||||
[
|
||||
{ level: 1, value: 5, start: 0, label: '2' },
|
||||
{ level: 1, value: 4, start: 5, label: '6' },
|
||||
{ start: 0, itemIndex: 1 },
|
||||
{ start: 5, itemIndex: 5 },
|
||||
],
|
||||
[
|
||||
{ level: 2, value: 3, start: 0, label: '3' },
|
||||
{ level: 2, value: 1, start: 3, label: '5' },
|
||||
{ level: 2, value: 3, start: 5, label: '7' },
|
||||
{ start: 0, itemIndex: 2 },
|
||||
{ start: 3, itemIndex: 4 },
|
||||
{ start: 5, itemIndex: 6 },
|
||||
],
|
||||
[
|
||||
{ level: 3, value: 1, start: 0, label: '4' },
|
||||
{ level: 3, value: 2, start: 5, label: '8' },
|
||||
{ start: 0, itemIndex: 3 },
|
||||
{ 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: 'value', values: [10, 5, 3, 1] },
|
||||
{ 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([
|
||||
[{ level: 0, value: 10, start: 0, label: '1' }],
|
||||
[{ start: 0, itemIndex: 0 }],
|
||||
[
|
||||
{ level: 1, value: 5, start: 0, label: '2' },
|
||||
{ level: 1, value: 3, start: 5, label: '3' },
|
||||
{ level: 1, value: 1, start: 8, label: '4' },
|
||||
{ start: 0, itemIndex: 1 },
|
||||
{ start: 5, itemIndex: 2 },
|
||||
{ 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 };
|
||||
export type ItemWithStart = Item & { start: number };
|
||||
import { SampleUnit } from '../types';
|
||||
|
||||
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
|
||||
* rendering code.
|
||||
* @param dataView
|
||||
*/
|
||||
export function nestedSetToLevels(dataView: DataFrameView<Item>): ItemWithStart[][] {
|
||||
const levels: ItemWithStart[][] = [];
|
||||
export function nestedSetToLevels(container: FlameGraphDataContainer): LevelItem[][] {
|
||||
const levels: LevelItem[][] = [];
|
||||
let offset = 0;
|
||||
|
||||
for (let i = 0; i < dataView.length; i++) {
|
||||
// We have to clone the items as .get(i) returns a changing pointer not the data themselves.
|
||||
const item = { ...dataView.get(i) };
|
||||
const prevItem = i > 0 ? { ...dataView.get(i - 1) } : undefined;
|
||||
for (let i = 0; i < container.data.length; i++) {
|
||||
const currentLevel = container.getLevel(i);
|
||||
const prevLevel = i > 0 ? container.getLevel(i - 1) : undefined;
|
||||
|
||||
levels[item.level] = levels[item.level] || [];
|
||||
if (prevItem && prevItem.level >= item.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.
|
||||
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[item.level][levels[item.level].length - 1];
|
||||
offset = lastItem.start + lastItem.value;
|
||||
const lastItem = levels[currentLevel][levels[currentLevel].length - 1];
|
||||
offset = lastItem.start + container.getValue(lastItem.itemIndex);
|
||||
}
|
||||
const newItem: ItemWithStart = {
|
||||
...item,
|
||||
const newItem: LevelItem = {
|
||||
itemIndex: i,
|
||||
start: offset,
|
||||
};
|
||||
|
||||
levels[item.level].push(newItem);
|
||||
levels[currentLevel].push(newItem);
|
||||
}
|
||||
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';
|
||||
|
||||
function makeDataFrame(fields: Record<string, Array<number | string>>) {
|
||||
return new MutableDataFrame({
|
||||
fields: Object.keys(fields).map((key) => ({
|
||||
name: key,
|
||||
values: fields[key],
|
||||
})),
|
||||
});
|
||||
}
|
||||
|
||||
describe('getRectDimensionsForLevel', () => {
|
||||
it('should render a single item', () => {
|
||||
const level: ItemWithStart[] = [{ level: 1, start: 0, value: 100, label: '1', self: 0 }];
|
||||
const result = getRectDimensionsForLevel(
|
||||
level,
|
||||
1,
|
||||
100,
|
||||
0,
|
||||
10,
|
||||
getDisplayProcessor({ field: { config: {} }, theme: createTheme() }),
|
||||
(val) => val.toString()
|
||||
);
|
||||
const level: LevelItem[] = [{ start: 0, itemIndex: 0 }];
|
||||
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([
|
||||
{
|
||||
width: 999,
|
||||
@ -30,20 +32,15 @@ describe('getRectDimensionsForLevel', () => {
|
||||
});
|
||||
|
||||
it('should render a multiple items', () => {
|
||||
const level: ItemWithStart[] = [
|
||||
{ level: 2, start: 0, value: 100, label: '1', self: 0 },
|
||||
{ level: 2, start: 100, value: 50, label: '2', self: 0 },
|
||||
{ level: 2, start: 150, value: 50, label: '3', self: 0 },
|
||||
const level: LevelItem[] = [
|
||||
{ start: 0, itemIndex: 0 },
|
||||
{ start: 100, itemIndex: 1 },
|
||||
{ start: 150, itemIndex: 2 },
|
||||
];
|
||||
const result = getRectDimensionsForLevel(
|
||||
level,
|
||||
2,
|
||||
100,
|
||||
0,
|
||||
10,
|
||||
getDisplayProcessor({ field: { config: {} }, theme: createTheme() }),
|
||||
(val) => val.toString()
|
||||
const container = new FlameGraphDataContainer(
|
||||
makeDataFrame({ value: [100, 50, 50], level: [2, 2, 2], label: ['1', '2', '3'], self: [0, 0, 0] })
|
||||
);
|
||||
const result = getRectDimensionsForLevel(container, level, 2, 100, 0, 10);
|
||||
expect(result).toEqual([
|
||||
{ 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' },
|
||||
@ -52,20 +49,15 @@ describe('getRectDimensionsForLevel', () => {
|
||||
});
|
||||
|
||||
it('should render a collapsed items', () => {
|
||||
const level: ItemWithStart[] = [
|
||||
{ level: 2, start: 0, value: 100, label: '1', self: 0 },
|
||||
{ level: 2, start: 100, value: 2, label: '2', self: 0 },
|
||||
{ level: 2, start: 102, value: 1, label: '3', self: 0 },
|
||||
const level: LevelItem[] = [
|
||||
{ start: 0, itemIndex: 0 },
|
||||
{ start: 100, itemIndex: 1 },
|
||||
{ start: 102, itemIndex: 2 },
|
||||
];
|
||||
const result = getRectDimensionsForLevel(
|
||||
level,
|
||||
2,
|
||||
100,
|
||||
0,
|
||||
1,
|
||||
getDisplayProcessor({ field: { config: {} }, theme: createTheme() }),
|
||||
(val) => val.toString()
|
||||
const container = new FlameGraphDataContainer(
|
||||
makeDataFrame({ value: [100, 2, 1], level: [2, 2, 2], label: ['1', '2', '3'], self: [0, 0, 0] })
|
||||
);
|
||||
const result = getRectDimensionsForLevel(container, level, 2, 100, 0, 1);
|
||||
expect(result).toEqual([
|
||||
{ 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' },
|
||||
|
@ -1,4 +1,3 @@
|
||||
import { DisplayProcessor } from '@grafana/data';
|
||||
import { colors } from '@grafana/ui';
|
||||
|
||||
import {
|
||||
@ -10,7 +9,7 @@ import {
|
||||
PIXELS_PER_LEVEL,
|
||||
} from '../../constants';
|
||||
|
||||
import { ItemWithStart } from './dataTransform';
|
||||
import { FlameGraphDataContainer, LevelItem } from './dataTransform';
|
||||
|
||||
type RectData = {
|
||||
width: number;
|
||||
@ -28,19 +27,18 @@ type RectData = {
|
||||
* into bigger rects.
|
||||
*/
|
||||
export function getRectDimensionsForLevel(
|
||||
level: ItemWithStart[],
|
||||
data: FlameGraphDataContainer,
|
||||
level: LevelItem[],
|
||||
levelIndex: number,
|
||||
totalTicks: number,
|
||||
rangeMin: number,
|
||||
pixelsPerTick: number,
|
||||
processor: DisplayProcessor,
|
||||
getLabelValue: (value: number | string) => string
|
||||
pixelsPerTick: number
|
||||
): RectData[] {
|
||||
const coordinatesLevel = [];
|
||||
for (let barIndex = 0; barIndex < level.length; barIndex += 1) {
|
||||
const item = level[barIndex];
|
||||
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
|
||||
const collapsed = curBarTicks * pixelsPerTick <= COLLAPSE_THRESHOLD;
|
||||
@ -48,14 +46,14 @@ export function getRectDimensionsForLevel(
|
||||
while (
|
||||
barIndex < level.length - 1 &&
|
||||
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;
|
||||
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;
|
||||
|
||||
const width = curBarTicks * pixelsPerTick - (collapsed ? 0 : BAR_BORDER_WIDTH * 2);
|
||||
@ -66,7 +64,7 @@ export function getRectDimensionsForLevel(
|
||||
y: levelIndex * PIXELS_PER_LEVEL,
|
||||
collapsed,
|
||||
ticks: curBarTicks,
|
||||
label: getLabelValue(item.label),
|
||||
label: data.getLabel(item.itemIndex),
|
||||
unitLabel: unit,
|
||||
});
|
||||
}
|
||||
|
@ -1,14 +1,14 @@
|
||||
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 { DataFrame, DataFrameView, CoreApp, getEnumDisplayProcessor } from '@grafana/data';
|
||||
import { DataFrame, CoreApp } from '@grafana/data';
|
||||
import { useStyles2, useTheme2 } from '@grafana/ui';
|
||||
|
||||
import { MIN_WIDTH_TO_SHOW_BOTH_TOPTABLE_AND_FLAMEGRAPH, PIXELS_PER_LEVEL } from '../constants';
|
||||
|
||||
import FlameGraph from './FlameGraph/FlameGraph';
|
||||
import { Item, nestedSetToLevels } from './FlameGraph/dataTransform';
|
||||
import { FlameGraphDataContainer, LevelItem, nestedSetToLevels } from './FlameGraph/dataTransform';
|
||||
import FlameGraphHeader from './FlameGraphHeader';
|
||||
import FlameGraphTopTableContainer from './TopTable/FlameGraphTopTableContainer';
|
||||
import { SelectedView } from './types';
|
||||
@ -30,38 +30,21 @@ const FlameGraphContainer = (props: Props) => {
|
||||
const [search, setSearch] = useState('');
|
||||
const [selectedView, setSelectedView] = useState(SelectedView.Both);
|
||||
const [sizeRef, { width: containerWidth }] = useMeasure<HTMLDivElement>();
|
||||
|
||||
const labelField = props.data?.fields.find((f) => f.name === 'label');
|
||||
|
||||
const theme = useTheme2();
|
||||
|
||||
// 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.
|
||||
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(() => {
|
||||
const [dataContainer, levels] = useMemo((): [FlameGraphDataContainer, LevelItem[][]] | [undefined, undefined] => {
|
||||
if (!props.data) {
|
||||
return [];
|
||||
return [undefined, undefined];
|
||||
}
|
||||
const dataView = new DataFrameView<Item>(props.data);
|
||||
return nestedSetToLevels(dataView);
|
||||
}, [props.data]);
|
||||
const container = new FlameGraphDataContainer(props.data, theme);
|
||||
|
||||
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
|
||||
useEffect(() => {
|
||||
@ -83,7 +66,7 @@ const FlameGraphContainer = (props: Props) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
{props.data && (
|
||||
{dataContainer && (
|
||||
<div ref={sizeRef} className={styles.container}>
|
||||
<FlameGraphHeader
|
||||
app={props.app}
|
||||
@ -100,7 +83,7 @@ const FlameGraphContainer = (props: Props) => {
|
||||
|
||||
{selectedView !== SelectedView.FlameGraph && (
|
||||
<FlameGraphTopTableContainer
|
||||
data={props.data}
|
||||
data={dataContainer}
|
||||
app={props.app}
|
||||
totalLevels={levels.length}
|
||||
selectedView={selectedView}
|
||||
@ -110,13 +93,12 @@ const FlameGraphContainer = (props: Props) => {
|
||||
setSelectedBarIndex={setSelectedBarIndex}
|
||||
setRangeMin={setRangeMin}
|
||||
setRangeMax={setRangeMax}
|
||||
getLabelValue={getLabelValue}
|
||||
/>
|
||||
)}
|
||||
|
||||
{selectedView !== SelectedView.TopTable && (
|
||||
<FlameGraph
|
||||
data={props.data}
|
||||
data={dataContainer}
|
||||
app={props.app}
|
||||
flameGraphHeight={props.flameGraphHeight}
|
||||
levels={levels}
|
||||
@ -130,7 +112,6 @@ const FlameGraphContainer = (props: Props) => {
|
||||
setRangeMin={setRangeMin}
|
||||
setRangeMax={setRangeMax}
|
||||
selectedView={selectedView}
|
||||
getLabelValue={getLabelValue}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
@ -1,9 +1,9 @@
|
||||
import { render, screen } from '@testing-library/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 { SelectedView } from '../types';
|
||||
|
||||
@ -15,12 +15,12 @@ describe('FlameGraphTopTableContainer', () => {
|
||||
const [selectedView, _] = useState(SelectedView.Both);
|
||||
|
||||
const flameGraphData = new MutableDataFrame(data);
|
||||
const dataView = new DataFrameView<Item>(flameGraphData);
|
||||
const levels = nestedSetToLevels(dataView);
|
||||
const container = new FlameGraphDataContainer(flameGraphData);
|
||||
const levels = nestedSetToLevels(container);
|
||||
|
||||
return (
|
||||
<FlameGraphTopTableContainer
|
||||
data={flameGraphData}
|
||||
data={container}
|
||||
app={CoreApp.Explore}
|
||||
totalLevels={levels.length}
|
||||
selectedView={selectedView}
|
||||
@ -30,7 +30,6 @@ describe('FlameGraphTopTableContainer', () => {
|
||||
setSelectedBarIndex={jest.fn()}
|
||||
setRangeMin={jest.fn()}
|
||||
setRangeMax={jest.fn()}
|
||||
getLabelValue={(val) => val.toString()}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
@ -1,17 +1,18 @@
|
||||
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 { CoreApp, DataFrame, Field, FieldType, getDisplayProcessor } from '@grafana/data';
|
||||
import { useStyles2, useTheme2 } from '@grafana/ui';
|
||||
import { CoreApp, DisplayValue } from '@grafana/data';
|
||||
import { useStyles2 } from '@grafana/ui';
|
||||
|
||||
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';
|
||||
|
||||
type Props = {
|
||||
data: DataFrame;
|
||||
data: FlameGraphDataContainer;
|
||||
app: CoreApp;
|
||||
totalLevels: number;
|
||||
selectedView: SelectedView;
|
||||
@ -21,7 +22,6 @@ type Props = {
|
||||
setSelectedBarIndex: (bar: number) => void;
|
||||
setRangeMin: (range: number) => void;
|
||||
setRangeMax: (range: number) => void;
|
||||
getLabelValue: (label: string | number) => string;
|
||||
};
|
||||
|
||||
const FlameGraphTopTableContainer = ({
|
||||
@ -35,70 +35,26 @@ const FlameGraphTopTableContainer = ({
|
||||
setSelectedBarIndex,
|
||||
setRangeMin,
|
||||
setRangeMax,
|
||||
getLabelValue,
|
||||
}: Props) => {
|
||||
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 labelsField = data.fields.find((f) => f.name === 'label');
|
||||
|
||||
const sortLevelsIntoTable = useCallback(() => {
|
||||
let label, self, value;
|
||||
const topTable = useMemo(() => {
|
||||
// Group the data by label
|
||||
// TODO: should be by filename + funcName + linenumber?
|
||||
let table: { [key: string]: TableData } = {};
|
||||
|
||||
if (valueField && selfField && labelsField) {
|
||||
const valueValues = valueField.values;
|
||||
const selfValues = selfField.values;
|
||||
const labelValues = labelsField.values;
|
||||
|
||||
for (let i = 0; i < valueValues.length; i++) {
|
||||
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;
|
||||
}
|
||||
for (let i = 0; i < data.data.length; i++) {
|
||||
const value = data.getValue(i);
|
||||
const self = data.getSelf(i);
|
||||
const label = data.getLabel(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[] = [];
|
||||
for (let key in table) {
|
||||
const selfUnit = getTopTableData(selfField!, table[key].self);
|
||||
const valueUnit = getTopTableData(valueField!, table[key].total);
|
||||
const selfUnit = handleUnits(data.valueDisplayProcessor(table[key].self), data.getUnitTitle());
|
||||
const valueUnit = handleUnits(data.valueDisplayProcessor(table[key].total), data.getUnitTitle());
|
||||
|
||||
topTable.push({
|
||||
symbol: key,
|
||||
@ -107,8 +63,8 @@ const FlameGraphTopTableContainer = ({
|
||||
});
|
||||
}
|
||||
|
||||
setTopTable(topTable);
|
||||
}, [data.fields, selfField, sortLevelsIntoTable, valueField, getTopTableData]);
|
||||
return topTable;
|
||||
}, [data]);
|
||||
|
||||
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 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 = {
|
||||
e: MouseEvent;
|
||||
levelIndex: number;
|
||||
|
Loading…
Reference in New Issue
Block a user