Flame Graph for Performance Profiling — D3.js

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 D3.js

Renders

JavaScript source (D3.js)

// anyplot.ai
// flamegraph-basic: Flame Graph for Performance Profiling
// Library: d3 7.9.0 | JavaScript 22.22.3
// Quality: 90/100 | Created: 2026-06-08
//# anyplot-orientation: landscape

const t = window.ANYPLOT_TOKENS;
const theme = window.ANYPLOT_THEME;
const { width, height } = window.ANYPLOT_SIZE;

const margin = { top: 110, right: 50, bottom: 78, left: 50 };
const iw = width - margin.left - margin.right;
const ih = height - margin.top - margin.bottom;

// --- Data: synthetic CPU profile (semicolon stacks, sample counts) ---------
// Models a typical web-service profile: HTTP request handler, background job
// worker, and the runtime garbage collector. `stack` is the call path from
// `main` to the leaf frame; `value` is how many CPU samples were observed in
// that exact stack frame (i.e. self-time of the leaf).
const samples = [
  { stack: "main;http_handler;parse_request;tokenize_headers", value: 38 },
  { stack: "main;http_handler;parse_request;decode_body;json_parse", value: 64 },
  { stack: "main;http_handler;parse_request;decode_body", value: 12 },
  { stack: "main;http_handler;route_match;regex_compile", value: 22 },
  { stack: "main;http_handler;route_match;regex_exec", value: 31 },
  { stack: "main;http_handler;auth_middleware;verify_jwt;hmac_sha256", value: 54 },
  { stack: "main;http_handler;auth_middleware;verify_jwt", value: 9 },
  { stack: "main;http_handler;auth_middleware;load_session;redis_get", value: 41 },
  { stack: "main;http_handler;dispatch;get_user;db_query;pool_checkout", value: 18 },
  { stack: "main;http_handler;dispatch;get_user;db_query;execute;deserialize_rows", value: 112 },
  { stack: "main;http_handler;dispatch;get_user;db_query;execute", value: 47 },
  { stack: "main;http_handler;dispatch;get_user;render_template;escape_html", value: 28 },
  { stack: "main;http_handler;dispatch;get_user;render_template", value: 11 },
  { stack: "main;http_handler;dispatch;post_order;validate;schema_check", value: 24 },
  { stack: "main;http_handler;dispatch;post_order;db_transaction;pool_checkout", value: 16 },
  { stack: "main;http_handler;dispatch;post_order;db_transaction;execute;deserialize_rows", value: 76 },
  { stack: "main;http_handler;dispatch;post_order;db_transaction;execute", value: 35 },
  { stack: "main;http_handler;dispatch;post_order;publish_event;kafka_send", value: 22 },
  { stack: "main;http_handler;serialize_response;json_encode", value: 44 },
  { stack: "main;http_handler;serialize_response", value: 8 },
  { stack: "main;worker;poll_queue;sqs_receive", value: 36 },
  { stack: "main;worker;process_job;load_payload;decompress;zstd_decode", value: 29 },
  { stack: "main;worker;process_job;load_payload", value: 8 },
  { stack: "main;worker;process_job;aggregate_metrics;groupby;hash_combine", value: 41 },
  { stack: "main;worker;process_job;aggregate_metrics;groupby", value: 9 },
  { stack: "main;worker;process_job;aggregate_metrics;compute_stats", value: 38 },
  { stack: "main;worker;process_job;persist_result;db_query;execute", value: 27 },
  { stack: "main;worker;process_job;persist_result;s3_put", value: 19 },
  { stack: "main;runtime_gc;mark_phase;trace_objects", value: 48 },
  { stack: "main;runtime_gc;mark_phase;rescan_remembered_set", value: 14 },
  { stack: "main;runtime_gc;sweep_phase;free_unreachable", value: 23 },
  { stack: "main;runtime_gc;sweep_phase;compact_arena", value: 11 },
];

