@grafana/flamegraph
Version:
Grafana flamegraph visualization component
1,466 lines (1,450 loc) • 259 kB
JavaScript
'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);