UNPKG

@grafana/flamegraph

Version:

Grafana flamegraph visualization component

1,466 lines (1,450 loc) • 259 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var jsxRuntime = require('react/jsx-runtime'); var css = require('@emotion/css'); var uFuzzy = require('@leeoniya/ufuzzy'); var react = require('react'); var reactUse = require('react-use'); var ui = require('@grafana/ui'); var data$1 = require('@grafana/data'); var color = require('tinycolor2'); var d3 = require('d3'); var lodash = require('lodash'); var useDebounce = require('react-use/lib/useDebounce'); var usePrevious = require('react-use/lib/usePrevious'); var AutoSizer = require('react-virtualized-auto-sizer'); function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e : { default: e }; } var uFuzzy__default = /*#__PURE__*/_interopDefaultCompat(uFuzzy); var color__default = /*#__PURE__*/_interopDefaultCompat(color); var useDebounce__default = /*#__PURE__*/_interopDefaultCompat(useDebounce); var usePrevious__default = /*#__PURE__*/_interopDefaultCompat(usePrevious); var AutoSizer__default = /*#__PURE__*/_interopDefaultCompat(AutoSizer); const PIXELS_PER_LEVEL = 22 * window.devicePixelRatio; const MUTE_THRESHOLD = 10 * window.devicePixelRatio; const HIDE_THRESHOLD = 0.5 * window.devicePixelRatio; const LABEL_THRESHOLD = 20 * window.devicePixelRatio; const BAR_BORDER_WIDTH = 0.5 * window.devicePixelRatio; const BAR_TEXT_PADDING_LEFT = 4 * window.devicePixelRatio; const GROUP_STRIP_WIDTH = 3 * window.devicePixelRatio; const GROUP_STRIP_PADDING = 3 * window.devicePixelRatio; const GROUP_STRIP_MARGIN_LEFT = 4 * window.devicePixelRatio; const GROUP_TEXT_OFFSET = 2 * window.devicePixelRatio; const MIN_WIDTH_TO_SHOW_BOTH_TOPTABLE_AND_FLAMEGRAPH = 800; const TOP_TABLE_COLUMN_WIDTH = 120; const FlameGraphContextMenu = ({ data, itemData, onMenuItemClick, onItemFocus, onSandwich, collapseConfig, onExpandGroup, onCollapseGroup, onExpandAllGroups, onCollapseAllGroups, getExtraContextMenuButtons, collapsing, allGroupsExpanded, allGroupsCollapsed, selectedView, search }) => { function renderItems() { const extraButtons = (getExtraContextMenuButtons == null ? void 0 : getExtraContextMenuButtons(itemData, data.data, { selectedView, isDiff: data.isDiffFlamegraph(), search, collapseConfig })) || []; return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [ /* @__PURE__ */ jsxRuntime.jsx( ui.MenuItem, { label: "Focus block", icon: "eye", onClick: () => { onItemFocus(); onMenuItemClick(); } } ), /* @__PURE__ */ jsxRuntime.jsx( ui.MenuItem, { label: "Copy function name", icon: "copy", onClick: () => { navigator.clipboard.writeText(itemData.label).then(() => { onMenuItemClick(); }); } } ), /* @__PURE__ */ jsxRuntime.jsx( ui.MenuItem, { label: "Sandwich view", icon: "gf-show-context", onClick: () => { onSandwich(); onMenuItemClick(); } } ), extraButtons.map(({ label, icon, onClick }) => { return /* @__PURE__ */ jsxRuntime.jsx(ui.MenuItem, { label, icon, onClick: () => onClick() }, label); }), collapsing && /* @__PURE__ */ jsxRuntime.jsxs(ui.MenuGroup, { label: "Grouping", children: [ collapseConfig ? collapseConfig.collapsed ? /* @__PURE__ */ jsxRuntime.jsx( ui.MenuItem, { label: "Expand group", icon: "angle-double-down", onClick: () => { onExpandGroup(); onMenuItemClick(); } } ) : /* @__PURE__ */ jsxRuntime.jsx( ui.MenuItem, { label: "Collapse group", icon: "angle-double-up", onClick: () => { onCollapseGroup(); onMenuItemClick(); } } ) : null, !allGroupsExpanded && /* @__PURE__ */ jsxRuntime.jsx( ui.MenuItem, { label: "Expand all groups", icon: "angle-double-down", onClick: () => { onExpandAllGroups(); onMenuItemClick(); } } ), !allGroupsCollapsed && /* @__PURE__ */ jsxRuntime.jsx( ui.MenuItem, { label: "Collapse all groups", icon: "angle-double-up", onClick: () => { onCollapseAllGroups(); onMenuItemClick(); } } ) ] }) ] }); } return /* @__PURE__ */ jsxRuntime.jsx("div", { "data-testid": "contextMenu", children: /* @__PURE__ */ jsxRuntime.jsx( ui.ContextMenu, { renderMenuItems: renderItems, x: itemData.posX + 10, y: itemData.posY, focusOnOpen: false } ) }); }; const FlameGraphTooltip = ({ data, item, totalTicks, position, collapseConfig }) => { const styles = ui.useStyles2(getStyles$6); if (!(item && position)) { return null; } let content; if (data.isDiffFlamegraph()) { const tableData = getDiffTooltipData(data, item, totalTicks); content = /* @__PURE__ */ jsxRuntime.jsx( ui.InteractiveTable, { className: styles.tooltipTable, columns: [ { id: "label", header: "" }, { id: "baseline", header: "Baseline" }, { id: "comparison", header: "Comparison" }, { id: "diff", header: "Diff" } ], data: tableData, getRowId: (originalRow) => originalRow.rowId } ); } else { const tooltipData = getTooltipData(data, item, totalTicks); content = /* @__PURE__ */ jsxRuntime.jsxs("p", { className: styles.lastParagraph, children: [ tooltipData.unitTitle, /* @__PURE__ */ jsxRuntime.jsx("br", {}), "Total: ", /* @__PURE__ */ jsxRuntime.jsx("b", { children: tooltipData.unitValue }), " (", tooltipData.percentValue, "%)", /* @__PURE__ */ jsxRuntime.jsx("br", {}), "Self: ", /* @__PURE__ */ jsxRuntime.jsx("b", { children: tooltipData.unitSelf }), " (", tooltipData.percentSelf, "%)", /* @__PURE__ */ jsxRuntime.jsx("br", {}), "Samples: ", /* @__PURE__ */ jsxRuntime.jsx("b", { children: tooltipData.samples }) ] }); } return /* @__PURE__ */ jsxRuntime.jsx(ui.Portal, { children: /* @__PURE__ */ jsxRuntime.jsx(ui.VizTooltipContainer, { className: styles.tooltipContainer, position, offset: { x: 15, y: 0 }, children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: styles.tooltipContent, children: [ /* @__PURE__ */ jsxRuntime.jsxs("p", { className: styles.tooltipName, children: [ data.getLabel(item.itemIndexes[0]), collapseConfig && collapseConfig.collapsed ? /* @__PURE__ */ jsxRuntime.jsxs("span", { children: [ /* @__PURE__ */ jsxRuntime.jsx("br", {}), "and ", collapseConfig.items.length, " similar items" ] }) : "" ] }), content ] }) }) }); }; const getTooltipData = (data, item, totalTicks) => { const displayValue = data.valueDisplayProcessor(item.value); const displaySelf = data.getSelfDisplay(item.itemIndexes); const percentValue = Math.round(1e4 * (displayValue.numeric / totalTicks)) / 100; const percentSelf = Math.round(1e4 * (displaySelf.numeric / totalTicks)) / 100; let unitValue = displayValue.text + displayValue.suffix; let unitSelf = displaySelf.text + displaySelf.suffix; const unitTitle = data.getUnitTitle(); if (unitTitle === "Count") { if (!displayValue.suffix) { unitValue = displayValue.text; } if (!displaySelf.suffix) { unitSelf = displaySelf.text; } } return { percentValue, percentSelf, unitTitle, unitValue, unitSelf, samples: displayValue.numeric.toLocaleString() }; }; const getDiffTooltipData = (data, item, totalTicks) => { const levels = data.getLevels(); const totalTicksRight = levels[0][0].valueRight; const totalTicksLeft = totalTicks - totalTicksRight; const valueLeft = item.value - item.valueRight; const percentageLeft = Math.round(1e4 * valueLeft / totalTicksLeft) / 100; const percentageRight = Math.round(1e4 * item.valueRight / totalTicksRight) / 100; const diff = (percentageRight - percentageLeft) / percentageLeft * 100; const displayValueLeft = getValueWithUnit(data, data.valueDisplayProcessor(valueLeft)); const displayValueRight = getValueWithUnit(data, data.valueDisplayProcessor(item.valueRight)); const shortValFormat = data$1.getValueFormat("short"); return [ { rowId: "1", label: "% of total", baseline: percentageLeft + "%", comparison: percentageRight + "%", diff: shortValFormat(diff).text + "%" }, { rowId: "2", label: "Value", baseline: displayValueLeft, comparison: displayValueRight, diff: getValueWithUnit(data, data.valueDisplayProcessor(item.valueRight - valueLeft)) }, { rowId: "3", label: "Samples", baseline: shortValFormat(valueLeft).text, comparison: shortValFormat(item.valueRight).text, diff: shortValFormat(item.valueRight - valueLeft).text } ]; }; function getValueWithUnit(data, displayValue) { let unitValue = displayValue.text + displayValue.suffix; const unitTitle = data.getUnitTitle(); if (unitTitle === "Count") { if (!displayValue.suffix) { unitValue = displayValue.text; } } return unitValue; } const getStyles$6 = (theme) => ({ tooltipContainer: css.css({ title: "tooltipContainer", overflow: "hidden" }), tooltipContent: css.css({ title: "tooltipContent", fontSize: theme.typography.bodySmall.fontSize, width: "100%" }), tooltipName: css.css({ title: "tooltipName", marginTop: 0, wordBreak: "break-all" }), lastParagraph: css.css({ title: "lastParagraph", marginBottom: 0 }), name: css.css({ title: "name", marginBottom: "10px" }), tooltipTable: css.css({ title: "tooltipTable", maxWidth: "400px" }) }); var SampleUnit = /* @__PURE__ */ ((SampleUnit2) => { SampleUnit2["Bytes"] = "bytes"; SampleUnit2["Short"] = "short"; SampleUnit2["Nanoseconds"] = "ns"; return SampleUnit2; })(SampleUnit || {}); var SelectedView = /* @__PURE__ */ ((SelectedView2) => { SelectedView2["TopTable"] = "topTable"; SelectedView2["FlameGraph"] = "flameGraph"; SelectedView2["Both"] = "both"; return SelectedView2; })(SelectedView || {}); var ColorScheme = /* @__PURE__ */ ((ColorScheme2) => { ColorScheme2["ValueBased"] = "valueBased"; ColorScheme2["PackageBased"] = "packageBased"; return ColorScheme2; })(ColorScheme || {}); var ColorSchemeDiff = /* @__PURE__ */ ((ColorSchemeDiff2) => { ColorSchemeDiff2["Default"] = "default"; ColorSchemeDiff2["DiffColorBlind"] = "diffColorBlind"; return ColorSchemeDiff2; })(ColorSchemeDiff || {}); function murmurhash3_32_gc(key, seed = 0) { let remainder; let bytes; let h1; let h1b; let c1; let c2; let k1; let i; remainder = key.length & 3; bytes = key.length - remainder; h1 = seed; c1 = 3432918353; c2 = 461845907; i = 0; while (i < bytes) { k1 = key.charCodeAt(i) & 255 | (key.charCodeAt(++i) & 255) << 8 | (key.charCodeAt(++i) & 255) << 16 | (key.charCodeAt(++i) & 255) << 24; ++i; k1 = (k1 & 65535) * c1 + (((k1 >>> 16) * c1 & 65535) << 16) & 4294967295; k1 = k1 << 15 | k1 >>> 17; k1 = (k1 & 65535) * c2 + (((k1 >>> 16) * c2 & 65535) << 16) & 4294967295; h1 ^= k1; h1 = h1 << 13 | h1 >>> 19; h1b = (h1 & 65535) * 5 + (((h1 >>> 16) * 5 & 65535) << 16) & 4294967295; h1 = (h1b & 65535) + 27492 + (((h1b >>> 16) + 58964 & 65535) << 16); } k1 = 0; switch (remainder) { case 3: k1 ^= (key.charCodeAt(i + 2) & 255) << 16; // fall through case 2: k1 ^= (key.charCodeAt(i + 1) & 255) << 8; // fall through case 1: k1 ^= key.charCodeAt(i) & 255; // fall through default: k1 = (k1 & 65535) * c1 + (((k1 >>> 16) * c1 & 65535) << 16) & 4294967295; k1 = k1 << 15 | k1 >>> 17; k1 = (k1 & 65535) * c2 + (((k1 >>> 16) * c2 & 65535) << 16) & 4294967295; h1 ^= k1; } h1 ^= key.length; h1 ^= h1 >>> 16; h1 = (h1 & 65535) * 2246822507 + (((h1 >>> 16) * 2246822507 & 65535) << 16) & 4294967295; h1 ^= h1 >>> 13; h1 = (h1 & 65535) * 3266489909 + (((h1 >>> 16) * 3266489909 & 65535) << 16) & 4294967295; h1 ^= h1 >>> 16; return h1 >>> 0; } const packageColors = [ color__default.default({ h: 24, s: 69, l: 60 }), color__default.default({ h: 34, s: 65, l: 65 }), color__default.default({ h: 194, s: 52, l: 61 }), color__default.default({ h: 163, s: 45, l: 55 }), color__default.default({ h: 211, s: 48, l: 60 }), color__default.default({ h: 246, s: 40, l: 65 }), color__default.default({ h: 305, s: 63, l: 79 }), color__default.default({ h: 47, s: 100, l: 73 }), color__default.default({ r: 183, g: 219, b: 171 }), color__default.default({ r: 244, g: 213, b: 152 }), color__default.default({ r: 78, g: 146, b: 249 }), color__default.default({ r: 249, g: 186, b: 143 }), color__default.default({ r: 242, g: 145, b: 145 }), color__default.default({ r: 130, g: 181, b: 216 }), color__default.default({ r: 229, g: 168, b: 226 }), color__default.default({ r: 174, g: 162, b: 224 }), color__default.default({ r: 154, g: 196, b: 138 }), color__default.default({ r: 242, g: 201, b: 109 }), color__default.default({ r: 101, g: 197, b: 219 }), color__default.default({ r: 249, g: 147, b: 78 }), color__default.default({ r: 234, g: 100, b: 96 }), color__default.default({ r: 81, g: 149, b: 206 }), color__default.default({ r: 214, g: 131, b: 206 }), color__default.default({ r: 128, g: 110, b: 183 }) ]; const byValueMinColor = getBarColorByValue(1, 100, 0, 1); const byValueMaxColor = getBarColorByValue(100, 100, 0, 1); const byValueGradient = `linear-gradient(90deg, ${byValueMinColor} 0%, ${byValueMaxColor} 100%)`; const byPackageGradient = `linear-gradient(90deg, ${packageColors[0]} 0%, ${packageColors[2]} 30%, ${packageColors[6]} 50%, ${packageColors[7]} 70%, ${packageColors[8]} 100%)`; function getBarColorByValue(value, totalTicks, rangeMin, rangeMax) { const intensity = Math.min(1, value / totalTicks / (rangeMax - rangeMin)); const h = 50 - 50 * intensity; const l = 65 + 7 * intensity; return color__default.default({ h, s: 100, l }); } function getBarColorByPackage(label, theme) { const packageName = getPackageName(label); const hash = murmurhash3_32_gc(packageName || "", 0); const colorIndex = hash % packageColors.length; let packageColor = packageColors[colorIndex].clone(); if (theme.isLight) { packageColor = packageColor.brighten(15); } return packageColor; } const diffDefaultColors = ["rgb(0, 170, 0)", "rgb(148, 142, 142)", "rgb(200, 0, 0)"]; const diffDefaultGradient = `linear-gradient(90deg, ${diffDefaultColors[0]} 0%, ${diffDefaultColors[1]} 50%, ${diffDefaultColors[2]} 100%)`; const diffColorBlindColors = ["rgb(26, 133, 255)", "rgb(148, 142, 142)", "rgb(220, 50, 32)"]; const diffColorBlindGradient = `linear-gradient(90deg, ${diffColorBlindColors[0]} 0%, ${diffColorBlindColors[1]} 50%, ${diffColorBlindColors[2]} 100%)`; function getBarColorByDiff(ticks, ticksRight, totalTicks, totalTicksRight, colorScheme) { const range = colorScheme === ColorSchemeDiff.Default ? diffDefaultColors : diffColorBlindColors; const colorScale = d3.scaleLinear().domain([-100, 0, 100]).range(range); const ticksLeft = ticks - ticksRight; const totalTicksLeft = totalTicks - totalTicksRight; if (totalTicksRight === 0 || totalTicksLeft === 0) { const rgbString2 = colorScale(0); return color__default.default(rgbString2); } const percentageLeft = Math.round(1e4 * ticksLeft / totalTicksLeft) / 100; const percentageRight = Math.round(1e4 * ticksRight / totalTicksRight) / 100; const diff = (percentageRight - percentageLeft) / percentageLeft * 100; const rgbString = colorScale(diff); return color__default.default(rgbString); } const matchers = [ ["phpspy", /^(?<packageName>([^\/]*\/)*)(?<filename>.*\.php+)(?<line_info>.*)$/], ["pyspy", /^(?<packageName>([^\/]*\/)*)(?<filename>.*\.py+)(?<line_info>.*)$/], ["rbspy", /^(?<packageName>([^\/]*\/)*)(?<filename>.*\.rb+)(?<line_info>.*)$/], [ "nodespy", /^(\.\/node_modules\/)?(?<packageName>[^/]*)(?<filename>.*\.?(jsx?|tsx?)?):(?<functionName>.*):(?<line_info>.*)$/ ], ["gospy", /^(?<packageName>.*?\/.*?\.|.*?\.|.+)(?<functionName>.*)$/], // also 'scrape' ["javaspy", /^(?<packageName>.+\/)(?<filename>.+\.)(?<functionName>.+)$/], ["dotnetspy", /^(?<packageName>.+)\.(.+)\.(.+)\(.*\)$/], ["tracing", /^(?<packageName>.+?):.*$/], ["pyroscope-rs", /^(?<packageName>[^::]+)/], ["ebpfspy", /^(?<packageName>.+)$/], ["unknown", /^(?<packageName>.+)$/] ]; function getPackageName(name) { var _a; for (const [_, matcher] of matchers) { const match = name.match(matcher); if (match) { return ((_a = match.groups) == null ? void 0 : _a.packageName) || ""; } } return void 0; } function useFlameRender(options) { const { canvasRef, data, root, depth, direction, wrapperWidth, rangeMin, rangeMax, matchedLabels, textAlign, totalViewTicks, totalColorTicks, totalTicksRight, colorScheme, focusedItemData, collapsedMap } = options; const ctx = useSetupCanvas(canvasRef, wrapperWidth, depth); const theme = ui.useTheme2(); const mutedColor = react.useMemo(() => { const barMutedColor = color__default.default(theme.colors.background.secondary); return theme.isLight ? barMutedColor.darken(10).toHexString() : barMutedColor.lighten(10).toHexString(); }, [theme]); const getBarColor = useColorFunction( totalColorTicks, totalTicksRight, colorScheme, theme, mutedColor, rangeMin, rangeMax, matchedLabels, focusedItemData ? focusedItemData.item.level : 0 ); const renderFunc = useRenderFunc(ctx, data, getBarColor, textAlign, collapsedMap); react.useEffect(() => { if (!ctx) { return; } ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height); const mutedPath2D = new Path2D(); walkTree( root, direction, data, totalViewTicks, rangeMin, rangeMax, wrapperWidth, collapsedMap, (item, x, y, width, height, label, muted) => { if (muted) { mutedPath2D.rect(x, y, width, height); } else { renderFunc(item, x, y, width, height, label); } } ); ctx.fillStyle = mutedColor; ctx.fill(mutedPath2D); }, [ ctx, data, root, wrapperWidth, rangeMin, rangeMax, totalViewTicks, direction, renderFunc, collapsedMap, mutedColor ]); } function useRenderFunc(ctx, data, getBarColor, textAlign, collapsedMap) { return react.useMemo(() => { if (!ctx) { return () => { }; } const renderFunc = (item, x, y, width, height, label) => { ctx.beginPath(); ctx.rect(x + BAR_BORDER_WIDTH, y, width, height); ctx.fillStyle = getBarColor(item, label, false); ctx.stroke(); ctx.fill(); const collapsedItemConfig = collapsedMap.get(item); let finalLabel = label; if (collapsedItemConfig && collapsedItemConfig.collapsed) { const numberOfCollapsedItems = collapsedItemConfig.items.length; finalLabel = `(${numberOfCollapsedItems}) ` + label; } if (width >= LABEL_THRESHOLD) { if (collapsedItemConfig) { renderLabel( ctx, data, finalLabel, item, width, textAlign === "left" ? x + GROUP_STRIP_MARGIN_LEFT + GROUP_TEXT_OFFSET : x, y, textAlign ); renderGroupingStrip(ctx, x, y, height, item, collapsedItemConfig); } else { renderLabel(ctx, data, finalLabel, item, width, x, y, textAlign); } } }; return renderFunc; }, [ctx, getBarColor, textAlign, data, collapsedMap]); } function renderGroupingStrip(ctx, x, y, height, item, collapsedItemConfig) { const groupStripX = x + GROUP_STRIP_MARGIN_LEFT; ctx.beginPath(); ctx.rect(x, y, groupStripX - x + GROUP_STRIP_WIDTH + GROUP_STRIP_PADDING, height); ctx.fill(); ctx.beginPath(); if (collapsedItemConfig.collapsed) { ctx.rect(groupStripX, y + height / 4, GROUP_STRIP_WIDTH, height / 2); } else { if (collapsedItemConfig.items[0] === item) { ctx.rect(groupStripX, y + height / 2, GROUP_STRIP_WIDTH, height / 2); } else if (collapsedItemConfig.items[collapsedItemConfig.items.length - 1] === item) { ctx.rect(groupStripX, y, GROUP_STRIP_WIDTH, height / 2); } else { ctx.rect(groupStripX, y, GROUP_STRIP_WIDTH, height); } } ctx.fillStyle = "#666"; ctx.fill(); } function walkTree(root, direction, data, totalViewTicks, rangeMin, rangeMax, wrapperWidth, collapsedMap, renderFunc) { const stack = []; stack.push({ item: root, levelOffset: 0 }); const pixelsPerTick = wrapperWidth * window.devicePixelRatio / totalViewTicks / (rangeMax - rangeMin); let collapsedItemRendered = void 0; while (stack.length > 0) { const { item, levelOffset } = stack.shift(); let curBarTicks = item.value; const muted = curBarTicks * pixelsPerTick <= MUTE_THRESHOLD; const width = curBarTicks * pixelsPerTick - (muted ? 0 : BAR_BORDER_WIDTH * 2); const height = PIXELS_PER_LEVEL; if (width < HIDE_THRESHOLD) { continue; } let offsetModifier = 0; let skipRender = false; const collapsedItemConfig = collapsedMap.get(item); const isCollapsedItem = collapsedItemConfig && collapsedItemConfig.collapsed; if (isCollapsedItem) { if (collapsedItemRendered === collapsedItemConfig.items[0]) { offsetModifier = direction === "children" ? -1 : 1; skipRender = true; } else { collapsedItemRendered = void 0; } } else { collapsedItemRendered = void 0; } if (!skipRender) { const barX = getBarX(item.start, totalViewTicks, rangeMin, pixelsPerTick); const barY = (item.level + levelOffset) * PIXELS_PER_LEVEL; let label = data.getLabel(item.itemIndexes[0]); if (isCollapsedItem) { collapsedItemRendered = item; } renderFunc(item, barX, barY, width, height, label, muted); } const nextList = direction === "children" ? item.children : item.parents; if (nextList) { stack.unshift(...nextList.map((c) => ({ item: c, levelOffset: levelOffset + offsetModifier }))); } } } function useColorFunction(totalTicks, totalTicksRight, colorScheme, theme, mutedColor, rangeMin, rangeMax, matchedLabels, topLevel) { return react.useCallback( function getColor(item, label, muted) { if (muted && !matchedLabels) { return mutedColor; } const barColor = item.valueRight !== void 0 && (colorScheme === ColorSchemeDiff.Default || colorScheme === ColorSchemeDiff.DiffColorBlind) ? getBarColorByDiff(item.value, item.valueRight, totalTicks, totalTicksRight, colorScheme) : colorScheme === ColorScheme.ValueBased ? getBarColorByValue(item.value, totalTicks, rangeMin, rangeMax) : getBarColorByPackage(label, theme); if (matchedLabels) { return matchedLabels.has(label) ? barColor.toHslString() : mutedColor; } return item.level > topLevel - 1 ? barColor.toHslString() : barColor.lighten(15).toHslString(); }, [totalTicks, totalTicksRight, colorScheme, theme, rangeMin, rangeMax, matchedLabels, topLevel, mutedColor] ); } function useSetupCanvas(canvasRef, wrapperWidth, numberOfLevels) { const [ctx, setCtx] = react.useState(); react.useEffect(() => { if (!(numberOfLevels && canvasRef.current)) { return; } const ctx2 = canvasRef.current.getContext("2d"); const height = PIXELS_PER_LEVEL * numberOfLevels; canvasRef.current.width = Math.round(wrapperWidth * window.devicePixelRatio); canvasRef.current.height = Math.round(height); canvasRef.current.style.width = `${wrapperWidth}px`; canvasRef.current.style.height = `${height / window.devicePixelRatio}px`; ctx2.textBaseline = "middle"; ctx2.font = 12 * window.devicePixelRatio + "px monospace"; ctx2.strokeStyle = "white"; setCtx(ctx2); }, [canvasRef, setCtx, wrapperWidth, numberOfLevels]); return ctx; } function renderLabel(ctx, data, label, item, width, x, y, textAlign) { ctx.save(); ctx.clip(); ctx.fillStyle = "#222"; const displayValue = data.valueDisplayProcessor(item.value); const unit = displayValue.suffix ? displayValue.text + displayValue.suffix : displayValue.text; const measure = ctx.measureText(label); const spaceForTextInRect = width - BAR_TEXT_PADDING_LEFT; let fullLabel = `${label} (${unit})`; let labelX = Math.max(x, 0) + BAR_TEXT_PADDING_LEFT; if (measure.width > spaceForTextInRect) { ctx.textAlign = textAlign; if (textAlign === "right") { fullLabel = label; labelX = x + width - BAR_TEXT_PADDING_LEFT; } } ctx.fillText(fullLabel, labelX, y + PIXELS_PER_LEVEL / 2 + 2); ctx.restore(); } function getBarX(offset, totalTicks, rangeMin, pixelsPerTick) { return (offset - totalTicks * rangeMin) * pixelsPerTick; } const FlameGraphCanvas = ({ data, rangeMin, rangeMax, matchedLabels, setRangeMin, setRangeMax, onItemFocused, focusedItemData, textAlign, onSandwich, colorScheme, totalProfileTicks, totalProfileTicksRight, totalViewTicks, root, direction, depth, showFlameGraphOnly, collapsedMap, setCollapsedMap, collapsing, getExtraContextMenuButtons, selectedView, search }) => { const styles = getStyles$5(); const [sizeRef, { width: wrapperWidth }] = reactUse.useMeasure(); const graphRef = react.useRef(null); const [tooltipItem, setTooltipItem] = react.useState(); const [clickedItemData, setClickedItemData] = react.useState(); useFlameRender({ canvasRef: graphRef, colorScheme, data, focusedItemData, root, direction, depth, rangeMax, rangeMin, 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. totalColorTicks: data.isDiffFlamegraph() ? totalProfileTicks : totalViewTicks, totalTicksRight: totalProfileTicksRight, wrapperWidth, collapsedMap }); const onGraphClick = react.useCallback( (e) => { setTooltipItem(void 0); const pixelsPerTick = graphRef.current.clientWidth / totalViewTicks / (rangeMax - rangeMin); const item = convertPixelCoordinatesToBarCoordinates( { x: e.nativeEvent.offsetX, y: e.nativeEvent.offsetY }, root, direction, depth, pixelsPerTick, totalViewTicks, rangeMin, collapsedMap ); if (item) { setClickedItemData({ posY: e.clientY, posX: e.clientX, item, label: data.getLabel(item.itemIndexes[0]) }); } else { setClickedItemData(void 0); } }, [data, rangeMin, rangeMax, totalViewTicks, root, direction, depth, collapsedMap] ); const [mousePosition, setMousePosition] = react.useState(); const onGraphMouseMove = react.useCallback( (e) => { if (clickedItemData === void 0) { setTooltipItem(void 0); setMousePosition(void 0); const pixelsPerTick = graphRef.current.clientWidth / totalViewTicks / (rangeMax - rangeMin); const item = convertPixelCoordinatesToBarCoordinates( { x: e.nativeEvent.offsetX, y: e.nativeEvent.offsetY }, root, direction, depth, pixelsPerTick, totalViewTicks, rangeMin, collapsedMap ); if (item) { setMousePosition({ x: e.clientX, y: e.clientY }); setTooltipItem(item); } } }, [rangeMin, rangeMax, totalViewTicks, clickedItemData, setMousePosition, root, direction, depth, collapsedMap] ); const onGraphMouseLeave = react.useCallback(() => { setTooltipItem(void 0); }, []); react.useEffect(() => { const handleOnClick = (e) => { var _a; if (e.target instanceof HTMLElement && ((_a = e.target.parentElement) == null ? void 0 : _a.id) !== "flameGraphCanvasContainer_clickOutsideCheck") { setClickedItemData(void 0); } }; window.addEventListener("click", handleOnClick); return () => window.removeEventListener("click", handleOnClick); }, [setClickedItemData]); return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: styles.graph, children: [ /* @__PURE__ */ jsxRuntime.jsx("div", { className: styles.canvasWrapper, id: "flameGraphCanvasContainer_clickOutsideCheck", ref: sizeRef, children: /* @__PURE__ */ jsxRuntime.jsx( "canvas", { ref: graphRef, "data-testid": "flameGraph", onClick: onGraphClick, onMouseMove: onGraphMouseMove, onMouseLeave: onGraphMouseLeave } ) }), /* @__PURE__ */ jsxRuntime.jsx( FlameGraphTooltip, { position: mousePosition, item: tooltipItem, data, totalTicks: totalViewTicks, collapseConfig: tooltipItem ? collapsedMap.get(tooltipItem) : void 0 } ), !showFlameGraphOnly && clickedItemData && /* @__PURE__ */ jsxRuntime.jsx( FlameGraphContextMenu, { data, itemData: clickedItemData, collapsing, collapseConfig: collapsedMap.get(clickedItemData.item), onMenuItemClick: () => { setClickedItemData(void 0); }, onItemFocus: () => { setRangeMin(clickedItemData.item.start / totalViewTicks); setRangeMax((clickedItemData.item.start + clickedItemData.item.value) / totalViewTicks); onItemFocused(clickedItemData); }, onSandwich: () => { onSandwich(data.getLabel(clickedItemData.item.itemIndexes[0])); }, onExpandGroup: () => { setCollapsedMap(collapsedMap.setCollapsedStatus(clickedItemData.item, false)); }, onCollapseGroup: () => { setCollapsedMap(collapsedMap.setCollapsedStatus(clickedItemData.item, true)); }, onExpandAllGroups: () => { setCollapsedMap(collapsedMap.setAllCollapsedStatus(false)); }, onCollapseAllGroups: () => { setCollapsedMap(collapsedMap.setAllCollapsedStatus(true)); }, allGroupsCollapsed: Array.from(collapsedMap.values()).every((i) => i.collapsed), allGroupsExpanded: Array.from(collapsedMap.values()).every((i) => !i.collapsed), getExtraContextMenuButtons, selectedView, search } ) ] }); }; const getStyles$5 = () => ({ graph: css.css({ label: "graph", overflow: "auto", flexGrow: 1, flexBasis: "50%" }), canvasContainer: css.css({ label: "canvasContainer", display: "flex" }), canvasWrapper: css.css({ label: "canvasWrapper", cursor: "pointer", flex: 1, overflow: "hidden" }), sandwichMarker: css.css({ label: "sandwichMarker", writingMode: "vertical-lr", transform: "rotate(180deg)", overflow: "hidden", whiteSpace: "nowrap" }), sandwichMarkerIcon: css.css({ label: "sandwichMarkerIcon", verticalAlign: "baseline" }) }); const convertPixelCoordinatesToBarCoordinates = (pos, root, direction, depth, pixelsPerTick, totalTicks, rangeMin, collapsedMap) => { let next = root; let currentLevel = direction === "children" ? 0 : depth - 1; const levelIndex = Math.floor(pos.y / (PIXELS_PER_LEVEL / window.devicePixelRatio)); let found = void 0; while (next) { const node = next; next = void 0; if (currentLevel === levelIndex) { found = node; break; } const nextList = direction === "children" ? node.children : node.parents || []; for (const child of nextList) { const xStart = getBarX(child.start, totalTicks, rangeMin, pixelsPerTick); const xEnd = getBarX(child.start + child.value, totalTicks, rangeMin, pixelsPerTick); if (xStart <= pos.x && pos.x < xEnd) { next = child; const collapsedConfig = collapsedMap.get(child); if (!collapsedConfig || !collapsedConfig.collapsed || collapsedConfig.items[0] === child) { currentLevel = currentLevel + (direction === "children" ? 1 : -1); } break; } } } return found; }; const FlameGraphMetadata = react.memo( ({ data, focusedItem, totalTicks, sandwichedLabel, onFocusPillClick, onSandwichPillClick }) => { const styles = ui.useStyles2(getStyles$4); const parts = []; const ticksVal = data$1.getValueFormat("short")(totalTicks); const displayValue = data.valueDisplayProcessor(totalTicks); let unitValue = displayValue.text + displayValue.suffix; const unitTitle = data.getUnitTitle(); if (unitTitle === "Count") { if (!displayValue.suffix) { unitValue = displayValue.text; } } parts.push( /* @__PURE__ */ jsxRuntime.jsxs("div", { className: styles.metadataPill, children: [ unitValue, " | ", ticksVal.text, ticksVal.suffix, " samples (", unitTitle, ")" ] }, "default") ); if (sandwichedLabel) { parts.push( /* @__PURE__ */ jsxRuntime.jsx(ui.Tooltip, { content: sandwichedLabel, placement: "top", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [ /* @__PURE__ */ jsxRuntime.jsx(ui.Icon, { size: "sm", name: "angle-right" }), /* @__PURE__ */ jsxRuntime.jsxs("div", { className: styles.metadataPill, children: [ /* @__PURE__ */ jsxRuntime.jsx(ui.Icon, { size: "sm", name: "gf-show-context" }), " ", /* @__PURE__ */ jsxRuntime.jsx("span", { className: styles.metadataPillName, children: sandwichedLabel.substring(sandwichedLabel.lastIndexOf("/") + 1) }), /* @__PURE__ */ jsxRuntime.jsx( ui.IconButton, { className: styles.pillCloseButton, name: "times", size: "sm", onClick: onSandwichPillClick, tooltip: "Remove sandwich view", "aria-label": "Remove sandwich view" } ) ] }) ] }) }, "sandwich") ); } if (focusedItem) { const percentValue = totalTicks > 0 ? Math.round(1e4 * (focusedItem.item.value / totalTicks)) / 100 : 0; const iconName = percentValue > 0 ? "eye" : "exclamation-circle"; parts.push( /* @__PURE__ */ jsxRuntime.jsx(ui.Tooltip, { content: focusedItem.label, placement: "top", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [ /* @__PURE__ */ jsxRuntime.jsx(ui.Icon, { size: "sm", name: "angle-right" }), /* @__PURE__ */ jsxRuntime.jsxs("div", { className: styles.metadataPill, children: [ /* @__PURE__ */ jsxRuntime.jsx(ui.Icon, { size: "sm", name: iconName }), "\xA0", percentValue, "% of total", /* @__PURE__ */ jsxRuntime.jsx( ui.IconButton, { className: styles.pillCloseButton, name: "times", size: "sm", onClick: onFocusPillClick, tooltip: "Remove focus", "aria-label": "Remove focus" } ) ] }) ] }) }, "focus") ); } return /* @__PURE__ */ jsxRuntime.jsx("div", { className: styles.metadata, children: parts }); } ); FlameGraphMetadata.displayName = "FlameGraphMetadata"; const getStyles$4 = (theme) => ({ metadataPill: css.css({ label: "metadataPill", display: "inline-flex", alignItems: "center", background: theme.colors.background.secondary, borderRadius: theme.shape.borderRadius(8), padding: theme.spacing(0.5, 1), fontSize: theme.typography.bodySmall.fontSize, fontWeight: theme.typography.fontWeightMedium, lineHeight: theme.typography.bodySmall.lineHeight, color: theme.colors.text.secondary }), pillCloseButton: css.css({ label: "pillCloseButton", verticalAlign: "text-bottom", margin: theme.spacing(0, 0.5) }), metadata: css.css({ display: "flex", alignItems: "center", justifyContent: "center", margin: "8px 0" }), metadataPillName: css.css({ label: "metadataPillName", maxWidth: "200px", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", marginLeft: theme.spacing(0.5) }) }); const FlameGraph = ({ data, rangeMin, rangeMax, matchedLabels, setRangeMin, setRangeMax, onItemFocused, focusedItemData, textAlign, onSandwich, sandwichItem, onFocusPillClick, onSandwichPillClick, colorScheme, showFlameGraphOnly, getExtraContextMenuButtons, collapsing, selectedView, search, collapsedMap, setCollapsedMap }) => { const styles = getStyles$3(); const [levels, setLevels] = react.useState(); const [levelsCallers, setLevelsCallers] = react.useState(); const [totalProfileTicks, setTotalProfileTicks] = react.useState(0); const [totalProfileTicksRight, setTotalProfileTicksRight] = react.useState(); const [totalViewTicks, setTotalViewTicks] = react.useState(0); react.useEffect(() => { var _a, _b, _c; if (data) { let levels2 = data.getLevels(); let totalProfileTicks2 = levels2.length ? levels2[0][0].value : 0; let totalProfileTicksRight2 = levels2.length ? levels2[0][0].valueRight : void 0; let totalViewTicks2 = totalProfileTicks2; let levelsCallers2 = void 0; if (sandwichItem) { const [callers, callees] = data.getSandwichLevels(sandwichItem); levels2 = callees; levelsCallers2 = callers; totalViewTicks2 = (_c = (_b = (_a = callees[0]) == null ? void 0 : _a[0]) == null ? void 0 : _b.value) != null ? _c : 0; } setLevels(levels2); setLevelsCallers(levelsCallers2); setTotalProfileTicks(totalProfileTicks2); setTotalProfileTicksRight(totalProfileTicksRight2); setTotalViewTicks(totalViewTicks2); } }, [data, sandwichItem]); if (!levels) { return null; } const commonCanvasProps = { data, rangeMin, rangeMax, matchedLabels, setRangeMin, setRangeMax, onItemFocused, focusedItemData, textAlign, onSandwich, colorScheme, totalProfileTicks, totalProfileTicksRight, totalViewTicks, showFlameGraphOnly, collapsedMap, setCollapsedMap, getExtraContextMenuButtons, collapsing, search, selectedView }; let canvas = null; if (levelsCallers == null ? void 0 : levelsCallers.length) { canvas = /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: styles.sandwichCanvasWrapper, children: [ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: styles.sandwichMarker, children: [ "Callers", /* @__PURE__ */ jsxRuntime.jsx(ui.Icon, { className: styles.sandwichMarkerIcon, name: "arrow-down" }) ] }), /* @__PURE__ */ jsxRuntime.jsx( FlameGraphCanvas, { ...commonCanvasProps, root: levelsCallers[levelsCallers.length - 1][0], depth: levelsCallers.length, direction: "parents", collapsing: false } ) ] }), /* @__PURE__ */ jsxRuntime.jsxs("div", { className: styles.sandwichCanvasWrapper, children: [ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: css.cx(styles.sandwichMarker, styles.sandwichMarkerCalees), children: [ /* @__PURE__ */ jsxRuntime.jsx(ui.Icon, { className: styles.sandwichMarkerIcon, name: "arrow-up" }), "Callees" ] }), /* @__PURE__ */ jsxRuntime.jsx( FlameGraphCanvas, { ...commonCanvasProps, root: levels[0][0], depth: levels.length, direction: "children", collapsing: false } ) ] }) ] }); } else if (levels == null ? void 0 : levels.length) { canvas = /* @__PURE__ */ jsxRuntime.jsx(FlameGraphCanvas, { ...commonCanvasProps, root: levels[0][0], depth: levels.length, direction: "children" }); } return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: styles.graph, children: [ /* @__PURE__ */ jsxRuntime.jsx( FlameGraphMetadata, { data, focusedItem: focusedItemData, sandwichedLabel: sandwichItem, totalTicks: totalViewTicks, onFocusPillClick, onSandwichPillClick } ), canvas ] }); }; const getStyles$3 = () => ({ graph: css.css({ label: "graph", overflow: "auto", flexGrow: 1, flexBasis: "50%" }), sandwichCanvasWrapper: css.css({ label: "sandwichCanvasWrapper", display: "flex", marginBottom: `${PIXELS_PER_LEVEL / window.devicePixelRatio}px` }), sandwichMarker: css.css({ label: "sandwichMarker", writingMode: "vertical-lr", transform: "rotate(180deg)", overflow: "hidden", whiteSpace: "nowrap" }), sandwichMarkerCalees: css.css({ label: "sandwichMarkerCalees", textAlign: "right" }), sandwichMarkerIcon: css.css({ label: "sandwichMarkerIcon", verticalAlign: "baseline" }) }); function mergeParentSubtrees(roots, data) { const newRoots = getParentSubtrees(roots); return mergeSubtrees(newRoots, data, "parents"); } function getParentSubtrees(roots) { return roots.map((r) => { var _a, _b; if (!((_a = r.parents) == null ? void 0 : _a.length)) { return r; } const newRoot = { ...r, children: [] }; const stack = [ { child: newRoot, parent: r.parents[0] } ]; while (stack.length) { const args = stack.shift(); const newNode = { ...args.parent, children: args.child ? [args.child] : [], parents: [] }; if (args.child) { newNode.value = args.child.value; args.child.parents = [newNode]; } if ((_b = args.parent.parents) == null ? void 0 : _b.length) { stack.push({ child: newNode, parent: args.parent.parents[0] }); } } return newRoot; }); } function mergeSubtrees(roots, data, direction = "children") { var _a; const oppositeDirection = direction === "parents" ? "children" : "parents"; const levels = []; const stack = [ { previous: void 0, items: roots, level: 0 } ]; while (stack.length) { const args = stack.shift(); const indexes = args.items.flatMap((i) => i.itemIndexes); const newItem = { // We use the items value instead of value from the data frame, cause we could have changed it in the process value: args.items.reduce((acc, i) => acc + i.value, 0), itemIndexes: indexes, // these will change later children: [], parents: [], start: 0, level: args.level }; levels[args.level] = levels[args.level] || []; levels[args.level].push(newItem); if (args.previous) { newItem[oppositeDirection] = [args.previous]; const prevSiblingsVal = ((_a = args.previous[direction]) == null ? void 0 : _a.reduce((acc, node) => { return acc + node.value; }, 0)) || 0; newItem.start = args.previous.start + prevSiblingsVal; args.previous[direction].push(newItem); } const nextItems = args.items.flatMap((i) => i[direction] || []); const nextGroups = lodash.groupBy(nextItems, (c) => data.getLabel(c.itemIndexes[0])); for (const g of Object.values(nextGroups)) { stack.push({ previous: newItem, items: g, level: args.level + 1 }); } } if (direction === "parents") { levels.reverse(); levels.forEach((level, index) => { level.forEach((item) => { item.level = index; }); }); } return levels; } function nestedSetToLevels(container, options) { const levels = []; let offset = 0; let parent = void 0; const uniqueLabels = {}; for (let i = 0; i < container.data.length; i++) { const currentLevel = container.getLevel(i); const prevLevel = i > 0 ? container.getLevel(i - 1) : void 0; levels[currentLevel] = levels[currentLevel] || []; if (prevLevel && prevLevel >= currentLevel) { const lastSibling = levels[currentLevel][levels[currentLevel].length - 1]; offset = lastSibling.start + container.getValue(lastSibling.itemIndexes[0]) + container.getValueRight(lastSibling.itemIndexes[0]); parent = lastSibling.parents[0]; } const newItem = { itemIndexes: [i], value: container.getValue(i) + container.getValueRight(i), valueRight: container.isDiffFlamegraph() ? container.getValueRight(i) : void 0, start: offset, parents: parent && [parent], children: [], level: currentLevel }; if (uniqueLabels[container.getLabel(i)]) { uniqueLabels[container.getLabel(i)].push(newItem); } else { uniqueLabels[container.getLabel(i)] = [newItem]; } if (parent) { parent.children.push(newItem); } parent = newItem; levels[currentLevel].push(newItem); } const collapsedMapContainer = new CollapsedMapBuilder(options == null ? void 0 : options.collapsingThreshold); if (options == null ? void 0 : options.collapsing) { collapsedMapContainer.addTree(levels[0][0]); } return [levels, uniqueLabels, collapsedMapContainer.getCollapsedMap()]; } class CollapsedMap { constructor(map) { // The levelItem used as a key is the item that will always be rendered in the flame graph. The config.items are all // the items that are in the group and if the config.collapsed is true they will be hidden. this.map = /* @__PURE__ */ new Map(); this.map = map || /* @__PURE__ */ new Map(); } get(item) { return this.map.get(item); } keys() { return this.map.keys(); } values() { return this.map.values(); } size() { return this.map.size; } setCollapsedStatus(item, collapsed) { const newMap = new Map(this.map); const collapsedConfig = this.map.get(item); const newConfig = { ...collapsedConfig, collapsed }; for (const item2 of collapsedConfig.items) { newMap.set(item2, newConfig); } return new CollapsedMap(newMap); } setAllCollapsedStatus(collapsed) { const newMap = new Map(this.map); for (const item of this.map.keys()) { const collapsedConfig = this.map.get(item); const newConfig = { ...collapsedConfig, collapsed }; newMap.set(item, newConfig); } return new CollapsedMap(newMap); } } class CollapsedMapBuilder { constructor(threshold) { this.map = /* @__PURE__ */ new Map(); this.threshold = 0.99; if (threshold !== void 0) { this.threshold = threshold; } } addTree(root) { var _a; const stack = [root]; while (stack.length) { const current = stack.shift(); if ((_a = current.parents) == null ? void 0 : _a.length) { this.addItem(current, current.parents[0]); } if (current.children.length) { stack.unshift(...current.children); } } } // The heuristics here is pretty simple right now. Just check if it's single child and if we are within threshold. // We assume items with small self just aren't too important while we cannot really collapse items with siblings // as it's not clear what to do with said sibling. addItem(item, parent) { if (parent && item.value > parent.value * this.threshold && parent.children.length === 1) { if (this.map.has(parent)) { const config = this.map.get(parent); this.map.set(item, config); config.items.push(item); } else { const config = { items: [parent, item], collapsed: true }; this.map.set(parent, config); this.map.set(item, config); } } } getCollapsedMap() { return new CollapsedMap(this.map); } } function getMessageCheckFieldsResult(wrongFields) { if (wrongFields.missingFields.length) { return `Data is missing fields: ${wrongFields.missingFields.join(", ")}`; } if (wrongFields.wrongTypeFields.length) { return `Data has fields of wrong type: ${wrongFields.wrongTypeFields.map((f) => `${f.name} has type ${f.type} but should be ${f.expectedTypes.join(" or ")}`).join(", ")}`; } return ""; } function checkFields(data) { const fields = [ ["label", [data$1.FieldType.string, data$1.FieldType.enum]], ["level", [data$1.FieldType.number]], ["value", [data$1.FieldType.number]], ["self", [data$1.FieldType.number]] ]; const missingFields = []; const wrongTypeFields = []; for (const field of fields) { const [name, types] = field; const frameField = data == null ? void 0 : data.fields.find((f) => f.name === name); if (!frameField) { missingFields.push(name); continue; } if (!types.includes(frameField.type)) { wrongTypeFields.push({ name, expectedTypes: types, type: frameField.type }); } } if (missingFields.length > 0 || wrongTypeFields.length > 0) { return { wrongTypeFields, missingFields }; } return void 0; } class FlameGraphDataContainer { constructor(data, options, theme = data$1.createTheme()) { var _a, _b, _c; this.data = data; this.options = options; const wrongFields = checkFields(data);