import * as data from "./data.ts"; export function loadFlameGraph( flameGraphData: data.FlameGraphNode, fnNames: data.FlameGraphFnNames, flameGraphDiv: HTMLDivElement, ) { flameGraphDiv.innerHTML = `
`; const canvas = document.querySelector( "#flame-graph-canvas", )!; const resetButton = document.querySelector( "#flame-graph-reset", )!; canvas.width = 1000; canvas.height = 500; const fnNameFont = "600 14px 'Roboto Mono'"; const ctx = canvas.getContext("2d")!; ctx.font = fnNameFont; type CalcNode = { x: number; y: number; w: number; h: number; title: string; percent: string; fgNode: data.FlameGraphNode; }; function calculateNodeRects( nodes: CalcNode[], node: data.FlameGraphNode, depth: number, totalAcc: data.FlameGraphNode["acc"], offsetAcc: data.FlameGraphNode["acc"], ) { const x = (offsetAcc / totalAcc) * canvas.width; const y = canvas.height - 30 * depth - 30; const w = ((node.acc + 1) / totalAcc) * canvas.width; const h = 30; const title = node.fn == 0 ? "" : fnNames[node.fn] ?? ""; const percent = `${(node.acc / totalAcc * 100).toFixed(1)}%`; nodes.push({ x, y, w, h, title, percent, fgNode: node }); const totalChildrenAcc = node.children.reduce( (acc, child) => acc + child.acc, 0, ); let newOffsetAcc = offsetAcc + (node.acc - totalChildrenAcc) / 2; for (const child of node.children) { calculateNodeRects(nodes, child, depth + 1, totalAcc, newOffsetAcc); newOffsetAcc += child.acc; } } function drawTextCanvas(node: CalcNode): HTMLCanvasElement { const { w, h, title } = node; const textCanvas = document.createElement("canvas"); textCanvas.width = Math.max(w - 8, 1); textCanvas.height = h; const textCtx = textCanvas.getContext("2d")!; textCtx.font = fnNameFont; textCtx.fillStyle = "black"; textCtx.fillText( title, ((w - 10) / 2 - ctx.measureText(title).width / 2) + 5 - 4, 20, ); return textCanvas; } function renderNodes(nodes: CalcNode[]) { for (const node of nodes) { const { x, y, w, h } = node; ctx.fillStyle = "rgb(227, 178, 60)"; ctx.fillRect(x + 2, y + 2, w - 4, h - 4); const textCanvas = drawTextCanvas(node); ctx.drawImage(textCanvas, x + 4, y); const edgePadding = 4; const edgeWidth = 8; const leftGradient = ctx.createLinearGradient( x + 2 + edgePadding, 0, x + 2 + edgeWidth, 0, ); leftGradient.addColorStop(1, "rgba(227, 178, 60, 0.0)"); leftGradient.addColorStop(0, "rgba(227, 178, 60, 1.0)"); ctx.fillStyle = leftGradient; ctx.fillRect(x + 2, y + 2, Math.min(edgeWidth, (w - 4) / 2), h - 4); const rightGradient = ctx.createLinearGradient( x + w - 2 - edgeWidth, 0, x + w - 2 - edgePadding, 0, ); rightGradient.addColorStop(0, "rgba(227, 178, 60, 0.0)"); rightGradient.addColorStop(1, "rgba(227, 178, 60, 1.0)"); ctx.fillStyle = rightGradient; ctx.fillRect( x + w - 2 - Math.min(edgeWidth, (w - 4) / 2), y + 2, Math.min(edgeWidth, (w - 4) / 2), h - 4, ); } const tooltip = document.getElementById("flame-graph-tooltip")!; const mousemoveEvent = (e: MouseEvent) => { const x = e.offsetX; const y = e.offsetY; const node = nodes.find((node) => x >= node.x && x < node.x + node.w && y >= node.y && y < node.y + node.h ); if (!node) { tooltip.hidden = true; return; } tooltip.innerText = `${node.title} ${node.percent}`; tooltip.style.left = `${e.clientX + 20}px`; tooltip.style.top = `${e.clientY + 20}px`; tooltip.hidden = false; }; const mouseleaveEvent = () => { tooltip.hidden = true; }; const mousedownEvent = (e: MouseEvent) => { const x = e.offsetX; const y = e.offsetY; const node = nodes.find((node) => x >= node.x && x < node.x + node.w && y >= node.y && y < node.y + node.h ); if (!node) { return; } tooltip.hidden = true; const newNodes: CalcNode[] = []; calculateNodeRects( newNodes, node.fgNode, 0, node.fgNode.acc, 0, ); canvas.removeEventListener("mousemove", mousemoveEvent); canvas.removeEventListener("mouseleave", mouseleaveEvent); canvas.removeEventListener("mousedown", mousedownEvent); ctx.clearRect(0, 0, canvas.width, canvas.height); renderNodes(newNodes); }; canvas.addEventListener("mousemove", mousemoveEvent); canvas.addEventListener("mouseleave", mouseleaveEvent); canvas.addEventListener("mousedown", mousedownEvent); } resetButton.addEventListener("click", () => { const nodes: CalcNode[] = []; calculateNodeRects(nodes, flameGraphData, 0, flameGraphData.acc, 0); renderNodes(nodes); }); const nodes: CalcNode[] = []; calculateNodeRects(nodes, flameGraphData, 0, flameGraphData.acc, 0); renderNodes(nodes); }