mirror of
https://github.com/grafana/grafana.git
synced 2024-11-29 20:24:18 -06:00
Flamegraph: Add table filtering for Flamegraph panel (#78962)
This commit is contained in:
parent
1c53561521
commit
aa12c6c772
@ -39,7 +39,6 @@ describe('FlameGraph', () => {
|
||||
data={container}
|
||||
rangeMin={0}
|
||||
rangeMax={1}
|
||||
search={''}
|
||||
setRangeMin={setRangeMin}
|
||||
setRangeMax={setRangeMax}
|
||||
onItemFocused={onItemFocused}
|
||||
|
@ -32,7 +32,7 @@ type Props = {
|
||||
data: FlameGraphDataContainer;
|
||||
rangeMin: number;
|
||||
rangeMax: number;
|
||||
search: string;
|
||||
matchedLabels?: Set<string>;
|
||||
setRangeMin: (range: number) => void;
|
||||
setRangeMax: (range: number) => void;
|
||||
style?: React.CSSProperties;
|
||||
@ -52,7 +52,7 @@ const FlameGraph = ({
|
||||
data,
|
||||
rangeMin,
|
||||
rangeMax,
|
||||
search,
|
||||
matchedLabels,
|
||||
setRangeMin,
|
||||
setRangeMax,
|
||||
onItemFocused,
|
||||
@ -108,7 +108,7 @@ const FlameGraph = ({
|
||||
data,
|
||||
rangeMin,
|
||||
rangeMax,
|
||||
search,
|
||||
matchedLabels,
|
||||
setRangeMin,
|
||||
setRangeMax,
|
||||
onItemFocused,
|
||||
|
@ -14,7 +14,7 @@ type Props = {
|
||||
data: FlameGraphDataContainer;
|
||||
rangeMin: number;
|
||||
rangeMax: number;
|
||||
search: string;
|
||||
matchedLabels: Set<string> | undefined;
|
||||
setRangeMin: (range: number) => void;
|
||||
setRangeMax: (range: number) => void;
|
||||
style?: React.CSSProperties;
|
||||
@ -43,7 +43,7 @@ const FlameGraphCanvas = ({
|
||||
data,
|
||||
rangeMin,
|
||||
rangeMax,
|
||||
search,
|
||||
matchedLabels,
|
||||
setRangeMin,
|
||||
setRangeMax,
|
||||
onItemFocused,
|
||||
@ -80,7 +80,7 @@ const FlameGraphCanvas = ({
|
||||
depth,
|
||||
rangeMax,
|
||||
rangeMin,
|
||||
search,
|
||||
matchedLabels,
|
||||
textAlign,
|
||||
totalViewTicks,
|
||||
// We need this so that if we have a diff profile and are in sandwich view we still show the same diff colors.
|
||||
|
@ -1,4 +1,3 @@
|
||||
import uFuzzy from '@leeoniya/ufuzzy';
|
||||
import { RefObject, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import color from 'tinycolor2';
|
||||
|
||||
@ -22,8 +21,6 @@ import { ClickedItemData, ColorScheme, ColorSchemeDiff, TextAlign } from '../typ
|
||||
import { getBarColorByDiff, getBarColorByPackage, getBarColorByValue } from './colors';
|
||||
import { CollapseConfig, CollapsedMap, FlameGraphDataContainer, LevelItem } from './dataTransform';
|
||||
|
||||
const ufuzzy = new uFuzzy();
|
||||
|
||||
type RenderOptions = {
|
||||
canvasRef: RefObject<HTMLCanvasElement>;
|
||||
data: FlameGraphDataContainer;
|
||||
@ -38,7 +35,7 @@ type RenderOptions = {
|
||||
rangeMin: number;
|
||||
rangeMax: number;
|
||||
|
||||
search: string;
|
||||
matchedLabels: Set<string> | undefined;
|
||||
textAlign: TextAlign;
|
||||
|
||||
// Total ticks that will be used for sizing
|
||||
@ -63,7 +60,7 @@ export function useFlameRender(options: RenderOptions) {
|
||||
wrapperWidth,
|
||||
rangeMin,
|
||||
rangeMax,
|
||||
search,
|
||||
matchedLabels,
|
||||
textAlign,
|
||||
totalViewTicks,
|
||||
totalColorTicks,
|
||||
@ -72,7 +69,6 @@ export function useFlameRender(options: RenderOptions) {
|
||||
focusedItemData,
|
||||
collapsedMap,
|
||||
} = options;
|
||||
const foundLabels = useFoundLabels(search, data);
|
||||
const ctx = useSetupCanvas(canvasRef, wrapperWidth, depth);
|
||||
const theme = useTheme2();
|
||||
|
||||
@ -92,7 +88,7 @@ export function useFlameRender(options: RenderOptions) {
|
||||
mutedColor,
|
||||
rangeMin,
|
||||
rangeMax,
|
||||
foundLabels,
|
||||
matchedLabels,
|
||||
focusedItemData ? focusedItemData.item.level : 0
|
||||
);
|
||||
|
||||
@ -338,28 +334,6 @@ export function walkTree(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Based on the search string it does a fuzzy search over all the unique labels so we can highlight them later.
|
||||
*/
|
||||
function useFoundLabels(search: string | undefined, data: FlameGraphDataContainer): Set<string> | undefined {
|
||||
return useMemo(() => {
|
||||
if (search) {
|
||||
const foundLabels = new Set<string>();
|
||||
let idxs = ufuzzy.filter(data.getUniqueLabels(), search);
|
||||
|
||||
if (idxs) {
|
||||
for (let idx of idxs) {
|
||||
foundLabels.add(data.getUniqueLabels()[idx]);
|
||||
}
|
||||
}
|
||||
|
||||
return foundLabels;
|
||||
}
|
||||
// In this case undefined means there was no search so no attempt to highlighting anything should be made.
|
||||
return undefined;
|
||||
}, [search, data]);
|
||||
}
|
||||
|
||||
function useColorFunction(
|
||||
totalTicks: number,
|
||||
totalTicksRight: number | undefined,
|
||||
@ -368,13 +342,13 @@ function useColorFunction(
|
||||
mutedColor: string,
|
||||
rangeMin: number,
|
||||
rangeMax: number,
|
||||
foundNames: Set<string> | undefined,
|
||||
matchedLabels: Set<string> | undefined,
|
||||
topLevel: number
|
||||
) {
|
||||
return useCallback(
|
||||
function getColor(item: LevelItem, label: string, muted: boolean) {
|
||||
// If collapsed and no search we can quickly return the muted color
|
||||
if (muted && !foundNames) {
|
||||
if (muted && !matchedLabels) {
|
||||
// Collapsed are always grayed
|
||||
return mutedColor;
|
||||
}
|
||||
@ -387,15 +361,15 @@ function useColorFunction(
|
||||
? getBarColorByValue(item.value, totalTicks, rangeMin, rangeMax)
|
||||
: getBarColorByPackage(label, theme);
|
||||
|
||||
if (foundNames) {
|
||||
if (matchedLabels) {
|
||||
// Means we are searching, we use color for matches and gray the rest
|
||||
return foundNames.has(label) ? barColor.toHslString() : mutedColor;
|
||||
return matchedLabels.has(label) ? barColor.toHslString() : mutedColor;
|
||||
}
|
||||
|
||||
// Mute if we are above the focused symbol
|
||||
return item.level > topLevel - 1 ? barColor.toHslString() : barColor.lighten(15).toHslString();
|
||||
},
|
||||
[totalTicks, totalTicksRight, colorScheme, theme, rangeMin, rangeMax, foundNames, topLevel, mutedColor]
|
||||
[totalTicks, totalTicksRight, colorScheme, theme, rangeMin, rangeMax, matchedLabels, topLevel, mutedColor]
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import React from 'react';
|
||||
|
||||
@ -40,9 +40,13 @@ describe('FlameGraphContainer', () => {
|
||||
render(<FlameGraphContainerWithProps />);
|
||||
await userEvent.click((await screen.findAllByTitle('Highlight symbol'))[0]);
|
||||
expect(screen.getByDisplayValue('net/http.HandlerFunc.ServeHTTP')).toBeInTheDocument();
|
||||
// Unclick the selection so that we can click something else and continue test checks
|
||||
await userEvent.click((await screen.findAllByTitle('Highlight symbol'))[0]);
|
||||
|
||||
await userEvent.click((await screen.findAllByTitle('Highlight symbol'))[1]);
|
||||
expect(screen.getByDisplayValue('total')).toBeInTheDocument();
|
||||
await userEvent.click((await screen.findAllByTitle('Highlight symbol'))[1]);
|
||||
// after it is highlighted it will be the only (first) item in the table so [1] -> [0]
|
||||
await userEvent.click((await screen.findAllByTitle('Highlight symbol'))[0]);
|
||||
expect(screen.queryByDisplayValue('total')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
@ -87,4 +91,27 @@ describe('FlameGraphContainer', () => {
|
||||
|
||||
expect(screen.queryByTestId(/Both/)).toBeNull();
|
||||
});
|
||||
|
||||
it('should filter table items based on search input', async () => {
|
||||
// Render the FlameGraphContainer with necessary props
|
||||
render(<FlameGraphContainerWithProps />);
|
||||
|
||||
// Checking for presence of this function before filter
|
||||
const matchingText = 'net/http.HandlerFunc.ServeHTTP';
|
||||
const nonMatchingText = 'runtime.systemstack';
|
||||
|
||||
expect(screen.queryAllByText(matchingText).length).toBe(1);
|
||||
expect(screen.queryAllByText(nonMatchingText).length).toBe(1);
|
||||
|
||||
// Apply the filter
|
||||
const searchInput = await screen.getByPlaceholderText('Search...');
|
||||
await userEvent.type(searchInput, 'Handler serve');
|
||||
|
||||
// We have to wait for filter to take effect
|
||||
await waitFor(() => {
|
||||
expect(screen.queryAllByText(nonMatchingText).length).toBe(0);
|
||||
});
|
||||
// Check we didn't lose the one that should match
|
||||
expect(screen.queryAllByText(matchingText).length).toBe(1);
|
||||
});
|
||||
});
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { css } from '@emotion/css';
|
||||
import uFuzzy from '@leeoniya/ufuzzy';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useMeasure } from 'react-use';
|
||||
|
||||
@ -12,6 +13,8 @@ import FlameGraphTopTableContainer from './TopTable/FlameGraphTopTableContainer'
|
||||
import { MIN_WIDTH_TO_SHOW_BOTH_TOPTABLE_AND_FLAMEGRAPH } from './constants';
|
||||
import { ClickedItemData, ColorScheme, ColorSchemeDiff, SelectedView, TextAlign } from './types';
|
||||
|
||||
const ufuzzy = new uFuzzy();
|
||||
|
||||
export type Props = {
|
||||
/**
|
||||
* DataFrame with the profile data. The dataFrame needs to have the following fields:
|
||||
@ -99,6 +102,7 @@ const FlameGraphContainer = ({
|
||||
}, [data, theme, disableCollapsing]);
|
||||
const [colorScheme, setColorScheme] = useColorScheme(dataContainer);
|
||||
const styles = getStyles(theme);
|
||||
const matchedLabels = useLabelSearch(search, dataContainer);
|
||||
|
||||
// If user resizes window with both as the selected view
|
||||
useEffect(() => {
|
||||
@ -149,7 +153,7 @@ const FlameGraphContainer = ({
|
||||
data={dataContainer}
|
||||
rangeMin={rangeMin}
|
||||
rangeMax={rangeMax}
|
||||
search={search}
|
||||
matchedLabels={matchedLabels}
|
||||
setRangeMin={setRangeMin}
|
||||
setRangeMax={setRangeMax}
|
||||
onItemFocused={(data) => setFocusedItemData(data)}
|
||||
@ -173,6 +177,7 @@ const FlameGraphContainer = ({
|
||||
data={dataContainer}
|
||||
onSymbolClick={onSymbolClick}
|
||||
search={search}
|
||||
matchedLabels={matchedLabels}
|
||||
sandwichItem={sandwichItem}
|
||||
onSandwich={setSandwichItem}
|
||||
onSearch={setSearch}
|
||||
@ -255,6 +260,31 @@ function useColorScheme(dataContainer: FlameGraphDataContainer | undefined) {
|
||||
return [colorScheme, setColorScheme] as const;
|
||||
}
|
||||
|
||||
/**
|
||||
* Based on the search string it does a fuzzy search over all the unique labels, so we can highlight them later.
|
||||
*/
|
||||
function useLabelSearch(
|
||||
search: string | undefined,
|
||||
data: FlameGraphDataContainer | undefined
|
||||
): Set<string> | undefined {
|
||||
return useMemo(() => {
|
||||
if (search && data) {
|
||||
const foundLabels = new Set<string>();
|
||||
let idxs = ufuzzy.filter(data.getUniqueLabels(), search);
|
||||
|
||||
if (idxs) {
|
||||
for (let idx of idxs) {
|
||||
foundLabels.add(data.getUniqueLabels()[idx]);
|
||||
}
|
||||
}
|
||||
|
||||
return foundLabels;
|
||||
}
|
||||
// In this case undefined means there was no search so no attempt to highlighting anything should be made.
|
||||
return undefined;
|
||||
}, [search, data]);
|
||||
}
|
||||
|
||||
function getStyles(theme: GrafanaTheme2) {
|
||||
return {
|
||||
container: css({
|
||||
|
@ -73,7 +73,7 @@ const FlameGraphHeader = ({
|
||||
onChange={(v) => {
|
||||
setLocalSearch(v.currentTarget.value);
|
||||
}}
|
||||
placeholder={'Search..'}
|
||||
placeholder={'Search...'}
|
||||
suffix={suffix}
|
||||
/>
|
||||
</div>
|
||||
|
@ -29,7 +29,10 @@ import { TableData } from '../types';
|
||||
type Props = {
|
||||
data: FlameGraphDataContainer;
|
||||
onSymbolClick: (symbol: string) => void;
|
||||
// This is used for highlighting the search button in case there is exact match.
|
||||
search?: string;
|
||||
// We use these to filter out rows in the table if users is doing text search.
|
||||
matchedLabels?: Set<string>;
|
||||
sandwichItem?: string;
|
||||
onSearch: (str: string) => void;
|
||||
onSandwich: (str?: string) => void;
|
||||
@ -37,23 +40,29 @@ type Props = {
|
||||
};
|
||||
|
||||
const FlameGraphTopTableContainer = React.memo(
|
||||
({ data, onSymbolClick, search, onSearch, sandwichItem, onSandwich, onTableSort }: Props) => {
|
||||
({ data, onSymbolClick, search, matchedLabels, onSearch, sandwichItem, onSandwich, onTableSort }: Props) => {
|
||||
const table = useMemo(() => {
|
||||
// Group the data by label, we show only one row per label and sum the values
|
||||
// TODO: should be by filename + funcName + linenumber?
|
||||
let table: { [key: string]: TableData } = {};
|
||||
let filteredTable: { [key: string]: TableData } = {};
|
||||
for (let i = 0; i < data.data.length; i++) {
|
||||
const value = data.getValue(i);
|
||||
const valueRight = data.getValueRight(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;
|
||||
table[label].totalRight = table[label].totalRight ? table[label].totalRight + valueRight : valueRight;
|
||||
|
||||
// If user is doing text search we filter out labels in the same way we highlight them in flamegraph.
|
||||
if (!matchedLabels || matchedLabels.has(label)) {
|
||||
filteredTable[label] = filteredTable[label] || {};
|
||||
filteredTable[label].self = filteredTable[label].self ? filteredTable[label].self + self : self;
|
||||
filteredTable[label].total = filteredTable[label].total ? filteredTable[label].total + value : value;
|
||||
filteredTable[label].totalRight = filteredTable[label].totalRight
|
||||
? filteredTable[label].totalRight + valueRight
|
||||
: valueRight;
|
||||
}
|
||||
}
|
||||
return table;
|
||||
}, [data]);
|
||||
return filteredTable;
|
||||
}, [data, matchedLabels]);
|
||||
|
||||
const styles = useStyles2(getStyles);
|
||||
const theme = useTheme2();
|
||||
|
Loading…
Reference in New Issue
Block a user