Phlare: Use enum config to send deduplicated func and filenames (#64435)

This commit is contained in:
Andrej Ocenas
2023-03-13 11:06:04 +01:00
committed by GitHub
parent 5275a203aa
commit 6647217208
11 changed files with 152 additions and 42 deletions

View File

@@ -47,6 +47,7 @@ describe('FlameGraph', () => {
setRangeMin={setRangeMin}
setRangeMax={setRangeMax}
selectedView={selectedView}
getLabelValue={(val) => val.toString()}
/>
);
};

View File

@@ -48,6 +48,7 @@ type Props = {
setRangeMax: (range: number) => void;
selectedView: SelectedView;
style?: React.CSSProperties;
getLabelValue: (label: string | number) => string;
};
const FlameGraph = ({
@@ -65,11 +66,13 @@ 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');
}
@@ -85,7 +88,13 @@ const FlameGraph = ({
});
const uniqueLabels = useMemo(() => {
return [...new Set<string>(data.fields.find((f) => f.name === 'label')?.values.toArray())];
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(() => {
@@ -131,14 +140,33 @@ const FlameGraph = ({
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);
const dimensions = getRectDimensionsForLevel(
level,
levelIndex,
totalTicks,
rangeMin,
pixelsPerTick,
processor,
getLabelValue
);
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]
[
levels,
wrapperWidth,
valueField,
totalTicks,
rangeMin,
rangeMax,
search,
topLevelIndex,
foundLabels,
getLabelValue,
]
);
useEffect(() => {
@@ -235,7 +263,7 @@ const FlameGraph = ({
<div className={styles.canvasContainer} id="flameGraphCanvasContainer">
<canvas ref={graphRef} data-testid="flameGraph" />
</div>
<FlameGraphTooltip tooltipRef={tooltipRef} tooltipData={tooltipData!} />
<FlameGraphTooltip tooltipRef={tooltipRef} tooltipData={tooltipData!} getLabelValue={getLabelValue} />
{contextMenuData && (
<FlameGraphContextMenu
contextMenuData={contextMenuData!}

View File

@@ -9,9 +9,10 @@ import { TooltipData, SampleUnit } from '../types';
type Props = {
tooltipRef: LegacyRef<HTMLDivElement>;
tooltipData: TooltipData;
getLabelValue: (label: string | number) => string;
};
const FlameGraphTooltip = ({ tooltipRef, tooltipData }: Props) => {
const FlameGraphTooltip = ({ tooltipRef, tooltipData, getLabelValue }: Props) => {
const styles = useStyles2(getStyles);
return (
@@ -20,7 +21,7 @@ const FlameGraphTooltip = ({ tooltipRef, tooltipData }: Props) => {
<Tooltip
content={
<div>
<p>{tooltipData.name}</p>
<p>{getLabelValue(tooltipData.name)}</p>
<p className={styles.lastParagraph}>
{tooltipData.unitTitle}
<br />

View File

@@ -12,7 +12,8 @@ describe('getRectDimensionsForLevel', () => {
100,
0,
10,
getDisplayProcessor({ field: { config: {} }, theme: createTheme() })
getDisplayProcessor({ field: { config: {} }, theme: createTheme() }),
(val) => val.toString()
);
expect(result).toEqual([
{
@@ -40,7 +41,8 @@ describe('getRectDimensionsForLevel', () => {
100,
0,
10,
getDisplayProcessor({ field: { config: {} }, theme: createTheme() })
getDisplayProcessor({ field: { config: {} }, theme: createTheme() }),
(val) => val.toString()
);
expect(result).toEqual([
{ width: 999, height: 22, x: 0, y: 44, collapsed: false, ticks: 100, label: '1', unitLabel: '100' },
@@ -61,7 +63,8 @@ describe('getRectDimensionsForLevel', () => {
100,
0,
1,
getDisplayProcessor({ field: { config: {} }, theme: createTheme() })
getDisplayProcessor({ field: { config: {} }, theme: createTheme() }),
(val) => val.toString()
);
expect(result).toEqual([
{ width: 99, height: 22, x: 0, y: 44, collapsed: false, ticks: 100, label: '1', unitLabel: '100' },

View File

@@ -26,11 +26,6 @@ type RectData = {
/**
* Compute the pixel coordinates for each bar in a level. We need full level of bars so that we can collapse small bars
* into bigger rects.
* @param level
* @param levelIndex
* @param totalTicks
* @param rangeMin
* @param pixelsPerTick
*/
export function getRectDimensionsForLevel(
level: ItemWithStart[],
@@ -38,7 +33,8 @@ export function getRectDimensionsForLevel(
totalTicks: number,
rangeMin: number,
pixelsPerTick: number,
processor: DisplayProcessor
processor: DisplayProcessor,
getLabelValue: (value: number | string) => string
): RectData[] {
const coordinatesLevel = [];
for (let barIndex = 0; barIndex < level.length; barIndex += 1) {
@@ -70,7 +66,7 @@ export function getRectDimensionsForLevel(
y: levelIndex * PIXELS_PER_LEVEL,
collapsed,
ticks: curBarTicks,
label: item.label,
label: getLabelValue(item.label),
unitLabel: unit,
});
}

View File

@@ -1,8 +1,8 @@
import { css } from '@emotion/css';
import React, { useEffect, useMemo, useState } from 'react';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useMeasure } from 'react-use';
import { DataFrame, DataFrameView, CoreApp } from '@grafana/data';
import { DataFrame, DataFrameView, CoreApp, getEnumDisplayProcessor, createTheme } from '@grafana/data';
import { useStyles2 } from '@grafana/ui';
import { MIN_WIDTH_TO_SHOW_BOTH_TOPTABLE_AND_FLAMEGRAPH, PIXELS_PER_LEVEL } from '../constants';
@@ -14,7 +14,7 @@ import FlameGraphTopTableContainer from './TopTable/FlameGraphTopTableContainer'
import { SelectedView } from './types';
type Props = {
data: DataFrame;
data?: DataFrame;
app: CoreApp;
// Height for flame graph when not used in explore.
// This needs to be different to explore flame graph height as we
@@ -31,6 +31,23 @@ const FlameGraphContainer = (props: Props) => {
const [selectedView, setSelectedView] = useState(SelectedView.Both);
const [sizeRef, { width: containerWidth }] = useMeasure<HTMLDivElement>();
const labelField = props.data?.fields.find((f) => f.name === 'label');
// 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(createTheme(), enumConfig)(label).text;
} else {
return label.toString();
}
},
[labelField]
);
// 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.
@@ -91,6 +108,7 @@ const FlameGraphContainer = (props: Props) => {
setSelectedBarIndex={setSelectedBarIndex}
setRangeMin={setRangeMin}
setRangeMax={setRangeMax}
getLabelValue={getLabelValue}
/>
)}
@@ -110,6 +128,7 @@ const FlameGraphContainer = (props: Props) => {
setRangeMin={setRangeMin}
setRangeMax={setRangeMax}
selectedView={selectedView}
getLabelValue={getLabelValue}
/>
)}
</div>

View File

@@ -30,6 +30,7 @@ describe('FlameGraphTopTableContainer', () => {
setSelectedBarIndex={jest.fn()}
setRangeMin={jest.fn()}
setRangeMax={jest.fn()}
getLabelValue={(val) => val.toString()}
/>
);
};

View File

@@ -21,6 +21,7 @@ type Props = {
setSelectedBarIndex: (bar: number) => void;
setRangeMin: (range: number) => void;
setRangeMax: (range: number) => void;
getLabelValue: (label: string | number) => string;
};
const FlameGraphTopTableContainer = ({
@@ -34,26 +35,29 @@ const FlameGraphTopTableContainer = ({
setSelectedBarIndex,
setRangeMin,
setRangeMax,
getLabelValue,
}: Props) => {
const styles = useStyles2(() => getStyles(selectedView, app));
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;
let table: { [key: string]: TableData } = {};
if (data.fields.length > 3) {
const valueValues = data.fields[1].values;
const selfValues = data.fields[2].values;
const labelValues = data.fields[3].values;
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 = labelValues.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;
@@ -61,7 +65,7 @@ const FlameGraphTopTableContainer = ({
}
return table;
}, [data.fields]);
}, [getLabelValue, selfField, valueField, labelsField]);
const getTopTableData = (field: Field, value: number) => {
const processor = getDisplayProcessor({ field, theme: createTheme() /* theme does not matter for us here */ });