Flame Graph for Performance Profiling — MUI X Charts

A flame graph visualizes hierarchical call stack data from performance profiling, where each horizontal bar represents a function in the call stack and its width is proportional to the time (or samples) spent in that function. Stacks are layered bottom-to-top showing caller-to-callee relationships. Invented by Brendan Gregg, flame graphs are the standard visualization for identifying CPU bottlenecks and hot code paths across all major programming languages and profiling tools.

Flame Graph for Performance Profiling rendered with MUI X Charts

Renders

JavaScript source (MUI X Charts)

// anyplot.ai
// flamegraph-basic: Flame Graph for Performance Profiling
// Library: muix 7.29.1 | JavaScript 22.23.2
// Quality: 96/100 | Created: 2026-08-20
import { ChartContainer } from "@mui/x-charts/ChartContainer";
import { useXScale, useYScale } from "@mui/x-charts/hooks";

const t = window.ANYPLOT_TOKENS;

const title = "flamegraph-basic · javascript · muix · anyplot.ai";
const titleFontSize = Math.max(16, Math.round(34 * Math.min(1, 67 / title.length)));

// --- Data: a simulated CPU profile of a web-request handler, expressed as a
// call tree (function -> children) and fanned out via a fixed-seed LCG into
// dozens of realistic-looking stack traces -- comfortably inside the spec's
// 50-500 unique stack traces -- rather than a small illustrative example.
// Each node's samples equal the sum of its children's, exactly as a real
// profiler reports it. -------------------------------------------------------
let seed = 20260820;
function rand() {
  seed = (seed * 48271) % 2147483647;
  return seed / 2147483647;
}

const VERB = ["parse", "validate", "compute", "fetch", "serialize", "cache", "dispatch", "format", "aggregate", "filter", "normalize", "encode", "decode", "resolve", "persist", "merge"];
const NOUN = ["headers", "payload", "record", "query", "response", "token", "schema", "row", "buffer", "session", "event", "job", "metric", "column", "batch", "socket"];

const usedNames = new Set(["main", "parse_request", "authenticate_user", "handle_business_logic", "write_response"]);
function makeName() {
  let name;
  do {
    name = `${VERB[Math.floor(rand() * VERB.length)]}_${NOUN[Math.floor(rand() * NOUN.length)]}`;
  } while (usedNames.has(name));
  usedNames.add(name);
  return name;
}

// Recursively split `samples` into 2-3 unevenly-weighted children until
// `depth` runs out or a subtree is too small to split further, so the
// resulting traces read as an uneven, realistic profile rather than a
// perfectly balanced synthetic tree.
function expand(node, depth) {
  if (depth <= 0 || node.samples < 220) return node;
  const childCount = 2 + Math.floor(rand() * 2); // 2-3 children
  const weights = Array.from({ length: childCount }, () => 0.5 + rand());
  const weightSum = weights.reduce((a, b) => a + b, 0);
  let remaining = node.samples;
  node.children = weights.map((w, i) => {
    const samples = i === childCount - 1 ? remaining : Math.round((w / weightSum) * node.samples);
    remaining -= samples;
    return expand({ name: makeName(), samples }, depth - 1);
  });
  return node;
}

const callTree = {
  name: "main",
  children: [
    expand({ name: "parse_request", samples: 2100 }, 4),
    expand({ name: "authenticate_user", samples: 1400 }, 4),
    expand({ name: "handle_business_logic", samples: 7700 }, 5),
    expand({ name: "write_response", samples: 2800 }, 4),
  ],
};
callTree.samples = callTree.children.reduce((sum, c) => sum + c.samples, 0);
const TOTAL_SAMPLES = callTree.samples;

// Flatten the tree into positioned frames: x0/width in sample units (so bar
// width is proportional to samples, per the spec), depth = stack row.
// Siblings are laid out contiguously left-to-right with no gaps, standard
// flame-graph (icicle) layout.
function layoutFrames(node, depth, x0, out) {
  out.push({ name: node.name, depth, x0, width: node.samples, samples: node.samples });
  let cursor = x0;
  (node.children ?? []).forEach((child) => {
    layoutFrames(child, depth + 1, cursor, out);
    cursor += child.samples;
  });
  return out;
}
const frames = layoutFrames(callTree, 0, 0, []);
const maxDepth = Math.max(...frames.map((f) => f.depth));

