{"spec_id":"flamegraph-basic","library":"chartjs","language":"javascript","code":"// anyplot.ai\n// flamegraph-basic: Flame Graph for Performance Profiling\n// Library: chartjs 4.4.7 | JavaScript 22.22.3\n// Quality: 93/100 | Created: 2026-06-08\n//# anyplot-orientation: landscape\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data: simulated CPU profile of a web API request handler --------------\n// Tree of call stacks. `self` = samples spent in the function itself (not in\n// its children). A node's *total* = self + sum(children.total) and becomes the\n// width of its flame-graph bar. Children sit above the parent and fill it\n// left-to-right; any leftover width at depth+1 is the parent's `self` time.\nconst profile = {\n  name: \"main\", self: 1, children: [\n    { name: \"runServer\", self: 2, children: [\n      { name: \"handleRequest\", self: 4, children: [\n        { name: \"parseHeaders\", self: 3, children: [\n          { name: \"decodeUtf8\", self: 12, children: [] },\n          { name: \"lowercaseKeys\", self: 8, children: [] },\n          { name: \"splitCookies\", self: 6, children: [] },\n        ] },\n        { name: \"routeRequest\", self: 2, children: [\n          { name: \"authMiddleware\", self: 3, children: [\n            { name: \"verifyToken\", self: 4, children: [\n              { name: \"parseJwt\", self: 8, children: [] },\n              { name: \"validateSig\", self: 14, children: [] },\n            ] },\n            { name: \"loadUser\", self: 3, children: [\n              { name: \"cacheGet\", self: 4, children: [] },\n              { name: \"dbFetch\", self: 22, children: [] },\n            ] },\n          ] },\n          { name: \"rateLimitCheck\", self: 2, children: [\n            { name: \"bucketLookup\", self: 6, children: [] },\n            { name: \"bucketUpdate\", self: 8, children: [] },\n          ] },\n          { name: \"dispatchHandler\", self: 4, children: [\n            { name: \"queryDB\", self: 5, children: [\n              { name: \"openConn\", self: 10, children: [] },\n              { name: \"executeQuery\", self: 92, children: [] },\n              { name: \"parseRows\", self: 18, children: [] },\n              { name: \"closeConn\", self: 6, children: [] },\n            ] },\n            { name: \"renderTemplate\", self: 3, children: [\n              { name: \"loadTemplate\", self: 10, children: [] },\n              { name: \"compileTemplate\", self: 28, children: [] },\n              { name: \"renderHTML\", self: 24, children: [] },\n              { name: \"escapeHTML\", self: 12, children: [] },\n            ] },\n            { name: \"serialize\", self: 4, children: [\n              { name: \"jsonStringify\", self: 16, children: [] },\n              { name: \"gzipCompress\", self: 22, children: [] },\n            ] },\n          ] },\n        ] },\n        { name: \"writeResponse\", self: 3, children: [\n          { name: \"setHeaders\", self: 6, children: [] },\n          { name: \"flushBuffer\", self: 14, children: [] },\n        ] },\n      ] },\n      { name: \"gcMinor\", self: 6, children: [\n        { name: \"markRefs\", self: 10, children: [] },\n        { name: \"sweepHeap\", self: 14, children: [] },\n      ] },\n      { name: \"logRequest\", self: 2, children: [\n        { name: \"formatLog\", self: 4, children: [] },\n        { name: \"writeLog\", self: 8, children: [] },\n      ] },\n    ] },\n    { name: \"backgroundJobs\", self: 2, children: [\n      { name: \"cronTick\", self: 3, children: [\n        { name: \"scanJobs\", self: 6, children: [] },\n        { name: \"claimJob\", self: 4, children: [] },\n      ] },\n      { name: \"workerLoop\", self: 3, children: [\n        { name: \"fetchJob\", self: 8, children: [] },\n        { name: \"runJob\", self: 4, children: [\n          { name: \"emailSend\", self: 18, children: [] },\n          { name: \"imageResize\", self: 3, children: [\n            { name: \"decodeImg\", self: 12, children: [] },\n            { name: \"resampleImg\", self: 26, children: [] },\n            { name: \"encodeImg\", self: 14, children: [] },\n          ] },\n          { name: \"dataExport\", self: 18, children: [] },\n        ] },\n      ] },\n    ] },\n  ],\n};\n\n// Flatten the tree into (depth, start, end, total, self) frames via DFS.\nconst frames = [];\nlet maxDepth = 0;\nfunction visit(node, depth, x) {\n  if (depth > maxDepth) maxDepth = depth;\n  let childX = x;\n  let kidsTotal = 0;\n  for (const child of node.children) {\n    const ct = visit(child, depth + 1, childX);\n    childX += ct;\n    kidsTotal += ct;\n  }\n  const total = node.self + kidsTotal;\n  frames.push({ name: node.name, depth, start: x, end: x + total, total, self: node.self });\n  return total;\n}\nconst totalSamples = visit(profile, 0, 0);\n\n// --- Warm palette tiered by self-samples (hotness encoding) ----------------\n// Imprint's three warm anchors map to a self-time tier so the eye lands on\n// the actual hotspot (the matte-red frame) instead of color being decorative.\n// Dark text rides on the lighter amber/ochre tiers; light text on matte red.\n// Thresholds: low ≤10 (cool amber), medium ≤24 (ochre), high >24 (matte red).\nfunction colorFor(self) {\n  if (self > 24) return { fill: \"#AE3030\", text: \"#FFFDF6\" };\n  if (self > 10) return { fill: \"#BD8233\", text: \"#1A1A17\" };\n  return { fill: \"#DDCC77\", text: \"#1A1A17\" };\n}\n\n// --- Mount -----------------------------------------------------------------\nconst canvas = document.createElement(\"canvas\");\ndocument.getElementById(\"container\").appendChild(canvas);\n\n// --- Custom flame-graph renderer ------------------------------------------\n// Chart.js core has no flame-graph type, so we run a `type: 'bar'` chart for\n// its axes / title chrome and draw the per-frame rectangles ourselves in an\n// `afterDatasetsDraw` plugin. y-pixel positions are computed from chartArea\n// rather than the category scale so reverse / band alignment stay exact.\nconst flamePlugin = {\n  id: \"flamegraph\",\n  afterDatasetsDraw(chart) {\n    const { ctx, chartArea, scales: { x } } = chart;\n    const rows = maxDepth + 1;\n    const bandPx = (chartArea.bottom - chartArea.top) / rows;\n    const gap = 2;\n    const barH = Math.max(2, bandPx - gap);\n\n    ctx.save();\n    ctx.font = \"600 14px -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif\";\n    ctx.textBaseline = \"middle\";\n\n    for (const f of frames) {\n      const xLeft = x.getPixelForValue(f.start);\n      const xRight = x.getPixelForValue(f.end);\n      // depth 0 at the bottom band; depth N at the top.\n      const yCenter = chartArea.bottom - (f.depth + 0.5) * bandPx;\n      const top = yCenter - barH / 2;\n      const w = Math.max(1, xRight - xLeft - 1);\n\n      const { fill, text } = colorFor(f.self);\n      ctx.fillStyle = fill;\n      ctx.fillRect(xLeft, top, w, barH);\n      // pageBg-coloured hairline separates adjacent siblings on both themes.\n      ctx.strokeStyle = t.pageBg;\n      ctx.lineWidth = 1;\n      ctx.strokeRect(xLeft + 0.5, top + 0.5, w - 1, barH - 1);\n\n      if (w >= 60) {\n        ctx.fillStyle = text;\n        let label = f.name;\n        const maxText = w - 12;\n        if (ctx.measureText(label).width > maxText) {\n          while (label.length > 2 && ctx.measureText(label + \"…\").width > maxText) {\n            label = label.slice(0, -1);\n          }\n          label = label + \"…\";\n        }\n        ctx.fillText(label, xLeft + 6, yCenter);\n      }\n    }\n    ctx.restore();\n  },\n};\n\n// --- Chart -----------------------------------------------------------------\nconst depthLabels = Array.from({ length: maxDepth + 1 }, (_, i) => String(i));\n\nnew Chart(canvas, {\n  type: \"bar\",\n  data: {\n    labels: depthLabels,\n    datasets: [{\n      label: \"Stack frames\",\n      data: depthLabels.map(() => 0), // placeholder; real bars are drawn by the plugin\n      backgroundColor: \"rgba(0,0,0,0)\",\n      borderColor: \"rgba(0,0,0,0)\",\n    }],\n  },\n  options: {\n    indexAxis: \"y\",\n    responsive: true,\n    maintainAspectRatio: false,\n    animation: false,\n    layout: { padding: { left: 8, right: 18, top: 4, bottom: 8 } },\n    plugins: {\n      title: {\n        display: true,\n        text: \"flamegraph-basic · javascript · chartjs · anyplot.ai\",\n        color: t.ink,\n        font: { size: 22, weight: \"600\" },\n        padding: { top: 4, bottom: 6 },\n      },\n      subtitle: {\n        display: true,\n        text: \"Simulated CPU profile · bar width = samples · color = self-time tier · horizontal order is arbitrary, not temporal\",\n        color: t.inkSoft,\n        font: { size: 14, style: \"italic\" },\n        padding: { bottom: 16 },\n      },\n      legend: { display: false },\n      tooltip: { enabled: false },\n    },\n    scales: {\n      x: {\n        type: \"linear\",\n        min: 0,\n        max: totalSamples,\n        title: {\n          display: true,\n          text: \"Samples\",\n          color: t.ink,\n          font: { size: 14 },\n          padding: { top: 8 },\n        },\n        ticks: { color: t.inkSoft, font: { size: 12 } },\n        grid: { color: t.grid, drawTicks: false },\n        border: { color: t.grid },\n      },\n      y: {\n        type: \"category\",\n        labels: depthLabels,\n        reverse: true, // depth 0 (root) at the bottom\n        title: {\n          display: true,\n          text: \"Call stack depth  (root → leaf)\",\n          color: t.ink,\n          font: { size: 14 },\n        },\n        // Hide numeric tick labels — visible bar stacking + axis title already\n        // convey depth; numeric ticks would just add chrome noise.\n        ticks: { display: false },\n        grid: { display: false, drawTicks: false },\n        border: { color: t.grid },\n      },\n    },\n  },\n  plugins: [flamePlugin],\n});\n"}