UNPKG

@grafana/flamegraph

Version:

Grafana flamegraph visualization component

614 lines (607 loc) • 20.6 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var jsxRuntime = require('react/jsx-runtime'); var react = require('react'); var css = require('@emotion/css'); var uFuzzy = require('@leeoniya/ufuzzy'); var reactUse = require('react-use'); var data = require('@grafana/data'); var ui = require('@grafana/ui'); var FlameGraph = require('./FlameGraph/FlameGraph.cjs'); var dataTransform = require('./FlameGraph/dataTransform.cjs'); var FlameGraphHeader = require('./FlameGraphHeader.cjs'); var FlameGraphPane = require('./FlameGraphPane.cjs'); var FlameGraphTopTableContainer = require('./TopTable/FlameGraphTopTableContainer.cjs'); var constants = require('./constants.cjs'); var hooks = require('./hooks.cjs'); var types = require('./types.cjs'); var utils = require('./utils.cjs'); function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e : { default: e }; } var uFuzzy__default = /*#__PURE__*/_interopDefaultCompat(uFuzzy); "use strict"; const ufuzzy = new uFuzzy__default.default(); const FlameGraphContainer = ({ data, onTableSymbolClick, onViewSelected, onTextAlignSelected, onTableSort, getTheme, stickyHeader, extraHeaderElements, vertical, showFlameGraphOnly, disableCollapsing, keepFocusOnDataChange, getExtraContextMenuButtons, showAnalyzeWithAssistant = true, enableNewUI }) => { const theme = react.useMemo(() => getTheme(), [getTheme]); if (enableNewUI) { return /* @__PURE__ */ jsxRuntime.jsx( NewUIContainer, { data, onTableSymbolClick, onViewSelected, onTextAlignSelected, onTableSort, theme, stickyHeader, extraHeaderElements, vertical, showFlameGraphOnly, disableCollapsing, keepFocusOnDataChange, getExtraContextMenuButtons, showAnalyzeWithAssistant } ); } return /* @__PURE__ */ jsxRuntime.jsx( LegacyContainer, { data, onTableSymbolClick, onViewSelected, onTextAlignSelected, onTableSort, theme, stickyHeader, extraHeaderElements, vertical, showFlameGraphOnly, disableCollapsing, keepFocusOnDataChange, getExtraContextMenuButtons, showAnalyzeWithAssistant } ); }; const LegacyContainer = ({ data: data$1, onTableSymbolClick, onViewSelected, onTextAlignSelected, onTableSort, theme, stickyHeader, extraHeaderElements, vertical, showFlameGraphOnly, disableCollapsing, keepFocusOnDataChange, getExtraContextMenuButtons, showAnalyzeWithAssistant }) => { const [focusedItemData, setFocusedItemData] = react.useState(); const [rangeMin, setRangeMin] = react.useState(0); const [rangeMax, setRangeMax] = react.useState(1); const [search, setSearch] = react.useState(""); const [selectedView, setSelectedView] = react.useState(types.SelectedView.Both); const [sizeRef, { width: containerWidth }] = reactUse.useMeasure(); const [textAlign, setTextAlign] = react.useState("left"); const [sandwichItem, setSandwichItem] = react.useState(); const [collapsedMap, setCollapsedMap] = react.useState(new dataTransform.CollapsedMap()); const onTableSymbolClickRef = react.useRef(onTableSymbolClick); const onTableSortRef = react.useRef(onTableSort); onTableSymbolClickRef.current = onTableSymbolClick; onTableSortRef.current = onTableSort; const dataContainer = react.useMemo(() => { if (!data$1) { return; } const container = new dataTransform.FlameGraphDataContainer(data$1, { collapsing: !disableCollapsing }, theme); setCollapsedMap(container.getCollapsedMap()); return container; }, [data$1, theme, disableCollapsing]); const [colorScheme, setColorScheme] = hooks.useColorScheme(dataContainer); const styles = getStyles(theme); const matchedLabels = useLabelSearch(search, dataContainer); react.useEffect(() => { if (containerWidth > 0 && containerWidth < constants.MIN_WIDTH_TO_SHOW_BOTH_TOPTABLE_AND_FLAMEGRAPH && selectedView === types.SelectedView.Both && !vertical) { setSelectedView(types.SelectedView.FlameGraph); } }, [selectedView, setSelectedView, containerWidth, vertical]); const resetFocus = react.useCallback(() => { setFocusedItemData(void 0); setRangeMin(0); setRangeMax(1); }, [setFocusedItemData, setRangeMax, setRangeMin]); const resetSandwich = react.useCallback(() => { setSandwichItem(void 0); }, [setSandwichItem]); react.useEffect(() => { var _a; if (!keepFocusOnDataChange) { resetFocus(); resetSandwich(); return; } if (dataContainer && focusedItemData) { const item = (_a = dataContainer.getNodesWithLabel(focusedItemData.label)) == null ? void 0 : _a[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]); const onSymbolClick = react.useCallback( (symbol) => { var _a; const anchored = `^${data.escapeStringForRegex(symbol)}$`; if (search === anchored) { setSearch(""); } else { (_a = onTableSymbolClickRef.current) == null ? void 0 : _a.call(onTableSymbolClickRef, symbol); setSearch(anchored); resetFocus(); } }, [setSearch, resetFocus, search] ); const onSearch = react.useCallback( (str) => { if (!str) { setSearch(""); return; } setSearch(`^${data.escapeStringForRegex(str)}$`); }, [setSearch] ); const onSandwich = react.useCallback( (label) => { resetFocus(); setSandwichItem(label); }, [resetFocus, setSandwichItem] ); const onTableSortStable = react.useCallback((sort) => { var _a; (_a = onTableSortRef.current) == null ? void 0 : _a.call(onTableSortRef, sort); }, []); if (!dataContainer) { return null; } const flameGraph = /* @__PURE__ */ jsxRuntime.jsx( FlameGraph, { data: dataContainer, rangeMin, rangeMax, matchedLabels, setRangeMin, setRangeMax, onItemFocused: (data2) => setFocusedItemData(data2), focusedItemData, textAlign, sandwichItem, onSandwich, onFocusPillClick: resetFocus, onSandwichPillClick: resetSandwich, colorScheme, showFlameGraphOnly, collapsing: !disableCollapsing, getExtraContextMenuButtons, selectedView, search, collapsedMap, setCollapsedMap } ); const table = /* @__PURE__ */ jsxRuntime.jsx( FlameGraphTopTableContainer.default, { data: dataContainer, onSymbolClick, search, matchedLabels, sandwichItem, onSandwich: setSandwichItem, onSearch, onTableSort: onTableSortStable, colorScheme } ); let body; if (showFlameGraphOnly || selectedView === types.SelectedView.FlameGraph) { body = flameGraph; } else if (selectedView === types.SelectedView.TopTable) { body = /* @__PURE__ */ jsxRuntime.jsx("div", { className: styles.tableContainer, children: table }); } else if (selectedView === types.SelectedView.Both) { if (vertical) { body = /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [ /* @__PURE__ */ jsxRuntime.jsx("div", { className: styles.verticalGraphContainer, children: flameGraph }), /* @__PURE__ */ jsxRuntime.jsx("div", { className: styles.verticalTableContainer, children: table }) ] }); } else { body = /* @__PURE__ */ jsxRuntime.jsxs("div", { className: styles.horizontalContainer, children: [ /* @__PURE__ */ jsxRuntime.jsx("div", { className: styles.horizontalTableContainer, children: table }), /* @__PURE__ */ jsxRuntime.jsx("div", { className: styles.horizontalGraphContainer, children: flameGraph }) ] }); } } return ( // We add the theme context to bridge the gap if this is rendered in non grafana environment where the context // isn't already provided. /* @__PURE__ */ jsxRuntime.jsx(ui.ThemeContext.Provider, { value: theme, children: /* @__PURE__ */ jsxRuntime.jsxs("div", { ref: sizeRef, className: styles.container, children: [ !showFlameGraphOnly && /* @__PURE__ */ jsxRuntime.jsx( FlameGraphHeader.default, { search, setSearch, selectedView, setSelectedView: (view) => { setSelectedView(view); onViewSelected == null ? void 0 : onViewSelected(view); }, containerWidth, onReset: () => { resetFocus(); resetSandwich(); }, textAlign, onTextAlignChange: (align) => { setTextAlign(align); onTextAlignSelected == null ? void 0 : onTextAlignSelected(align); }, showResetButton: Boolean(focusedItemData || sandwichItem), colorScheme, onColorSchemeChange: setColorScheme, stickyHeader: Boolean(stickyHeader), extraHeaderElements, vertical, isDiffMode: dataContainer.isDiffFlamegraph(), setCollapsedMap, collapsedMap, assistantContext: data$1 && showAnalyzeWithAssistant ? utils.getAssistantContextFromDataFrame(data$1) : void 0 } ), /* @__PURE__ */ jsxRuntime.jsx("div", { className: styles.body, children: body }) ] }) }) ); }; const NewUIContainer = ({ data, onTableSymbolClick, onViewSelected, onTextAlignSelected, onTableSort, theme, stickyHeader, extraHeaderElements, vertical, showFlameGraphOnly, disableCollapsing, keepFocusOnDataChange, getExtraContextMenuButtons, showAnalyzeWithAssistant }) => { const [search, setSearch] = react.useState(""); const [viewMode, setViewMode] = react.useState(types.ViewMode.Split); const [leftPaneView, setLeftPaneView] = react.useState(types.PaneView.TopTable); const [rightPaneView, setRightPaneView] = react.useState(types.PaneView.FlameGraph); const [singleView, setSingleView] = react.useState(types.PaneView.FlameGraph); const [panesSwapped, setPanesSwapped] = react.useState(false); const [sizeRef, { width: containerWidth }] = reactUse.useMeasure(); const [resetKey, setResetKey] = react.useState(0); const [focusedItemIndexes, setFocusedItemIndexes] = react.useState(void 0); const [sharedSandwichItem, setSharedSandwichItem] = react.useState(void 0); const canShowSplitView = containerWidth > 0 && (containerWidth >= constants.MIN_WIDTH_FOR_SPLIT_VIEW || Boolean(vertical)); const onTableSymbolClickRef = react.useRef(onTableSymbolClick); const onTextAlignSelectedRef = react.useRef(onTextAlignSelected); const onTableSortRef = react.useRef(onTableSort); react.useEffect(() => { onTableSymbolClickRef.current = onTableSymbolClick; onTextAlignSelectedRef.current = onTextAlignSelected; onTableSortRef.current = onTableSort; }); const stableOnTableSymbolClick = react.useCallback((symbol) => { var _a; (_a = onTableSymbolClickRef.current) == null ? void 0 : _a.call(onTableSymbolClickRef, symbol); }, []); const stableOnTextAlignSelected = react.useCallback((align) => { var _a; (_a = onTextAlignSelectedRef.current) == null ? void 0 : _a.call(onTextAlignSelectedRef, align); }, []); const stableOnTableSort = react.useCallback((sort) => { var _a; (_a = onTableSortRef.current) == null ? void 0 : _a.call(onTableSortRef, sort); }, []); const dataContainer = react.useMemo(() => { if (!data) { return; } return new dataTransform.FlameGraphDataContainer(data, { collapsing: !disableCollapsing }, theme); }, [data, theme, disableCollapsing]); const styles = getStyles(theme); const matchedLabels = useLabelSearch(search, dataContainer); const effectiveViewMode = canShowSplitView ? viewMode : types.ViewMode.Single; const prevViewMode = reactUse.usePrevious(viewMode); react.useEffect(() => { if (prevViewMode === void 0) { return; } if (prevViewMode === types.ViewMode.Split && viewMode === types.ViewMode.Single) { setSingleView(rightPaneView); } else if (prevViewMode === types.ViewMode.Single && viewMode === types.ViewMode.Split) { setRightPaneView(singleView); } }, [viewMode, prevViewMode, rightPaneView, singleView]); if (!dataContainer) { return null; } const commonPaneProps = { dataContainer, search, matchedLabels, onTableSymbolClick: stableOnTableSymbolClick, onTextAlignSelected: stableOnTextAlignSelected, onTableSort: stableOnTableSort, showFlameGraphOnly, disableCollapsing, getExtraContextMenuButtons, setSearch, resetKey, keepFocusOnDataChange, focusedItemIndexes, setFocusedItemIndexes }; let body; if (showFlameGraphOnly) { body = /* @__PURE__ */ jsxRuntime.jsx( FlameGraphPane, { ...commonPaneProps, paneView: types.PaneView.FlameGraph, viewMode: effectiveViewMode, paneViewForContextMenu: types.PaneView.FlameGraph, sharedSandwichItem, setSharedSandwichItem } ); } else if (effectiveViewMode === types.ViewMode.Single) { body = /* @__PURE__ */ jsxRuntime.jsx( FlameGraphPane, { ...commonPaneProps, paneView: singleView, viewMode: types.ViewMode.Single, paneViewForContextMenu: singleView, sharedSandwichItem, setSharedSandwichItem } ); } else { const shouldSyncSandwich = leftPaneView !== rightPaneView; const leftPane = /* @__PURE__ */ react.createElement( FlameGraphPane, { ...commonPaneProps, key: "left-pane", paneView: leftPaneView, viewMode: types.ViewMode.Split, paneViewForContextMenu: leftPaneView, sharedSandwichItem: shouldSyncSandwich ? sharedSandwichItem : void 0, setSharedSandwichItem: shouldSyncSandwich ? setSharedSandwichItem : void 0 } ); const rightPane = /* @__PURE__ */ react.createElement( FlameGraphPane, { ...commonPaneProps, key: "right-pane", paneView: rightPaneView, viewMode: types.ViewMode.Split, paneViewForContextMenu: rightPaneView, sharedSandwichItem: shouldSyncSandwich ? sharedSandwichItem : void 0, setSharedSandwichItem: shouldSyncSandwich ? setSharedSandwichItem : void 0 } ); if (vertical) { body = /* @__PURE__ */ jsxRuntime.jsxs("div", { className: styles.verticalContainer, children: [ /* @__PURE__ */ jsxRuntime.jsx("div", { className: styles.verticalPaneContainer, style: { order: panesSwapped ? 2 : 1 }, children: leftPane }), /* @__PURE__ */ jsxRuntime.jsx("div", { className: styles.verticalPaneContainer, style: { order: panesSwapped ? 1 : 2 }, children: rightPane }) ] }); } else { body = /* @__PURE__ */ jsxRuntime.jsxs("div", { className: styles.horizontalContainer, children: [ /* @__PURE__ */ jsxRuntime.jsx("div", { className: styles.horizontalPaneContainer, style: { order: panesSwapped ? 2 : 1 }, children: leftPane }), /* @__PURE__ */ jsxRuntime.jsx("div", { className: styles.horizontalPaneContainer, style: { order: panesSwapped ? 1 : 2 }, children: rightPane }) ] }); } } return /* @__PURE__ */ jsxRuntime.jsx(ui.ThemeContext.Provider, { value: theme, children: /* @__PURE__ */ jsxRuntime.jsxs("div", { ref: sizeRef, className: styles.container, children: [ !showFlameGraphOnly && /* @__PURE__ */ jsxRuntime.jsx( FlameGraphHeader.default, { enableNewUI: true, search, setSearch, viewMode, setViewMode: (mode) => { setViewMode(mode); onViewSelected == null ? void 0 : onViewSelected(mode === types.ViewMode.Split ? "split" : singleView); }, canShowSplitView, containerWidth, leftPaneView: panesSwapped ? rightPaneView : leftPaneView, setLeftPaneView: panesSwapped ? setRightPaneView : setLeftPaneView, rightPaneView: panesSwapped ? leftPaneView : rightPaneView, setRightPaneView: panesSwapped ? setLeftPaneView : setRightPaneView, singleView, setSingleView: (view) => { setSingleView(view); if (viewMode === types.ViewMode.Single) { onViewSelected == null ? void 0 : onViewSelected(view); } }, onSwapPanes: () => setPanesSwapped((s) => !s), onReset: () => { setSearch(""); setFocusedItemIndexes(void 0); setSharedSandwichItem(void 0); setResetKey((k) => k + 1); }, showResetButton: Boolean(search), stickyHeader: Boolean(stickyHeader), extraHeaderElements, assistantContext: data && showAnalyzeWithAssistant ? utils.getAssistantContextFromDataFrame(data) : void 0 } ), /* @__PURE__ */ jsxRuntime.jsx("div", { className: styles.body, children: body }) ] }) }); }; function useLabelSearch(search, data) { return react.useMemo(() => { if (!search || !data) { return void 0; } return labelSearch(search, data); }, [search, data]); } function labelSearch(search, data) { const foundLabels = /* @__PURE__ */ new Set(); const terms = search.split(","); const regexFilter = (labels, pattern) => { let regex; try { regex = new RegExp(pattern); } catch (e) { return false; } let foundMatch = false; for (let label of labels) { if (!regex.test(label)) { continue; } foundLabels.add(label); foundMatch = true; } return foundMatch; }; const fuzzyFilter = (labels, term) => { let idxs = ufuzzy.filter(labels, term); if (!idxs) { return false; } let foundMatch = false; for (let idx of idxs) { foundLabels.add(labels[idx]); foundMatch = true; } return foundMatch; }; for (let term of terms) { if (!term) { continue; } const found = regexFilter(data.getUniqueLabels(), term); if (!found) { fuzzyFilter(data.getUniqueLabels(), term); } } return foundLabels; } function getStyles(theme) { return { container: css.css({ label: "container", overflow: "auto", height: "100%", display: "flex", flex: "1 1 0", flexDirection: "column", minHeight: 0, gap: theme.spacing(1) }), body: css.css({ label: "body", flexGrow: 1 }), tableContainer: css.css({ // This is not ideal for dashboard panel where it creates a double scroll. In a panel it should be 100% but then // in explore we need a specific height. height: constants.FLAMEGRAPH_CONTAINER_HEIGHT }), horizontalContainer: css.css({ label: "horizontalContainer", display: "flex", minHeight: 0, flexDirection: "row", columnGap: theme.spacing(1), width: "100%" }), horizontalGraphContainer: css.css({ flexBasis: "50%" }), horizontalTableContainer: css.css({ flexBasis: "50%", maxHeight: constants.FLAMEGRAPH_CONTAINER_HEIGHT }), verticalGraphContainer: css.css({ marginBottom: theme.spacing(1) }), verticalTableContainer: css.css({ height: constants.FLAMEGRAPH_CONTAINER_HEIGHT }), verticalContainer: css.css({ label: "verticalContainer", display: "flex", flexDirection: "column" }), horizontalPaneContainer: css.css({ label: "horizontalPaneContainer", flexBasis: "50%", maxHeight: constants.FLAMEGRAPH_CONTAINER_HEIGHT, minWidth: 0, overflow: "auto" }), verticalPaneContainer: css.css({ label: "verticalPaneContainer", marginBottom: theme.spacing(1), height: constants.FLAMEGRAPH_CONTAINER_HEIGHT }) }; } exports.default = FlameGraphContainer; exports.labelSearch = labelSearch; exports.useLabelSearch = useLabelSearch; //# sourceMappingURL=FlameGraphContainer.cjs.map