Skip to content

Commit aa12c6c

Browse files
authored
Flamegraph: Add table filtering for Flamegraph panel (grafana#78962)
1 parent 1c53561 commit aa12c6c

8 files changed

+92
-53
lines changed

packages/grafana-flamegraph/src/FlameGraph/FlameGraph.test.tsx

-1
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,6 @@ describe('FlameGraph', () => {
3939
data={container}
4040
rangeMin={0}
4141
rangeMax={1}
42-
search={''}
4342
setRangeMin={setRangeMin}
4443
setRangeMax={setRangeMax}
4544
onItemFocused={onItemFocused}

packages/grafana-flamegraph/src/FlameGraph/FlameGraph.tsx

+3-3
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ type Props = {
3232
data: FlameGraphDataContainer;
3333
rangeMin: number;
3434
rangeMax: number;
35-
search: string;
35+
matchedLabels?: Set<string>;
3636
setRangeMin: (range: number) => void;
3737
setRangeMax: (range: number) => void;
3838
style?: React.CSSProperties;
@@ -52,7 +52,7 @@ const FlameGraph = ({
5252
data,
5353
rangeMin,
5454
rangeMax,
55-
search,
55+
matchedLabels,
5656
setRangeMin,
5757
setRangeMax,
5858
onItemFocused,
@@ -108,7 +108,7 @@ const FlameGraph = ({
108108
data,
109109
rangeMin,
110110
rangeMax,
111-
search,
111+
matchedLabels,
112112
setRangeMin,
113113
setRangeMax,
114114
onItemFocused,

packages/grafana-flamegraph/src/FlameGraph/FlameGraphCanvas.tsx

+3-3
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ type Props = {
1414
data: FlameGraphDataContainer;
1515
rangeMin: number;
1616
rangeMax: number;
17-
search: string;
17+
matchedLabels: Set<string> | undefined;
1818
setRangeMin: (range: number) => void;
1919
setRangeMax: (range: number) => void;
2020
style?: React.CSSProperties;
@@ -43,7 +43,7 @@ const FlameGraphCanvas = ({
4343
data,
4444
rangeMin,
4545
rangeMax,
46-
search,
46+
matchedLabels,
4747
setRangeMin,
4848
setRangeMax,
4949
onItemFocused,
@@ -80,7 +80,7 @@ const FlameGraphCanvas = ({
8080
depth,
8181
rangeMax,
8282
rangeMin,
83-
search,
83+
matchedLabels,
8484
textAlign,
8585
totalViewTicks,
8686
// We need this so that if we have a diff profile and are in sandwich view we still show the same diff colors.

packages/grafana-flamegraph/src/FlameGraph/rendering.ts

+8-34
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import uFuzzy from '@leeoniya/ufuzzy';
21
import { RefObject, useCallback, useEffect, useMemo, useState } from 'react';
32
import color from 'tinycolor2';
43

@@ -22,8 +21,6 @@ import { ClickedItemData, ColorScheme, ColorSchemeDiff, TextAlign } from '../typ
2221
import { getBarColorByDiff, getBarColorByPackage, getBarColorByValue } from './colors';
2322
import { CollapseConfig, CollapsedMap, FlameGraphDataContainer, LevelItem } from './dataTransform';
2423

25-
const ufuzzy = new uFuzzy();
26-
2724
type RenderOptions = {
2825
canvasRef: RefObject<HTMLCanvasElement>;
2926
data: FlameGraphDataContainer;
@@ -38,7 +35,7 @@ type RenderOptions = {
3835
rangeMin: number;
3936
rangeMax: number;
4037

41-
search: string;
38+
matchedLabels: Set<string> | undefined;
4239
textAlign: TextAlign;
4340

4441
// Total ticks that will be used for sizing
@@ -63,7 +60,7 @@ export function useFlameRender(options: RenderOptions) {
6360
wrapperWidth,
6461
rangeMin,
6562
rangeMax,
66-
search,
63+
matchedLabels,
6764
textAlign,
6865
totalViewTicks,
6966
totalColorTicks,
@@ -72,7 +69,6 @@ export function useFlameRender(options: RenderOptions) {
7269
focusedItemData,
7370
collapsedMap,
7471
} = options;
75-
const foundLabels = useFoundLabels(search, data);
7672
const ctx = useSetupCanvas(canvasRef, wrapperWidth, depth);
7773
const theme = useTheme2();
7874

@@ -92,7 +88,7 @@ export function useFlameRender(options: RenderOptions) {
9288
mutedColor,
9389
rangeMin,
9490
rangeMax,
95-
foundLabels,
91+
matchedLabels,
9692
focusedItemData ? focusedItemData.item.level : 0
9793
);
9894

@@ -338,28 +334,6 @@ export function walkTree(
338334
}
339335
}
340336

341-
/**
342-
* Based on the search string it does a fuzzy search over all the unique labels so we can highlight them later.
343-
*/
344-
function useFoundLabels(search: string | undefined, data: FlameGraphDataContainer): Set<string> | undefined {
345-
return useMemo(() => {
346-
if (search) {
347-
const foundLabels = new Set<string>();
348-
let idxs = ufuzzy.filter(data.getUniqueLabels(), search);
349-
350-
if (idxs) {
351-
for (let idx of idxs) {
352-
foundLabels.add(data.getUniqueLabels()[idx]);
353-
}
354-
}
355-
356-
return foundLabels;
357-
}
358-
// In this case undefined means there was no search so no attempt to highlighting anything should be made.
359-
return undefined;
360-
}, [search, data]);
361-
}
362-
363337
function useColorFunction(
364338
totalTicks: number,
365339
totalTicksRight: number | undefined,
@@ -368,13 +342,13 @@ function useColorFunction(
368342
mutedColor: string,
369343
rangeMin: number,
370344
rangeMax: number,
371-
foundNames: Set<string> | undefined,
345+
matchedLabels: Set<string> | undefined,
372346
topLevel: number
373347
) {
374348
return useCallback(
375349
function getColor(item: LevelItem, label: string, muted: boolean) {
376350
// If collapsed and no search we can quickly return the muted color
377-
if (muted && !foundNames) {
351+
if (muted && !matchedLabels) {
378352
// Collapsed are always grayed
379353
return mutedColor;
380354
}
@@ -387,15 +361,15 @@ function useColorFunction(
387361
? getBarColorByValue(item.value, totalTicks, rangeMin, rangeMax)
388362
: getBarColorByPackage(label, theme);
389363

390-
if (foundNames) {
364+
if (matchedLabels) {
391365
// Means we are searching, we use color for matches and gray the rest
392-
return foundNames.has(label) ? barColor.toHslString() : mutedColor;
366+
return matchedLabels.has(label) ? barColor.toHslString() : mutedColor;
393367
}
394368

395369
// Mute if we are above the focused symbol
396370
return item.level > topLevel - 1 ? barColor.toHslString() : barColor.lighten(15).toHslString();
397371
},
398-
[totalTicks, totalTicksRight, colorScheme, theme, rangeMin, rangeMax, foundNames, topLevel, mutedColor]
372+
[totalTicks, totalTicksRight, colorScheme, theme, rangeMin, rangeMax, matchedLabels, topLevel, mutedColor]
399373
);
400374
}
401375

packages/grafana-flamegraph/src/FlameGraphContainer.test.tsx

+29-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { render, screen } from '@testing-library/react';
1+
import { render, screen, waitFor } from '@testing-library/react';
22
import userEvent from '@testing-library/user-event';
33
import React from 'react';
44

@@ -40,9 +40,13 @@ describe('FlameGraphContainer', () => {
4040
render(<FlameGraphContainerWithProps />);
4141
await userEvent.click((await screen.findAllByTitle('Highlight symbol'))[0]);
4242
expect(screen.getByDisplayValue('net/http.HandlerFunc.ServeHTTP')).toBeInTheDocument();
43+
// Unclick the selection so that we can click something else and continue test checks
44+
await userEvent.click((await screen.findAllByTitle('Highlight symbol'))[0]);
45+
4346
await userEvent.click((await screen.findAllByTitle('Highlight symbol'))[1]);
4447
expect(screen.getByDisplayValue('total')).toBeInTheDocument();
45-
await userEvent.click((await screen.findAllByTitle('Highlight symbol'))[1]);
48+
// after it is highlighted it will be the only (first) item in the table so [1] -> [0]
49+
await userEvent.click((await screen.findAllByTitle('Highlight symbol'))[0]);
4650
expect(screen.queryByDisplayValue('total')).not.toBeInTheDocument();
4751
});
4852

@@ -87,4 +91,27 @@ describe('FlameGraphContainer', () => {
8791

8892
expect(screen.queryByTestId(/Both/)).toBeNull();
8993
});
94+
95+
it('should filter table items based on search input', async () => {
96+
// Render the FlameGraphContainer with necessary props
97+
render(<FlameGraphContainerWithProps />);
98+
99+
// Checking for presence of this function before filter
100+
const matchingText = 'net/http.HandlerFunc.ServeHTTP';
101+
const nonMatchingText = 'runtime.systemstack';
102+
103+
expect(screen.queryAllByText(matchingText).length).toBe(1);
104+
expect(screen.queryAllByText(nonMatchingText).length).toBe(1);
105+
106+
// Apply the filter
107+
const searchInput = await screen.getByPlaceholderText('Search...');
108+
await userEvent.type(searchInput, 'Handler serve');
109+
110+
// We have to wait for filter to take effect
111+
await waitFor(() => {
112+
expect(screen.queryAllByText(nonMatchingText).length).toBe(0);
113+
});
114+
// Check we didn't lose the one that should match
115+
expect(screen.queryAllByText(matchingText).length).toBe(1);
116+
});
90117
});

packages/grafana-flamegraph/src/FlameGraphContainer.tsx

+31-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { css } from '@emotion/css';
2+
import uFuzzy from '@leeoniya/ufuzzy';
23
import React, { useCallback, useEffect, useMemo, useState } from 'react';
34
import { useMeasure } from 'react-use';
45

@@ -12,6 +13,8 @@ import FlameGraphTopTableContainer from './TopTable/FlameGraphTopTableContainer'
1213
import { MIN_WIDTH_TO_SHOW_BOTH_TOPTABLE_AND_FLAMEGRAPH } from './constants';
1314
import { ClickedItemData, ColorScheme, ColorSchemeDiff, SelectedView, TextAlign } from './types';
1415

16+
const ufuzzy = new uFuzzy();
17+
1518
export type Props = {
1619
/**
1720
* DataFrame with the profile data. The dataFrame needs to have the following fields:
@@ -99,6 +102,7 @@ const FlameGraphContainer = ({
99102
}, [data, theme, disableCollapsing]);
100103
const [colorScheme, setColorScheme] = useColorScheme(dataContainer);
101104
const styles = getStyles(theme);
105+
const matchedLabels = useLabelSearch(search, dataContainer);
102106

103107
// If user resizes window with both as the selected view
104108
useEffect(() => {
@@ -149,7 +153,7 @@ const FlameGraphContainer = ({
149153
data={dataContainer}
150154
rangeMin={rangeMin}
151155
rangeMax={rangeMax}
152-
search={search}
156+
matchedLabels={matchedLabels}
153157
setRangeMin={setRangeMin}
154158
setRangeMax={setRangeMax}
155159
onItemFocused={(data) => setFocusedItemData(data)}
@@ -173,6 +177,7 @@ const FlameGraphContainer = ({
173177
data={dataContainer}
174178
onSymbolClick={onSymbolClick}
175179
search={search}
180+
matchedLabels={matchedLabels}
176181
sandwichItem={sandwichItem}
177182
onSandwich={setSandwichItem}
178183
onSearch={setSearch}
@@ -255,6 +260,31 @@ function useColorScheme(dataContainer: FlameGraphDataContainer | undefined) {
255260
return [colorScheme, setColorScheme] as const;
256261
}
257262

263+
/**
264+
* Based on the search string it does a fuzzy search over all the unique labels, so we can highlight them later.
265+
*/
266+
function useLabelSearch(
267+
search: string | undefined,
268+
data: FlameGraphDataContainer | undefined
269+
): Set<string> | undefined {
270+
return useMemo(() => {
271+
if (search && data) {
272+
const foundLabels = new Set<string>();
273+
let idxs = ufuzzy.filter(data.getUniqueLabels(), search);
274+
275+
if (idxs) {
276+
for (let idx of idxs) {
277+
foundLabels.add(data.getUniqueLabels()[idx]);
278+
}
279+
}
280+
281+
return foundLabels;
282+
}
283+
// In this case undefined means there was no search so no attempt to highlighting anything should be made.
284+
return undefined;
285+
}, [search, data]);
286+
}
287+
258288
function getStyles(theme: GrafanaTheme2) {
259289
return {
260290
container: css({

packages/grafana-flamegraph/src/FlameGraphHeader.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ const FlameGraphHeader = ({
7373
onChange={(v) => {
7474
setLocalSearch(v.currentTarget.value);
7575
}}
76-
placeholder={'Search..'}
76+
placeholder={'Search...'}
7777
suffix={suffix}
7878
/>
7979
</div>

packages/grafana-flamegraph/src/TopTable/FlameGraphTopTableContainer.tsx

+17-8
Original file line numberDiff line numberDiff line change
@@ -29,31 +29,40 @@ import { TableData } from '../types';
2929
type Props = {
3030
data: FlameGraphDataContainer;
3131
onSymbolClick: (symbol: string) => void;
32+
// This is used for highlighting the search button in case there is exact match.
3233
search?: string;
34+
// We use these to filter out rows in the table if users is doing text search.
35+
matchedLabels?: Set<string>;
3336
sandwichItem?: string;
3437
onSearch: (str: string) => void;
3538
onSandwich: (str?: string) => void;
3639
onTableSort?: (sort: string) => void;
3740
};
3841

3942
const FlameGraphTopTableContainer = React.memo(
40-
({ data, onSymbolClick, search, onSearch, sandwichItem, onSandwich, onTableSort }: Props) => {
43+
({ data, onSymbolClick, search, matchedLabels, onSearch, sandwichItem, onSandwich, onTableSort }: Props) => {
4144
const table = useMemo(() => {
4245
// Group the data by label, we show only one row per label and sum the values
4346
// TODO: should be by filename + funcName + linenumber?
44-
let table: { [key: string]: TableData } = {};
47+
let filteredTable: { [key: string]: TableData } = {};
4548
for (let i = 0; i < data.data.length; i++) {
4649
const value = data.getValue(i);
4750
const valueRight = data.getValueRight(i);
4851
const self = data.getSelf(i);
4952
const label = data.getLabel(i);
50-
table[label] = table[label] || {};
51-
table[label].self = table[label].self ? table[label].self + self : self;
52-
table[label].total = table[label].total ? table[label].total + value : value;
53-
table[label].totalRight = table[label].totalRight ? table[label].totalRight + valueRight : valueRight;
53+
54+
// If user is doing text search we filter out labels in the same way we highlight them in flamegraph.
55+
if (!matchedLabels || matchedLabels.has(label)) {
56+
filteredTable[label] = filteredTable[label] || {};
57+
filteredTable[label].self = filteredTable[label].self ? filteredTable[label].self + self : self;
58+
filteredTable[label].total = filteredTable[label].total ? filteredTable[label].total + value : value;
59+
filteredTable[label].totalRight = filteredTable[label].totalRight
60+
? filteredTable[label].totalRight + valueRight
61+
: valueRight;
62+
}
5463
}
55-
return table;
56-
}, [data]);
64+
return filteredTable;
65+
}, [data, matchedLabels]);
5766

5867
const styles = useStyles2(getStyles);
5968
const theme = useTheme2();

0 commit comments

Comments
 (0)