diff --git a/public/app/plugins/panel/flamegraph/components/FlameGraph/FlameGraph.test.tsx b/public/app/plugins/panel/flamegraph/components/FlameGraph/FlameGraph.test.tsx index ff12c0510f9..609ddab52ae 100644 --- a/public/app/plugins/panel/flamegraph/components/FlameGraph/FlameGraph.test.tsx +++ b/public/app/plugins/panel/flamegraph/components/FlameGraph/FlameGraph.test.tsx @@ -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(); + expect(screen.getByText('16.5 Bil (100%) of 16,460,000,000 total samples (Count)')).toBeDefined(); + }); }); diff --git a/public/app/plugins/panel/flamegraph/components/FlameGraph/FlameGraph.tsx b/public/app/plugins/panel/flamegraph/components/FlameGraph/FlameGraph.tsx index c7b3f8e0981..bbf4dd37959 100644 --- a/public/app/plugins/panel/flamegraph/components/FlameGraph/FlameGraph.tsx +++ b/public/app/plugins/panel/flamegraph/components/FlameGraph/FlameGraph.tsx @@ -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 (
+
@@ -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 `, }); diff --git a/public/app/plugins/panel/flamegraph/components/FlameGraph/FlameGraphMetadata.test.ts b/public/app/plugins/panel/flamegraph/components/FlameGraph/FlameGraphMetadata.test.ts new file mode 100644 index 00000000000..9c38490df90 --- /dev/null +++ b/public/app/plugins/panel/flamegraph/components/FlameGraph/FlameGraphMetadata.test.ts @@ -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(), + }; +} diff --git a/public/app/plugins/panel/flamegraph/components/FlameGraph/FlameGraphMetadata.tsx b/public/app/plugins/panel/flamegraph/components/FlameGraph/FlameGraphMetadata.tsx new file mode 100644 index 00000000000..ca0b6dab9db --- /dev/null +++ b/public/app/plugins/panel/flamegraph/components/FlameGraph/FlameGraphMetadata.tsx @@ -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>; + 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 <>{
{metadataText}
}; + } + 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; diff --git a/public/app/plugins/panel/flamegraph/components/FlameGraph/FlameGraphTooltip.test.ts b/public/app/plugins/panel/flamegraph/components/FlameGraph/FlameGraphTooltip.test.ts index cc28a035673..e21266b6ba5 100644 --- a/public/app/plugins/panel/flamegraph/components/FlameGraph/FlameGraphTooltip.test.ts +++ b/public/app/plugins/panel/flamegraph/components/FlameGraph/FlameGraphTooltip.test.ts @@ -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', diff --git a/public/app/plugins/panel/flamegraph/components/FlameGraph/FlameGraphTooltip.tsx b/public/app/plugins/panel/flamegraph/components/FlameGraph/FlameGraphTooltip.tsx index af02ec7a7e9..15a782940df 100644 --- a/public/app/plugins/panel/flamegraph/components/FlameGraph/FlameGraphTooltip.tsx +++ b/public/app/plugins/panel/flamegraph/components/FlameGraph/FlameGraphTooltip.tsx @@ -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, diff --git a/public/app/plugins/panel/flamegraph/components/FlameGraphContainer.tsx b/public/app/plugins/panel/flamegraph/components/FlameGraphContainer.tsx index c7272440ed1..4dbc033929c 100644 --- a/public/app/plugins/panel/flamegraph/components/FlameGraphContainer.tsx +++ b/public/app/plugins/panel/flamegraph/components/FlameGraphContainer.tsx @@ -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) => { { 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} diff --git a/public/app/plugins/panel/flamegraph/components/FlameGraphHeader.test.tsx b/public/app/plugins/panel/flamegraph/components/FlameGraphHeader.test.tsx index b11786c7abe..78b6cefb309 100644 --- a/public/app/plugins/panel/flamegraph/components/FlameGraphHeader.test.tsx +++ b/public/app/plugins/panel/flamegraph/components/FlameGraphHeader.test.tsx @@ -19,6 +19,7 @@ describe('FlameGraphHeader', () => { search={search} setSearch={setSearch} setTopLevelIndex={jest.fn()} + setSelectedBarIndex={jest.fn()} setRangeMin={jest.fn()} setRangeMax={jest.fn()} selectedView={selectedView} diff --git a/public/app/plugins/panel/flamegraph/components/FlameGraphHeader.tsx b/public/app/plugins/panel/flamegraph/components/FlameGraphHeader.tsx index 00ee80cf5e6..c539e8f1ecf 100644 --- a/public/app/plugins/panel/flamegraph/components/FlameGraphHeader.tsx +++ b/public/app/plugins/panel/flamegraph/components/FlameGraphHeader.tsx @@ -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(''); diff --git a/public/app/plugins/panel/flamegraph/components/TopTable/FlameGraphTopTable.tsx b/public/app/plugins/panel/flamegraph/components/TopTable/FlameGraphTopTable.tsx index 8f75fa32eb9..431b1999e65 100644 --- a/public/app/plugins/panel/flamegraph/components/TopTable/FlameGraphTopTable.tsx +++ b/public/app/plugins/panel/flamegraph/components/TopTable/FlameGraphTopTable.tsx @@ -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 (
{ - rowClicked(rowValue); + rowClicked(symbol); }} > {row.cells.map((cell) => { diff --git a/public/app/plugins/panel/flamegraph/components/TopTable/FlameGraphTopTableContainer.test.tsx b/public/app/plugins/panel/flamegraph/components/TopTable/FlameGraphTopTableContainer.test.tsx index b70a93a0895..0bf006f262c 100644 --- a/public/app/plugins/panel/flamegraph/components/TopTable/FlameGraphTopTableContainer.test.tsx +++ b/public/app/plugins/panel/flamegraph/components/TopTable/FlameGraphTopTableContainer.test.tsx @@ -27,6 +27,7 @@ describe('FlameGraphTopTableContainer', () => { search={search} setSearch={setSearch} setTopLevelIndex={jest.fn()} + setSelectedBarIndex={jest.fn()} setRangeMin={jest.fn()} setRangeMax={jest.fn()} /> diff --git a/public/app/plugins/panel/flamegraph/components/TopTable/FlameGraphTopTableContainer.tsx b/public/app/plugins/panel/flamegraph/components/TopTable/FlameGraphTopTableContainer.tsx index ae89c57c7de..1d3934897ad 100644 --- a/public/app/plugins/panel/flamegraph/components/TopTable/FlameGraphTopTableContainer.tsx +++ b/public/app/plugins/panel/flamegraph/components/TopTable/FlameGraphTopTableContainer.tsx @@ -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 `, }; }; diff --git a/public/app/plugins/panel/flamegraph/components/types.ts b/public/app/plugins/panel/flamegraph/components/types.ts index 13dbf648742..be1a0900a83 100644 --- a/public/app/plugins/panel/flamegraph/components/types.ts +++ b/public/app/plugins/panel/flamegraph/components/types.ts @@ -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',