// The dominant caller->callee chain (always descending into the heaviest
// child) -- the "hot path" a profiler reader would chase first.
function dominantChain(node, path) {
  path.push(node.name);
  if (!node.children || node.children.length === 0) return path;
  const hottestChild = node.children.reduce((a, b) => (b.samples > a.samples ? b : a));
  return dominantChain(hottestChild, path);
}
const HOT_CHAIN = dominantChain(callTree, []);
const HOT_PATH_NAMES = new Set(HOT_CHAIN);
const hotLeafSamples = (() => {
  let node = callTree;
  for (let i = 1; i < HOT_CHAIN.length; i++) node = node.children.find((c) => c.name === HOT_CHAIN[i]);
  return node.samples;
})();
const hotLeafPct = ((hotLeafSamples / TOTAL_SAMPLES) * 100).toFixed(1);
const hotPathCaption =
  HOT_CHAIN.length <= 4
    ? `Hot path: ${HOT_CHAIN.join(" → ")} — ${hotLeafPct}% of total samples`
    : `Hot path: ${HOT_CHAIN[0]} → ${HOT_CHAIN[1]} → … → ${HOT_CHAIN[HOT_CHAIN.length - 1]} — ${hotLeafPct}% of total samples`;

// --- Color: the spec's Notes call for "a warm color palette (yellows,
// oranges, reds) following the conventional flame graph aesthetic" -- built
// purely from Imprint anchors (amber/ochre/matte-red), keyed to each frame's
// share of total samples so hotter (more-sampled) frames read redder. The
// root is the 100%-baseline and keeps the theme-adaptive neutral treatment
// instead of joining the gradient. ------------------------------------------
const WARM_STOPS = [
  [221, 204, 119], // #DDCC77 amber
  [189, 130, 51], // #BD8233 ochre
  [174, 48, 48], // #AE3030 matte red
];
function warmColorRgb(u) {
  const clamped = Math.min(1, Math.max(0, u));
  const scaled = clamped * (WARM_STOPS.length - 1);
  const i = Math.min(WARM_STOPS.length - 2, Math.floor(scaled));
  const frac = scaled - i;
  return [0, 1, 2].map((c) => Math.round(WARM_STOPS[i][c] + (WARM_STOPS[i + 1][c] - WARM_STOPS[i][c]) * frac));
}
function relLuminance([r, g, b]) {
  const srgb = [r, g, b].map((v) => v / 255).map((v) => (v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4)));
  return 0.2126 * srgb[0] + 0.7152 * srgb[1] + 0.0722 * srgb[2];
}
const nonRootPcts = frames.filter((f) => f.depth > 0).map((f) => f.samples / TOTAL_SAMPLES);
const MIN_PCT = Math.min(...nonRootPcts);
const MAX_PCT = Math.max(...nonRootPcts);

const ROW_H = 0.92; // fraction of each depth row's band actually drawn (rest = gap)
const LABEL_FONT_SIZE = 14;
const CHAR_WIDTH_RATIO = 0.58; // rough average glyph width, in units of font size
const MARGIN = { top: 142, right: 40, bottom: 30, left: 110 };

// Each stack frame as a rect, sized/positioned from the layout above. The
// function name is drawn inside the bar only when it actually fits -- no
// rotated or truncated text, per the spec's "wide enough to fit" rule. A
// native <title> gives every frame a real (non-fake) hover tooltip. Frames
// on the dominant hot path get a bolder outline so the reader's eye is
// drawn straight to it instead of having to compare bar widths by hand.
function FlameFrames() {
  const xs = useXScale();
  const ys = useYScale();
  return (
    <g>
      {frames.map((f) => {
        const xLeft = xs(f.x0);
        const xRight = xs(f.x0 + f.width);
        const yTop = ys(f.depth + ROW_H);
        const yBottom = ys(f.depth);
        const w = xRight - xLeft;
        const h = yBottom - yTop;
        const pct = (f.samples / TOTAL_SAMPLES) * 100;
        const isRoot = f.depth === 0;
        const isHot = HOT_PATH_NAMES.has(f.name);
        const rgb = isRoot ? null : warmColorRgb((pct / 100 - MIN_PCT) / (MAX_PCT - MIN_PCT || 1));
        const fill = isRoot ? t.ink : `rgb(${rgb[0]}, ${rgb[1]}, ${rgb[2]})`;
        const labelFill = isRoot ? t.pageBg : relLuminance(rgb) > 0.45 ? "#1A1A17" : "#FAF8F1";
        const fits = w - 14 >= f.name.length * LABEL_FONT_SIZE * CHAR_WIDTH_RATIO;
        return (
          <g key={f.name}>
            <rect x={xLeft} y={yTop} width={w} height={h} fill={fill} stroke={isHot ? t.ink : t.pageBg} strokeWidth={isHot ? 3 : 1.5}>
              <title>{`${f.name}: ${f.samples.toLocaleString()} samples (${pct.toFixed(1)}%)`}</title>
            </rect>
            {fits && (
              <text
                x={xLeft + w / 2}
                y={yTop + h / 2}
                textAnchor="middle"
                dominantBaseline="central"
                fontSize={LABEL_FONT_SIZE}
                fontWeight={600}
                fill={labelFill}
                pointerEvents="none"
              >
                {f.name}
              </text>
            )}
          </g>
        );
      })}
    </g>
  );
}

