diff --git a/web/public/bundle.ts b/web/public/bundle.ts new file mode 100644 index 0000000..28b3820 --- /dev/null +++ b/web/public/bundle.ts @@ -0,0 +1,12 @@ +import * as esbuild from "npm:esbuild"; +import { denoPlugins } from "jsr:@luca/esbuild-deno-loader"; + +await esbuild.build({ + plugins: [...denoPlugins()], + entryPoints: ["./src/index.ts"], + outfile: "./dist/bundle.js", + bundle: true, + format: "esm", +}); + +esbuild.stop(); diff --git a/web/public/deno.jsonc b/web/public/deno.jsonc index 56e91e7..801f699 100644 --- a/web/public/deno.jsonc +++ b/web/public/deno.jsonc @@ -1,7 +1,6 @@ { - "compilerOptions": { - "checkJs": false, - "lib": ["dom", "dom.iterable", "dom.asynciterable"], + "compilerOptions": { + "lib": ["dom", "dom.iterable", "dom.asynciterable"] }, "fmt": { "indentWidth": 4 diff --git a/web/public/deno.lock b/web/public/deno.lock new file mode 100644 index 0000000..51942a2 --- /dev/null +++ b/web/public/deno.lock @@ -0,0 +1,129 @@ +{ + "version": "4", + "specifiers": { + "jsr:@luca/esbuild-deno-loader@*": "0.11.0", + "jsr:@std/bytes@^1.0.2": "1.0.4", + "jsr:@std/encoding@^1.0.5": "1.0.5", + "jsr:@std/path@^1.0.6": "1.0.8", + "npm:esbuild@*": "0.20.2", + "npm:esbuild@0.20.2": "0.20.2" + }, + "jsr": { + "@luca/esbuild-deno-loader@0.11.0": { + "integrity": "c05a989aa7c4ee6992a27be5f15cfc5be12834cab7ff84cabb47313737c51a2c", + "dependencies": [ + "jsr:@std/bytes", + "jsr:@std/encoding", + "jsr:@std/path" + ] + }, + "@std/bytes@1.0.4": { + "integrity": "11a0debe522707c95c7b7ef89b478c13fb1583a7cfb9a85674cd2cc2e3a28abc" + }, + "@std/encoding@1.0.5": { + "integrity": "ecf363d4fc25bd85bd915ff6733a7e79b67e0e7806334af15f4645c569fefc04" + }, + "@std/path@1.0.8": { + "integrity": "548fa456bb6a04d3c1a1e7477986b6cffbce95102d0bb447c67c4ee70e0364be" + } + }, + "npm": { + "@esbuild/aix-ppc64@0.20.2": { + "integrity": "sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==" + }, + "@esbuild/android-arm64@0.20.2": { + "integrity": "sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==" + }, + "@esbuild/android-arm@0.20.2": { + "integrity": "sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==" + }, + "@esbuild/android-x64@0.20.2": { + "integrity": "sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==" + }, + "@esbuild/darwin-arm64@0.20.2": { + "integrity": "sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==" + }, + "@esbuild/darwin-x64@0.20.2": { + "integrity": "sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==" + }, + "@esbuild/freebsd-arm64@0.20.2": { + "integrity": "sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==" + }, + "@esbuild/freebsd-x64@0.20.2": { + "integrity": "sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==" + }, + "@esbuild/linux-arm64@0.20.2": { + "integrity": "sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==" + }, + "@esbuild/linux-arm@0.20.2": { + "integrity": "sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==" + }, + "@esbuild/linux-ia32@0.20.2": { + "integrity": "sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==" + }, + "@esbuild/linux-loong64@0.20.2": { + "integrity": "sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==" + }, + "@esbuild/linux-mips64el@0.20.2": { + "integrity": "sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==" + }, + "@esbuild/linux-ppc64@0.20.2": { + "integrity": "sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==" + }, + "@esbuild/linux-riscv64@0.20.2": { + "integrity": "sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==" + }, + "@esbuild/linux-s390x@0.20.2": { + "integrity": "sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==" + }, + "@esbuild/linux-x64@0.20.2": { + "integrity": "sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==" + }, + "@esbuild/netbsd-x64@0.20.2": { + "integrity": "sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==" + }, + "@esbuild/openbsd-x64@0.20.2": { + "integrity": "sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==" + }, + "@esbuild/sunos-x64@0.20.2": { + "integrity": "sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==" + }, + "@esbuild/win32-arm64@0.20.2": { + "integrity": "sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==" + }, + "@esbuild/win32-ia32@0.20.2": { + "integrity": "sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==" + }, + "@esbuild/win32-x64@0.20.2": { + "integrity": "sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==" + }, + "esbuild@0.20.2": { + "integrity": "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==", + "dependencies": [ + "@esbuild/aix-ppc64", + "@esbuild/android-arm", + "@esbuild/android-arm64", + "@esbuild/android-x64", + "@esbuild/darwin-arm64", + "@esbuild/darwin-x64", + "@esbuild/freebsd-arm64", + "@esbuild/freebsd-x64", + "@esbuild/linux-arm", + "@esbuild/linux-arm64", + "@esbuild/linux-ia32", + "@esbuild/linux-loong64", + "@esbuild/linux-mips64el", + "@esbuild/linux-ppc64", + "@esbuild/linux-riscv64", + "@esbuild/linux-s390x", + "@esbuild/linux-x64", + "@esbuild/netbsd-x64", + "@esbuild/openbsd-x64", + "@esbuild/sunos-x64", + "@esbuild/win32-arm64", + "@esbuild/win32-ia32", + "@esbuild/win32-x64" + ] + } + } +} diff --git a/web/public/dist/bundle.js b/web/public/dist/bundle.js new file mode 100644 index 0000000..e7c790d --- /dev/null +++ b/web/public/dist/bundle.js @@ -0,0 +1,214 @@ +// 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: "