Flame graph: Add metadata above flame graph (#61921)

* Remove percentTitle

* Flame graph metadata

* Remove comment

* Update test

* Update metadata
This commit is contained in:
Joey Tawadrous 2023-01-30 14:02:26 +00:00 committed by GitHub
parent f19b07c0bc
commit 780f43a33d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 204 additions and 22 deletions

View File

@ -21,6 +21,7 @@ jest.mock('react-use', () => ({
describe('FlameGraph', () => {
const FlameGraphWithProps = () => {
const [topLevelIndex, setTopLevelIndex] = useState(0);
const [selectedBarIndex, setSelectedBarIndex] = useState(0);
const [rangeMin, setRangeMin] = useState(0);
const [rangeMax, setRangeMax] = useState(1);
const [search] = useState('');
@ -36,10 +37,12 @@ describe('FlameGraph', () => {
app={CoreApp.Explore}
levels={levels}
topLevelIndex={topLevelIndex}
selectedBarIndex={selectedBarIndex}
rangeMin={rangeMin}
rangeMax={rangeMax}
search={search}
setTopLevelIndex={setTopLevelIndex}
setSelectedBarIndex={setSelectedBarIndex}
setRangeMin={setRangeMin}
setRangeMax={setRangeMax}
selectedView={selectedView}
@ -59,4 +62,9 @@ describe('FlameGraph', () => {
const calls = ctx!.__getDrawCalls();
expect(calls).toMatchSnapshot();
});
it('should render metadata', async () => {
render(<FlameGraphWithProps />);
expect(screen.getByText('16.5 Bil (100%) of 16,460,000,000 total samples (Count)')).toBeDefined();
});
});

View File

@ -26,6 +26,7 @@ import { CoreApp, createTheme, DataFrame, FieldType, getDisplayProcessor } from
import { PIXELS_PER_LEVEL } from '../../constants';
import { TooltipData, SelectedView } from '../types';
import FlameGraphMetadata from './FlameGraphMetadata';
import FlameGraphTooltip, { getTooltipData } from './FlameGraphTooltip';
import { ItemWithStart } from './dataTransform';
import { getBarX, getRectDimensionsForLevel, renderRect } from './rendering';
@ -36,10 +37,12 @@ type Props = {
flameGraphHeight?: number;
levels: ItemWithStart[][];
topLevelIndex: number;
selectedBarIndex: number;
rangeMin: number;
rangeMax: number;
search: string;
setTopLevelIndex: (level: number) => void;
setSelectedBarIndex: (bar: number) => void;
setRangeMin: (range: number) => void;
setRangeMax: (range: number) => void;
selectedView: SelectedView;
@ -52,10 +55,12 @@ const FlameGraph = ({
flameGraphHeight,
levels,
topLevelIndex,
selectedBarIndex,
rangeMin,
rangeMax,
search,
setTopLevelIndex,
setSelectedBarIndex,
setRangeMin,
setRangeMax,
selectedView,
@ -140,6 +145,7 @@ const FlameGraph = ({
if (barIndex !== -1 && !isNaN(levelIndex) && !isNaN(barIndex)) {
setTopLevelIndex(levelIndex);
setSelectedBarIndex(barIndex);
setRangeMin(levels[levelIndex][barIndex].start / totalTicks);
setRangeMax((levels[levelIndex][barIndex].start + levels[levelIndex][barIndex].value) / totalTicks);
}
@ -181,10 +187,18 @@ const FlameGraph = ({
setRangeMax,
selectedView,
valueField,
setSelectedBarIndex,
]);
return (
<div className={styles.graph} ref={sizeRef}>
<FlameGraphMetadata
levels={levels}
topLevelIndex={topLevelIndex}
selectedBarIndex={selectedBarIndex}
valueField={valueField}
totalTicks={totalTicks}
/>
<canvas ref={graphRef} data-testid="flameGraph" />
<FlameGraphTooltip tooltipRef={tooltipRef} tooltipData={tooltipData!} showTooltip={showTooltip} />
</div>
@ -198,8 +212,8 @@ const getStyles = (selectedView: SelectedView, app: CoreApp, flameGraphHeight: n
overflow: scroll;
width: ${selectedView === SelectedView.FlameGraph ? '100%' : '50%'};
${app !== CoreApp.Explore
? `height: calc(${flameGraphHeight}px - 44px)`
: ''}; // 44px to adjust for header pushing content down
? `height: calc(${flameGraphHeight}px - 50px)`
: ''}; // 50px to adjust for header pushing content down
`,
});

View File

@ -0,0 +1,75 @@
import { ArrayVector, Field, FieldType } from '@grafana/data';
import { getMetadata } from './FlameGraphMetadata';
describe('should get metadata correctly', () => {
it('for bytes', () => {
const metadata = getMetadata(makeField('bytes'), 1_624_078_250, 8_624_078_250);
expect(metadata).toEqual({
percentValue: 18.83,
unitTitle: 'RAM',
unitValue: '1.51 GiB',
samples: '8,624,078,250',
});
});
it('with default unit', () => {
const metadata = getMetadata(makeField('none'), 1_624_078_250, 8_624_078_250);
expect(metadata).toEqual({
percentValue: 18.83,
unitTitle: 'Count',
unitValue: '1624078250',
samples: '8,624,078,250',
});
});
it('without unit', () => {
const metadata = getMetadata(
{
name: 'test',
type: FieldType.number,
values: new ArrayVector(),
config: {},
},
1_624_078_250,
8_624_078_250
);
expect(metadata).toEqual({
percentValue: 18.83,
unitTitle: 'Count',
unitValue: '1624078250',
samples: '8,624,078,250',
});
});
it('for objects', () => {
const metadata = getMetadata(makeField('short'), 1_624_078_250, 8_624_078_250);
expect(metadata).toEqual({
percentValue: 18.83,
unitTitle: 'Count',
unitValue: '1.62 Bil',
samples: '8,624,078,250',
});
});
it('for nanoseconds', () => {
const metadata = getMetadata(makeField('ns'), 1_624_078_250, 8_624_078_250);
expect(metadata).toEqual({
percentValue: 18.83,
unitTitle: 'Time',
unitValue: '1.62 s',
samples: '8,624,078,250',
});
});
});
function makeField(unit: string): Field {
return {
name: 'test',
type: FieldType.number,
config: {
unit,
},
values: new ArrayVector(),
};
}

View File

@ -0,0 +1,70 @@
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 { ItemWithStart } from './dataTransform';
type Props = {
levels: ItemWithStart[][];
topLevelIndex: number;
selectedBarIndex: number;
valueField: Field<number, Vector<number>>;
totalTicks: number;
};
const FlameGraphMetadata = React.memo(({ levels, topLevelIndex, selectedBarIndex, valueField, 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 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;
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;
}
return {
percentValue,
unitTitle,
unitValue,
samples: totalTicks.toLocaleString(),
};
};
FlameGraphMetadata.displayName = 'FlameGraphMetadata';
const getStyles = () => ({
metadata: css`
margin: 8px 0;
text-align: center;
`,
});
export default FlameGraphMetadata;

View File

@ -8,7 +8,6 @@ describe('should get tooltip data correctly', () => {
expect(tooltipData).toEqual({
name: 'total',
percentSelf: 0.01,
percentTitle: '% of total',
percentValue: 100,
unitTitle: 'RAM',
unitSelf: '955 KiB',
@ -17,12 +16,11 @@ describe('should get tooltip data correctly', () => {
});
});
it('with none unit', () => {
it('with default unit', () => {
const tooltipData = getTooltipData(makeField('none'), 'total', 8_624_078_250, 978_250, 8_624_078_250);
expect(tooltipData).toEqual({
name: 'total',
percentSelf: 0.01,
percentTitle: '% of total',
percentValue: 100,
unitSelf: '978250',
unitTitle: 'Count',
@ -47,7 +45,6 @@ describe('should get tooltip data correctly', () => {
expect(tooltipData).toEqual({
name: 'total',
percentSelf: 0.01,
percentTitle: '% of total',
percentValue: 100,
unitTitle: 'Count',
unitSelf: '978250',
@ -61,7 +58,6 @@ describe('should get tooltip data correctly', () => {
expect(tooltipData).toEqual({
name: 'total',
percentSelf: 0.01,
percentTitle: '% of total',
percentValue: 100,
unitTitle: 'Count',
unitSelf: '978 K',
@ -75,7 +71,6 @@ describe('should get tooltip data correctly', () => {
expect(tooltipData).toEqual({
name: 'total',
percentSelf: 0.01,
percentTitle: '% of total time',
percentValue: 100,
unitTitle: 'Time',
unitSelf: '978 µs',

View File

@ -50,7 +50,6 @@ export const getTooltipData = (
self: number,
totalTicks: number
): TooltipData => {
let percentTitle;
let unitTitle;
const processor = getDisplayProcessor({ field, theme: createTheme() /* theme does not matter for us here */ });
@ -64,15 +63,12 @@ export const getTooltipData = (
switch (field.config.unit) {
case SampleUnit.Bytes:
percentTitle = '% of total';
unitTitle = 'RAM';
break;
case SampleUnit.Nanoseconds:
percentTitle = '% of total time';
unitTitle = 'Time';
break;
default:
percentTitle = '% of total';
unitTitle = 'Count';
if (!displayValue.suffix) {
// Makes sure we don't show 123undefined or something like that if suffix isn't defined
@ -87,7 +83,6 @@ export const getTooltipData = (
return {
name: label,
percentTitle,
percentValue,
percentSelf,
unitTitle,

View File

@ -24,6 +24,7 @@ type Props = {
const FlameGraphContainer = (props: Props) => {
const [topLevelIndex, setTopLevelIndex] = useState(0);
const [selectedBarIndex, setSelectedBarIndex] = useState(0);
const [rangeMin, setRangeMin] = useState(0);
const [rangeMax, setRangeMax] = useState(1);
const [search, setSearch] = useState('');
@ -56,6 +57,7 @@ const FlameGraphContainer = (props: Props) => {
useEffect(() => {
setTopLevelIndex(0);
setSelectedBarIndex(0);
setRangeMin(0);
setRangeMax(1);
}, [props.data]);
@ -65,6 +67,7 @@ const FlameGraphContainer = (props: Props) => {
<FlameGraphHeader
app={props.app}
setTopLevelIndex={setTopLevelIndex}
setSelectedBarIndex={setSelectedBarIndex}
setRangeMin={setRangeMin}
setRangeMax={setRangeMax}
search={search}
@ -83,6 +86,7 @@ const FlameGraphContainer = (props: Props) => {
search={search}
setSearch={setSearch}
setTopLevelIndex={setTopLevelIndex}
setSelectedBarIndex={setSelectedBarIndex}
setRangeMin={setRangeMin}
setRangeMax={setRangeMax}
/>
@ -95,10 +99,12 @@ const FlameGraphContainer = (props: Props) => {
flameGraphHeight={props.flameGraphHeight}
levels={levels}
topLevelIndex={topLevelIndex}
selectedBarIndex={selectedBarIndex}
rangeMin={rangeMin}
rangeMax={rangeMax}
search={search}
setTopLevelIndex={setTopLevelIndex}
setSelectedBarIndex={setSelectedBarIndex}
setRangeMin={setRangeMin}
setRangeMax={setRangeMax}
selectedView={selectedView}

View File

@ -19,6 +19,7 @@ describe('FlameGraphHeader', () => {
search={search}
setSearch={setSearch}
setTopLevelIndex={jest.fn()}
setSelectedBarIndex={jest.fn()}
setRangeMin={jest.fn()}
setRangeMax={jest.fn()}
selectedView={selectedView}

View File

@ -12,6 +12,7 @@ type Props = {
app: CoreApp;
search: string;
setTopLevelIndex: (level: number) => void;
setSelectedBarIndex: (bar: number) => void;
setRangeMin: (range: number) => void;
setRangeMax: (range: number) => void;
setSearch: (search: string) => void;
@ -24,6 +25,7 @@ const FlameGraphHeader = ({
app,
search,
setTopLevelIndex,
setSelectedBarIndex,
setRangeMin,
setRangeMax,
setSearch,
@ -64,6 +66,7 @@ const FlameGraphHeader = ({
size={'md'}
onClick={() => {
setTopLevelIndex(0);
setSelectedBarIndex(0);
setRangeMin(0);
setRangeMax(1);
setSearch('');

View File

@ -16,6 +16,7 @@ type Props = {
search: string;
setSearch: (search: string) => void;
setTopLevelIndex: (level: number) => void;
setSelectedBarIndex: (bar: number) => void;
setRangeMin: (range: number) => void;
setRangeMax: (range: number) => void;
};
@ -27,6 +28,7 @@ const FlameGraphTopTable = ({
search,
setSearch,
setTopLevelIndex,
setSelectedBarIndex,
setRangeMin,
setRangeMax,
}: Props) => {
@ -84,18 +86,19 @@ const FlameGraphTopTable = ({
);
const rowClicked = useCallback(
(row: string) => {
if (search === row) {
(symbol: string) => {
if (search === symbol) {
setSearch('');
} else {
setSearch(row);
setSearch(symbol);
// Reset selected level in flamegraph when selecting row in top table
setTopLevelIndex(0);
setSelectedBarIndex(0);
setRangeMin(0);
setRangeMax(1);
}
},
[search, setRangeMax, setRangeMin, setSearch, setTopLevelIndex]
[search, setRangeMax, setRangeMin, setSearch, setTopLevelIndex, setSelectedBarIndex]
);
const { headerGroups, rows, prepareRow } = useTable(options, useSortBy, useAbsoluteLayout);
@ -105,15 +108,15 @@ const FlameGraphTopTable = ({
let row = rows[index];
prepareRow(row);
const rowValue = row.values[ColumnTypes.Symbol.toLowerCase()];
const classNames = cx(rowValue === search && styles.matchedRow, styles.row);
const symbol = row.values[ColumnTypes.Symbol.toLowerCase()];
const classNames = cx(symbol === search && styles.matchedRow, styles.row);
return (
<div
{...row.getRowProps({ style })}
className={classNames}
onClick={() => {
rowClicked(rowValue);
rowClicked(symbol);
}}
>
{row.cells.map((cell) => {

View File

@ -27,6 +27,7 @@ describe('FlameGraphTopTableContainer', () => {
search={search}
setSearch={setSearch}
setTopLevelIndex={jest.fn()}
setSelectedBarIndex={jest.fn()}
setRangeMin={jest.fn()}
setRangeMax={jest.fn()}
/>

View File

@ -18,6 +18,7 @@ type Props = {
search: string;
setSearch: (search: string) => void;
setTopLevelIndex: (level: number) => void;
setSelectedBarIndex: (bar: number) => void;
setRangeMin: (range: number) => void;
setRangeMax: (range: number) => void;
};
@ -30,6 +31,7 @@ const FlameGraphTopTableContainer = ({
search,
setSearch,
setTopLevelIndex,
setSelectedBarIndex,
setRangeMin,
setRangeMax,
}: Props) => {
@ -113,6 +115,7 @@ const FlameGraphTopTableContainer = ({
search={search}
setSearch={setSearch}
setTopLevelIndex={setTopLevelIndex}
setSelectedBarIndex={setSelectedBarIndex}
setRangeMin={setRangeMin}
setRangeMax={setRangeMax}
/>
@ -133,7 +136,9 @@ const getStyles = (selectedView: SelectedView, app: CoreApp) => {
float: left;
margin-right: ${marginRight};
width: ${selectedView === SelectedView.TopTable ? '100%' : `calc(50% - ${marginRight})`};
${app !== CoreApp.Explore ? 'height: calc(100% - 44px)' : ''}; // 44px to adjust for header pushing content down
${app !== CoreApp.Explore
? 'height: calc(100% - 50px)'
: 'height: calc(100% + 50px)'}; // 50px to adjust for header pushing content down
`,
};
};

View File

@ -1,6 +1,5 @@
export type TooltipData = {
name: string;
percentTitle: string;
percentValue: number;
percentSelf: number;
unitTitle: string;
@ -9,6 +8,13 @@ export type TooltipData = {
samples: string;
};
export type Metadata = {
percentValue: number;
unitTitle: string;
unitValue: string;
samples: string;
};
export enum SampleUnit {
Bytes = 'bytes',
Short = 'short',