diff --git a/packages/grafana-flamegraph/README.md b/packages/grafana-flamegraph/README.md index 5a0df9974c7..9bd09dfc6c6 100644 --- a/packages/grafana-flamegraph/README.md +++ b/packages/grafana-flamegraph/README.md @@ -26,17 +26,18 @@ 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. | -| onTableSymbolClick | (symbol: string) => void | Interaction hook that can be used to report on the interaction. Fires when user click on a name in the table. | -| onViewSelected | (view: string) => void | Interaction hook that can be used to report on the interaction. Fires when user changes the view to show (table/graph/both) | -| onTextAlignSelected | (align: string) => void | Interaction hook that can be used to report on the interaction. Fires when user changes the text align. | -| 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. | +| 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. | +| onTableSymbolClick | (symbol: string) => void | Interaction hook that can be used to report on the interaction. Fires when user click on a name in the table. | +| onViewSelected | (view: string) => void | Interaction hook that can be used to report on the interaction. Fires when user changes the view to show (table/graph/both) | +| onTextAlignSelected | (align: string) => void | Interaction hook that can be used to report on the interaction. Fires when user changes the text align. | +| 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 diff --git a/packages/grafana-flamegraph/src/FlameGraph/FlameGraph.tsx b/packages/grafana-flamegraph/src/FlameGraph/FlameGraph.tsx index ffd8c88d4f9..a6c78f8bbeb 100644 --- a/packages/grafana-flamegraph/src/FlameGraph/FlameGraph.tsx +++ b/packages/grafana-flamegraph/src/FlameGraph/FlameGraph.tsx @@ -136,40 +136,46 @@ const FlameGraph = ({ search, selectedView, }; - const canvas = levelsCallers ? ( - <> -
-
- Callers - -
- -
+ let canvas = null; -
-
- - Callees + if (levelsCallers?.length) { + canvas = ( + <> +
+
+ Callers + +
+
- -
- - ) : ( - - ); + +
+
+ + Callees +
+ +
+ + ); + } else if (levels?.length) { + canvas = ( + + ); + } return (
diff --git a/packages/grafana-flamegraph/src/FlameGraph/FlameGraphMetadata.tsx b/packages/grafana-flamegraph/src/FlameGraph/FlameGraphMetadata.tsx index 488b48965b3..c39cddc4c27 100644 --- a/packages/grafana-flamegraph/src/FlameGraph/FlameGraphMetadata.tsx +++ b/packages/grafana-flamegraph/src/FlameGraph/FlameGraphMetadata.tsx @@ -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,43 +42,50 @@ const FlameGraphMetadata = memo( if (sandwichedLabel) { parts.push( - - -
- {' '} - - {sandwichedLabel.substring(sandwichedLabel.lastIndexOf('/') + 1)} - - + +
+ +
+ {' '} + + {sandwichedLabel.substring(sandwichedLabel.lastIndexOf('/') + 1)} + + +
- +
); } 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( - - -
- {percentValue}% of total - + +
+ +
+ +  {percentValue}% of total + +
- +
); } @@ -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', diff --git a/packages/grafana-flamegraph/src/FlameGraphContainer.tsx b/packages/grafana-flamegraph/src/FlameGraphContainer.tsx index d1ff3652e45..4d23da838e2 100644 --- a/packages/grafana-flamegraph/src/FlameGraphContainer.tsx +++ b/packages/grafana-flamegraph/src/FlameGraphContainer.tsx @@ -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(); @@ -133,14 +138,44 @@ const FlameGraphContainer = ({ setRangeMax(1); }, [setFocusedItemData, setRangeMax, setRangeMin]); - function resetSandwich() { + const resetSandwich = useCallback(() => { setSandwichItem(undefined); - } + }, [setSandwichItem]); useEffect(() => { - resetFocus(); - resetSandwich(); - }, [data, resetFocus]); + if (!keepFocusOnDataChange) { + resetFocus(); + resetSandwich(); + 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) => {