// --- Build nested tree from semicolon-delimited stack paths ----------------
function buildTree(rows) {
  const root = { name: "main", children: [], _idx: {}, value: 0 };
  for (const { stack, value } of rows) {
    const parts = stack.split(";");
    let node = root;
    for (let i = 1; i < parts.length; i++) {
      const part = parts[i];
      if (!node._idx[part]) {
        const child = { name: part, children: [], _idx: {}, value: 0 };
        node._idx[part] = child;
        node.children.push(child);
      }
      node = node._idx[part];
    }
    node.value += value;
  }
  (function strip(n) {
    delete n._idx;
    if (!n.children.length) delete n.children;
    else n.children.forEach(strip);
  })(root);
  return root;
}

const treeRoot = buildTree(samples);

// d3.hierarchy + .sum: each node's value = self_value + Σ children.value.
// d3.partition then lays children horizontally inside the parent's range. When
// children's sum < parent.value (parent has self-time), the parent's bar on
// the row above shows a "gap" — that gap is the parent's self-time.
const rootH = d3.hierarchy(treeRoot)
  .sum((d) => d.value || 0)
  .sort((a, b) => b.value - a.value);

d3.partition().size([iw, 1])(rootH);

const nodes = rootH.descendants();
const maxDepth = d3.max(nodes, (d) => d.depth);
const rowGap = 3;
const rowH = Math.floor(ih / (maxDepth + 1));

// --- Color by top-level module (depth-1 ancestor) --------------------------
function topModule(node) {
  let n = node;
  while (n.depth > 1) n = n.parent;
  return n.data.name;
}

// Modules sorted by total samples (desc) so the biggest gets palette[0].
const modules = (rootH.children || [])
  .slice()
  .sort((a, b) => b.value - a.value)
  .map((c) => c.data.name);
const colorScale = d3.scaleOrdinal().domain(modules).range(t.palette);

// "main" root bar — outside the categorical pool; use the muted anchor.
const mutedAnchor = theme === "dark" ? "#A8A79F" : "#6B6A63";

// Per-fill text-color choice tuned for the Imprint palette (luminance-based
// alone misclassifies ochre and cyan at the edges).
const TEXT_DARK = "#1A1A17";
const TEXT_LIGHT = "#F0EFE8";
const FILL_TO_TEXT = {
  "#009E73": TEXT_LIGHT, // brand green
  "#C475FD": TEXT_DARK,  // lavender
  "#4467A3": TEXT_LIGHT, // blue
  "#BD8233": TEXT_LIGHT, // ochre
  "#AE3030": TEXT_LIGHT, // matte red
  "#2ABCCD": TEXT_DARK,  // cyan
  "#954477": TEXT_LIGHT, // rose
  "#99B314": TEXT_DARK,  // lime
};
const pickTextColor = (fill) => FILL_TO_TEXT[fill] || TEXT_LIGHT;

// --- SVG mount -------------------------------------------------------------
const svg = d3.select("#container").append("svg")
  .attr("width", width).attr("height", height);
const g = svg.append("g").attr("transform", `translate(${margin.left},${margin.top})`);

// --- Bars ------------------------------------------------------------------
const cell = g.selectAll(".cell").data(nodes).join("g")
  .attr("class", "cell")
  .attr("transform", (d) => {
    const x = d.x0;
    const y = ih - (d.depth + 1) * rowH;
    return `translate(${x},${y})`;
  });

cell.append("rect")
  .attr("width", (d) => Math.max(0, d.x1 - d.x0 - 1))
  .attr("height", rowH - rowGap)
  .attr("fill", (d) => d.depth === 0 ? mutedAnchor : colorScale(topModule(d)))
  .attr("stroke", t.pageBg)
  .attr("stroke-width", 0.8);

// Function-name labels — only rendered when the bar is wide enough to fit at
// least 3 characters at the chosen monospace font; otherwise omitted entirely.
const charW = 8.6;
const padX = 10;
const fontPx = 15;
cell.append("text")
  .attr("x", padX)
  .attr("y", (rowH - rowGap) / 2 + 1)
  .attr("dominant-baseline", "middle")
  .style("font-size", `${fontPx}px`)
  .style("font-family", "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace")
  .style("font-weight", "500")
  .attr("fill", (d) => pickTextColor(d.depth === 0 ? mutedAnchor : colorScale(topModule(d))))
  .text((d) => {
    const w = d.x1 - d.x0;
    const maxChars = Math.floor((w - 2 * padX) / charW);
    if (maxChars < 3) return "";
    const name = d.data.name;
    if (name.length <= maxChars) return name;
    return name.slice(0, maxChars - 1) + "…";
  });

