mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Flame graph: Add metadata above flame graph (#61921)
* Remove percentTitle * Flame graph metadata * Remove comment * Update test * Update metadata
This commit is contained in:
parent
f19b07c0bc
commit
780f43a33d
@ -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();
|
||||
});
|
||||
});
|
||||
|
@ -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
|
||||
`,
|
||||
});
|
||||
|
||||
|
@ -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(),
|
||||
};
|
||||
}
|
@ -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;
|
@ -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',
|
||||
|
@ -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,
|
||||
|
@ -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}
|
||||
|
@ -19,6 +19,7 @@ describe('FlameGraphHeader', () => {
|
||||
search={search}
|
||||
setSearch={setSearch}
|
||||
setTopLevelIndex={jest.fn()}
|
||||
setSelectedBarIndex={jest.fn()}
|
||||
setRangeMin={jest.fn()}
|
||||
setRangeMax={jest.fn()}
|
||||
selectedView={selectedView}
|
||||
|
@ -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('');
|
||||
|
@ -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) => {
|
||||
|
@ -27,6 +27,7 @@ describe('FlameGraphTopTableContainer', () => {
|
||||
search={search}
|
||||
setSearch={setSearch}
|
||||
setTopLevelIndex={jest.fn()}
|
||||
setSelectedBarIndex={jest.fn()}
|
||||
setRangeMin={jest.fn()}
|
||||
setRangeMax={jest.fn()}
|
||||
/>
|
||||
|
@ -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
|
||||
`,
|
||||
};
|
||||
};
|
||||
|
@ -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',
|
||||
|
Loading…
Reference in New Issue
Block a user