FlameGraph: Add prop to keep focused items when the profile data changes (#98356)

This commit is contained in:
Marc M.
2025-01-20 11:34:48 +01:00
committed by GitHub
parent b3b044b54b
commit 27cdd25917
4 changed files with 131 additions and 80 deletions

View File

@@ -27,7 +27,7 @@ import { Flamegraph } from '@grafana/flamegraph';
#### Props
| Name | Type | Description |
| ------------------- | ------------------------ | --------------------------------------------------------------------------------------------------------------------------- |
| --------------------- | ------------------------ | --------------------------------------------------------------------------------------------------------------------------- |
| data | DataFrame | DataFrame with the profile data. Optional, if missing or empty the flamegraph is not rendered |
| stickyHeader | boolean | Whether the header should be sticky and be always visible on the top when scrolling. |
| getTheme | () => GrafanaTheme2 | Provides a theme for the visualization on which colors and some sizes are based. |
@@ -37,6 +37,7 @@ import { Flamegraph } from '@grafana/flamegraph';
| onTableSort | (sort: string) => void | Interaction hook that can be used to report on the interaction. Fires when user changes the teble sorting. |
| extraHeaderElements | React.ReactNode | Elements that will be shown in the header on the right side of the header buttons. Useful for additional functionality. |
| vertical | boolean | If true the flamegraph will be rendered on top of the table. |
| keepFocusOnDataChange | boolean | If true any focused block will stay focused when the profile data changes. Same for the sandwich view. |
##### DataFrame schema

View File

@@ -136,7 +136,10 @@ const FlameGraph = ({
search,
selectedView,
};
const canvas = levelsCallers ? (
let canvas = null;
if (levelsCallers?.length) {
canvas = (
<>
<div className={styles.sandwichCanvasWrapper}>
<div className={styles.sandwichMarker}>
@@ -167,9 +170,12 @@ const FlameGraph = ({
/>
</div>
</>
) : (
);
} else if (levels?.length) {
canvas = (
<FlameGraphCanvas {...commonCanvasProps} root={levels[0][0]} depth={levels.length} direction={'children'} />
);
}
return (
<div className={styles.graph}>

View File

@@ -2,7 +2,7 @@ import { css } from '@emotion/css';
import { memo, ReactNode } from 'react';
import { getValueFormat, GrafanaTheme2 } from '@grafana/data';
import { Icon, IconButton, useStyles2 } from '@grafana/ui';
import { Icon, IconButton, Tooltip, useStyles2 } from '@grafana/ui';
import { ClickedItemData } from '../types';
@@ -42,7 +42,8 @@ const FlameGraphMetadata = memo(
if (sandwichedLabel) {
parts.push(
<span key={'sandwich'}>
<Tooltip key={'sandwich'} content={sandwichedLabel} placement="top">
<div>
<Icon size={'sm'} name={'angle-right'} />
<div className={styles.metadataPill}>
<Icon size={'sm'} name={'gf-show-context'} />{' '}
@@ -58,17 +59,22 @@ const FlameGraphMetadata = memo(
aria-label={'Remove sandwich view'}
/>
</div>
</span>
</div>
</Tooltip>
);
}
if (focusedItem) {
const percentValue = Math.round(10000 * (focusedItem.item.value / totalTicks)) / 100;
const percentValue = totalTicks > 0 ? Math.round(10000 * (focusedItem.item.value / totalTicks)) / 100 : 0;
const iconName = percentValue > 0 ? 'eye' : 'exclamation-circle';
parts.push(
<span key={'focus'}>
<Tooltip key={'focus'} content={focusedItem.label} placement="top">
<div>
<Icon size={'sm'} name={'angle-right'} />
<div className={styles.metadataPill}>
<Icon size={'sm'} name={'eye'} /> {percentValue}% of total
<Icon size={'sm'} name={iconName} />
&nbsp;{percentValue}% of total
<IconButton
className={styles.pillCloseButton}
name={'times'}
@@ -78,7 +84,8 @@ const FlameGraphMetadata = memo(
aria-label={'Remove focus'}
/>
</div>
</span>
</div>
</Tooltip>
);
}
@@ -107,8 +114,10 @@ const getStyles = (theme: GrafanaTheme2) => ({
margin: theme.spacing(0, 0.5),
}),
metadata: css({
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
margin: '8px 0',
textAlign: 'center',
}),
metadataPillName: css({
label: 'metadataPillName',

View File

@@ -73,6 +73,10 @@ export type Props = {
* Disable behaviour where similar items in the same stack will be collapsed into single item.
*/
disableCollapsing?: boolean;
/**
* Whether or not to keep any focused item when the profile data changes.
*/
keepFocusOnDataChange?: boolean;
};
const FlameGraphContainer = ({
@@ -87,6 +91,7 @@ const FlameGraphContainer = ({
vertical,
showFlameGraphOnly,
disableCollapsing,
keepFocusOnDataChange,
getExtraContextMenuButtons,
}: Props) => {
const [focusedItemData, setFocusedItemData] = useState<ClickedItemData>();
@@ -133,14 +138,44 @@ const FlameGraphContainer = ({
setRangeMax(1);
}, [setFocusedItemData, setRangeMax, setRangeMin]);
function resetSandwich() {
const resetSandwich = useCallback(() => {
setSandwichItem(undefined);
}
}, [setSandwichItem]);
useEffect(() => {
if (!keepFocusOnDataChange) {
resetFocus();
resetSandwich();
}, [data, resetFocus]);
return;
}
if (dataContainer && focusedItemData) {
const item = dataContainer.getNodesWithLabel(focusedItemData.label)?.[0];
if (item) {
setFocusedItemData({ ...focusedItemData, item });
const levels = dataContainer.getLevels();
const totalViewTicks = levels.length ? levels[0][0].value : 0;
setRangeMin(item.start / totalViewTicks);
setRangeMax((item.start + item.value) / totalViewTicks);
} else {
setFocusedItemData({
...focusedItemData,
item: {
start: 0,
value: 0,
itemIndexes: [],
children: [],
level: 0,
},
});
setRangeMin(0);
setRangeMax(1);
}
}
}, [dataContainer, keepFocusOnDataChange]); // eslint-disable-line react-hooks/exhaustive-deps
const onSymbolClick = useCallback(
(symbol: string) => {