// --- X axis (sample counts) ------------------------------------------------
const totalSamples = rootH.value;
const xScale = d3.scaleLinear().domain([0, totalSamples]).range([0, iw]);
const xAxisG = g.append("g")
  .attr("transform", `translate(0,${ih + 6})`)
  .call(d3.axisBottom(xScale).ticks(10).tickFormat(d3.format(",d")).tickSizeOuter(0));
xAxisG.selectAll("text").attr("fill", t.inkSoft).style("font-size", "14px");
xAxisG.selectAll("line").attr("stroke", t.inkSoft);
xAxisG.select(".domain").attr("stroke", t.inkSoft);

svg.append("text")
  .attr("x", margin.left + iw / 2)
  .attr("y", height - 18)
  .attr("text-anchor", "middle")
  .attr("fill", t.inkSoft).style("font-size", "15px")
  .text(`Cumulative CPU samples (${d3.format(",d")(totalSamples)} total) — bar width ∝ time in stack`);

// --- Module legend (centered, below title) ---------------------------------
const swatchW = 24, swatchH = 16, swatchGap = 12, itemGap = 56;
// Pre-measure each label so the legend can be centered tightly.
const tmpText = svg.append("text").style("font-size", "16px").style("visibility", "hidden");
const measure = (s) => { tmpText.text(s); return tmpText.node().getComputedTextLength(); };
const itemWidths = modules.map((m) => swatchW + swatchGap + measure(m));
tmpText.remove();
const legendTotalW = itemWidths.reduce((a, b) => a + b, 0) + itemGap * (modules.length - 1);
let cursor = Math.round((width - legendTotalW) / 2);
const legendY = 78;

modules.forEach((m, i) => {
  const grp = svg.append("g").attr("transform", `translate(${cursor},${legendY})`);
  grp.append("rect")
    .attr("x", 0).attr("y", -12)
    .attr("width", swatchW).attr("height", swatchH)
    .attr("fill", colorScale(m));
  grp.append("text")
    .attr("x", swatchW + swatchGap).attr("y", 0)
    .attr("fill", t.inkSoft).style("font-size", "16px")
    .text(m);
  cursor += itemWidths[i] + itemGap;
});

// --- Title -----------------------------------------------------------------
svg.append("text")
  .attr("x", width / 2).attr("y", 44)
  .attr("text-anchor", "middle")
  .attr("fill", t.ink).style("font-size", "22px").style("font-weight", "600")
  .text("flamegraph-basic · javascript · d3 · anyplot.ai");

Retrieve this implementation

Runnable source as JSON, for any HTTP client: https://api.anyplot.ai/specs/flamegraph-basic/d3/code. Any spec id and library id listed in llms-full.txt fit the same URL shape; every URL below is complete and callable.

{
  "spec_id": "flamegraph-basic",
  "language": "javascript",
  "library": "d3",
  "page": "https://anyplot.ai/flamegraph-basic/javascript/d3",
  "hub": "https://anyplot.ai/flamegraph-basic",
  "code_json": "https://api.anyplot.ai/specs/flamegraph-basic/d3/code",
  "spec_json": "https://api.anyplot.ai/specs/flamegraph-basic",
  "render_light_png": "https://storage.googleapis.com/anyplot-images/plots/flamegraph-basic/javascript/d3/plot-light.png",
  "render_dark_png": "https://storage.googleapis.com/anyplot-images/plots/flamegraph-basic/javascript/d3/plot-dark.png",
  "interactive_light_html": "https://storage.googleapis.com/anyplot-images/plots/flamegraph-basic/javascript/d3/plot-light.html",
  "interactive_dark_html": "https://storage.googleapis.com/anyplot-images/plots/flamegraph-basic/javascript/d3/plot-dark.html",
  "quality_score": 90.0,
  "license": "MIT",
  "guide": "https://anyplot.ai/llms.txt"
}

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

Other implementations