// Row labels on the left -- the chart has no numeric x-axis (bar width is a
// proportion, not a metric to read off a scale; x-position is layout-only,
// not temporal -- see spec), so depth is the only axis worth labeling.
function DepthLabels() {
  const ys = useYScale();
  return (
    <g fontSize={13} fill={t.inkSoft} textAnchor="end">
      {Array.from({ length: maxDepth + 1 }, (_, d) => (
        <text key={d} x={MARGIN.left - 14} y={ys(d + ROW_H / 2)} dominantBaseline="central">
          {d === 0 ? "Depth 0 · root" : `Depth ${d}`}
        </text>
      ))}
    </g>
  );
}

// Color key: the root's neutral swatch plus the warm amber->ochre->red scale
// used for every other frame, keyed to each frame's share of total samples.
function HeatLegend() {
  const FS = 13;
  const CHAR_W = FS * 0.58;
  const { width } = window.ANYPLOT_SIZE;
  const y = 118;
  const barW = 220;
  const barH = 14;
  const rootLabel = "root · 100% of samples";
  const leftLabel = "fewer samples";
  const rightLabel = "more samples";
  const rootSwatchW = 14;
  const rootBlockW = rootSwatchW + 8 + rootLabel.length * CHAR_W;
  const leftLabelW = leftLabel.length * CHAR_W;
  const rightLabelW = rightLabel.length * CHAR_W;
  const GAP = 16;
  const SECTION_GAP = 40;
  const barBlockW = leftLabelW + GAP + barW + GAP + rightLabelW;
  const totalWidth = rootBlockW + SECTION_GAP + barBlockW;

  let x = width / 2 - totalWidth / 2;
  const rootSwatchX = x;
  const rootLabelX = rootSwatchX + rootSwatchW + 8;
  x += rootBlockW + SECTION_GAP;
  const leftLabelX = x;
  x += leftLabelW + GAP;
  const barX = x;
  x += barW + GAP;
  const rightLabelX = x;

  return (
    <g fontSize={FS} fill={t.inkSoft}>
      <defs>
        <linearGradient id="flamegraphHeatGradient" x1="0%" x2="100%" y1="0%" y2="0%">
          <stop offset="0%" stopColor="#DDCC77" />
          <stop offset="50%" stopColor="#BD8233" />
          <stop offset="100%" stopColor="#AE3030" />
        </linearGradient>
      </defs>
      <rect x={rootSwatchX} y={y - rootSwatchW + 2} width={rootSwatchW} height={rootSwatchW} rx={2} fill={t.ink} />
      <text x={rootLabelX} y={y}>
        {rootLabel}
      </text>
      <text x={leftLabelX} y={y}>
        {leftLabel}
      </text>
      <rect x={barX} y={y - barH + 2} width={barW} height={barH} rx={2} fill="url(#flamegraphHeatGradient)" />
      <text x={rightLabelX} y={y}>
        {rightLabel}
      </text>
    </g>
  );
}

// --- Chart (default-exported component -- the harness mounts it) -------------
export default function Chart() {
  const { width, height } = window.ANYPLOT_SIZE;
  const subtitle = `Simulated web-request CPU profile · ${TOTAL_SAMPLES.toLocaleString()} samples across ${frames.length} stack frames`;

  return (
    <ChartContainer
      width={width}
      height={height}
      series={[]}
      margin={MARGIN}
      xAxis={[{ scaleType: "linear", min: 0, max: TOTAL_SAMPLES, disableLine: true, disableTicks: true, valueFormatter: () => "" }]}
      yAxis={[{ scaleType: "linear", min: 0, max: maxDepth + 1, disableLine: true, disableTicks: true, valueFormatter: () => "" }]}
      skipAnimation
    >
      <FlameFrames />
      <DepthLabels />
      <HeatLegend />
      <text x={width / 2} y={46} textAnchor="middle" fontSize={titleFontSize} fontWeight={600} fill={t.ink}>
        {title}
      </text>
      <text x={width / 2} y={72} textAnchor="middle" fontSize={15} fill={t.inkSoft}>
        {subtitle}
      </text>
      <text x={width / 2} y={94} textAnchor="middle" fontSize={13} fontStyle="italic" fill={t.inkSoft}>
        {hotPathCaption}
      </text>
    </ChartContainer>
  );
}

Part of Flame Graph for Performance Profiling on anyplot.ai.

Other implementations