// src/index.ts var codeCoverageDiv = document.querySelector("#code-coverage"); var flameGraphDiv = document.querySelector("#flame-graph"); function drawText(text, codeCoverageData) { const tooltip = document.getElementById("covers-tooltip"); const entries = codeCoverageData.toSorted((a, b) => b.index - a.index); const charEntries = {}; const elements = []; let line = 1; let col = 1; for (let index = 0; index < text.length; ++index) { if (text[index] == "\n") { col = 1; line += 1; elements.push("\n"); continue; } const entry = entries.find((entry2) => index >= entry2.index); charEntries[`${line}-${col}`] = entry; const color = (ratio) => `rgba(${255 - 255 * ratio}, ${255 * ratio}, 125, 0.5)`; const span = document.createElement("span"); span.style.backgroundColor = color(Math.min(entry.covers / 25, 1)); span.innerText = text[index]; span.dataset.covers = entry.covers; elements.push(span); col += 1; } function positionInBox(position, boundingRect) { const [x, y] = position; const outside = x < boundingRect.left || x >= boundingRect.right || y < boundingRect.top || y >= boundingRect.bottom; return !outside; } testytestytesty.append(...elements); document.addEventListener("mousemove", (event) => { const [x, y] = [event.clientX, event.clientY]; const outerBox = testytestytesty.getBoundingClientRect(); if (!positionInBox([x, y], outerBox)) { console.log("+"); return; } const element = elements.find((element2) => { if (!element2.dataset?.covers) { return false; } const isIn = positionInBox([x, y], element2.getBoundingClientRect()); return isIn; }); if (!element) { tooltip.hidden = true; return; } const covers = parseInt(element.dataset.covers); tooltip.hidden = false; tooltip.style.left = `${event.clientX + 20}px`; tooltip.style.top = `${event.clientY + 20}px`; tooltip.innerText = `Ran ${covers} time${covers !== 1 ? "s" : ""}`; }); } function loadCodeCoverage(text, codeCoverageData) { codeCoverageDiv.innerHTML = `
${text}
`; const canvas = document.querySelector("#code-coverage-canvas"); canvas.width = 1e3; canvas.height = 500; const ctx = canvas.getContext("2d"); ctx.font = "20px monospace"; const { width: chWidth } = ctx.measureText("-"); const chHeight = 23; const color = (ratio) => `rgba(${255 - 255 * ratio}, ${255 * ratio}, 125, 0.5)`; const entries = codeCoverageData.toSorted((a, b) => b.index - a.index); const charEntries = {}; let line = 1; let col = 1; for (let index = 0; index < text.length; ++index) { if (text[index] == "\n") { col = 1; line += 1; continue; } const entry = entries.find((entry2) => index >= entry2.index); charEntries[`${line}-${col}`] = entry; ctx.fillStyle = color(Math.min(entry.covers / 25, 1)); ctx.fillRect( (col - 1) * chWidth, (line - 1) * chHeight, chWidth, chHeight ); col += 1; } const tooltip = document.getElementById("covers-tooltip"); canvas.addEventListener("mousemove", (e) => { const col2 = Math.floor(e.offsetX / chWidth + 1); const line2 = Math.floor(e.offsetY / chHeight + 1); const key = `${line2}-${col2}`; if (!(key in charEntries)) { tooltip.hidden = true; return; } const entry = charEntries[key]; tooltip.innerText = `Ran ${entry.covers} time${entry.covers !== 1 ? "s" : ""}`; tooltip.style.left = `${e.clientX + 20}px`; tooltip.style.top = `${e.clientY + 20}px`; tooltip.hidden = false; }); canvas.addEventListener("mouseleave", () => { tooltip.hidden = true; }); } function loadFlameGraph(flameGraphData, fnNames) { flameGraphDiv.innerHTML = ` `; const canvas = document.querySelector("#flame-graph-canvas"); canvas.width = 1e3; canvas.height = 500; const ctx = canvas.getContext("2d"); ctx.font = "16px monospace"; const nodes = []; function calculateNodeRects(node, depth, totalAcc, offsetAcc) { 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 = fnNames[node.fn]; const percent = `${(node.acc / totalAcc * 100).toFixed(1)}%`; nodes.push({ x, y, w, h, title, percent }); 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(child, depth + 1, totalAcc, newOffsetAcc); newOffsetAcc += child.acc; } } calculateNodeRects(flameGraphData, 0, flameGraphData.acc, 0); for (const node of nodes) { const { x, y, w, h, title } = node; ctx.fillStyle = "rgb(255, 125, 0)"; ctx.fillRect( x + 1, y + 1, w - 2, h - 2 ); ctx.fillStyle = "black"; ctx.fillText( title, x + (w - 10) / 2 - ctx.measureText(title).width / 2 + 5, y + 20 ); } const tooltip = document.getElementById("flame-graph-tooltip"); canvas.addEventListener("mousemove", (e) => { const x = e.offsetX; const y = e.offsetY; const node = nodes.find( (node2) => x >= node2.x && x < node2.x + node2.w && y >= node2.y && y < node2.y + node2.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; }); canvas.addEventListener("mouseleave", () => { tooltip.hidden = true; }); } var codeData = `fn add(a, b) { + a b } let result = 0; let i = 0; loop { if >= i 10 { break; } result = add(result, 5); i = + i 1; } `; function main() { const codeCoverageData = JSON.parse( `[{"index":0,"line":1,"col":1,"covers":2},{"index":28,"line":5,"col":1,"covers":1},{"index":44,"line":6,"col":1,"covers":1},{"index":55,"line":7,"col":1,"covers":1},{"index":66,"line":8,"col":5,"covers":11},{"index":104,"line":11,"col":5,"covers":10},{"index":19,"line":2,"col":5,"covers":10},{"index":133,"line":12,"col":5,"covers":10},{"index":87,"line":9,"col":9,"covers":1}]` ); const flameGraphData = JSON.parse( `{"fn":0,"acc":257,"parent":0,"children":[{"fn":18,"acc":251,"parent":0,"children":[{"fn":12,"acc":30,"parent":1,"children":[]}]}]}` ); const viewRadios = document.querySelectorAll('input[name="views"]'); for (const input of viewRadios) { input.addEventListener("input", (ev) => { console.log(ev); }); } loadCodeCoverage(codeData, codeCoverageData); loadFlameGraph(flameGraphData, { 0: "", 12: "add", 18: "main" }); drawText(codeData, codeCoverageData); } main();