From 4f9a671bdc1d4e6c25397eb23f523a5849e013a1 Mon Sep 17 00:00:00 2001 From: sfja Date: Thu, 21 Nov 2024 04:12:07 +0100 Subject: [PATCH] add web --- runtime/json.hpp | 9 ++- runtime/main.cpp | 2 + runtime/to_json.cpp | 2 +- runtime/vm.cpp | 2 + runtime/vm.hpp | 15 ++++ web/deno.lock | 162 ++++++++++++++++++++------------------ web/main.ts | 1 + web/public/deno.jsonc | 9 +++ web/public/index.html | 19 +++-- web/public/index.js | 179 ++++++++++++++++++++++++++++++++++++++++++ web/public/style.css | 89 +++++++++++++++++++++ 11 files changed, 401 insertions(+), 88 deletions(-) create mode 100644 web/public/deno.jsonc create mode 100644 web/public/index.js create mode 100644 web/public/style.css diff --git a/runtime/json.hpp b/runtime/json.hpp index bb34d0e..a340232 100644 --- a/runtime/json.hpp +++ b/runtime/json.hpp @@ -295,8 +295,8 @@ public: auto parse_val() -> Res>; private: - inline auto unexpected_tok_err(TokTyp expected, std::string_view msg) - -> Res> + inline auto unexpected_tok_err( + TokTyp expected, std::string_view msg) -> Res> { return Err { .pos = this->cur.val().pos, @@ -339,7 +339,12 @@ public: template auto add_comma_seperated(const T& values, F f) { + auto first = true; for (const auto& value : values) { + if (!first) { + add(","); + } + first = false; f(*this, value); } } diff --git a/runtime/main.cpp b/runtime/main.cpp index 5f2d29d..5ac7f73 100644 --- a/runtime/main.cpp +++ b/runtime/main.cpp @@ -174,4 +174,6 @@ int main() std::cout << std::format("done\n{}\n", vm.stack_repr_string(4)); auto flame_graph = vm.flame_graph_json(); std::cout << std::format("flame graph: {}\n", flame_graph); + auto code_coverage = vm.code_coverage_json(); + std::cout << std::format("code coverage: {}\n", code_coverage); } diff --git a/runtime/to_json.cpp b/runtime/to_json.cpp index 09e35cb..41f0589 100644 --- a/runtime/to_json.cpp +++ b/runtime/to_json.cpp @@ -15,7 +15,7 @@ void FlameGraphBuilder::fg_node_to_json( writer << "{\"fn\":" << std::to_string(node.fn) << ",\"acc\":" << std::to_string(node.acc) << ",\"parent\":" << std::to_string(node.parent) - << ",\"children:[\""; + << ",\"children\":["; auto first = true; for (auto child_index : node.children) { if (!first) { diff --git a/runtime/vm.cpp b/runtime/vm.cpp index da33055..d24c279 100644 --- a/runtime/vm.cpp +++ b/runtime/vm.cpp @@ -73,6 +73,7 @@ void VM::run_until_done() while (!done()) { run_instruction(); } + this->flame_graph.calculate_midway_result(this->instruction_counter); } void VM::run_n_instructions(size_t amount) @@ -81,6 +82,7 @@ void VM::run_n_instructions(size_t amount) run_instruction(); } + this->flame_graph.calculate_midway_result(this->instruction_counter); } void VM::run_instruction() diff --git a/runtime/vm.hpp b/runtime/vm.hpp index d6d3b82..0fadee4 100644 --- a/runtime/vm.hpp +++ b/runtime/vm.hpp @@ -56,6 +56,11 @@ public: this->current = this->nodes[this->current].parent; } + inline void calculate_midway_result(int64_t ic) + { + calculate_node_midway_result(ic, this->current); + } + void to_json(json::Writer& writer) const override; private: @@ -80,6 +85,16 @@ private: return {}; } + inline void calculate_node_midway_result(int64_t ic, size_t node_index) + { + int64_t diff = ic - this->nodes[this->current].ic_start; + this->nodes[this->current].acc += diff; + this->nodes[this->current].ic_start = ic; + if (node_index == 0) + return; + calculate_node_midway_result(ic, this->nodes[this->current].parent); + } + void fg_node_to_json(json::Writer& writer, size_t node_index) const; std::vector nodes = { FGNode(0, 0) }; diff --git a/web/deno.lock b/web/deno.lock index 0b86c04..cdf7a03 100644 --- a/web/deno.lock +++ b/web/deno.lock @@ -1,84 +1,90 @@ { - "version": "3", - "packages": { - "specifiers": { - "jsr:@oak/commons@^1.0": "jsr:@oak/commons@1.0.0", - "jsr:@oak/oak": "jsr:@oak/oak@17.1.3", - "jsr:@std/assert@^1.0": "jsr:@std/assert@1.0.8", - "jsr:@std/bytes@^1.0": "jsr:@std/bytes@1.0.4", - "jsr:@std/bytes@^1.0.2": "jsr:@std/bytes@1.0.4", - "jsr:@std/crypto@^1.0": "jsr:@std/crypto@1.0.3", - "jsr:@std/encoding@^1.0": "jsr:@std/encoding@1.0.5", - "jsr:@std/encoding@^1.0.5": "jsr:@std/encoding@1.0.5", - "jsr:@std/http@^1.0": "jsr:@std/http@1.0.10", - "jsr:@std/io@0.224": "jsr:@std/io@0.224.9", - "jsr:@std/media-types@^1.0": "jsr:@std/media-types@1.1.0", - "jsr:@std/path@^1.0": "jsr:@std/path@1.0.8", - "npm:path-to-regexp@6.2.1": "npm:path-to-regexp@6.2.1" + "version": "4", + "specifiers": { + "jsr:@oak/commons@1": "1.0.0", + "jsr:@oak/oak@*": "17.1.3", + "jsr:@std/assert@1": "1.0.8", + "jsr:@std/bytes@1": "1.0.4", + "jsr:@std/bytes@^1.0.2": "1.0.4", + "jsr:@std/crypto@1": "1.0.3", + "jsr:@std/encoding@1": "1.0.5", + "jsr:@std/encoding@^1.0.5": "1.0.5", + "jsr:@std/http@1": "1.0.10", + "jsr:@std/io@0.224": "0.224.9", + "jsr:@std/media-types@1": "1.1.0", + "jsr:@std/path@1": "1.0.8", + "npm:@types/node@*": "22.5.4", + "npm:path-to-regexp@6.2.1": "6.2.1" + }, + "jsr": { + "@oak/commons@1.0.0": { + "integrity": "49805b55603c3627a9d6235c0655aa2b6222d3036b3a13ff0380c16368f607ac", + "dependencies": [ + "jsr:@std/assert", + "jsr:@std/bytes@1", + "jsr:@std/crypto", + "jsr:@std/encoding@1", + "jsr:@std/http", + "jsr:@std/media-types" + ] }, - "jsr": { - "@oak/commons@1.0.0": { - "integrity": "49805b55603c3627a9d6235c0655aa2b6222d3036b3a13ff0380c16368f607ac", - "dependencies": [ - "jsr:@std/assert@^1.0", - "jsr:@std/bytes@^1.0", - "jsr:@std/crypto@^1.0", - "jsr:@std/encoding@^1.0", - "jsr:@std/http@^1.0", - "jsr:@std/media-types@^1.0" - ] - }, - "@oak/oak@17.1.3": { - "integrity": "d89296c22db91681dd3a2a1e1fd14e258d0d5a9654de55637aee5b661c159f33", - "dependencies": [ - "jsr:@oak/commons@^1.0", - "jsr:@std/assert@^1.0", - "jsr:@std/bytes@^1.0", - "jsr:@std/crypto@^1.0", - "jsr:@std/http@^1.0", - "jsr:@std/io@0.224", - "jsr:@std/media-types@^1.0", - "jsr:@std/path@^1.0", - "npm:path-to-regexp@6.2.1" - ] - }, - "@std/assert@1.0.8": { - "integrity": "ebe0bd7eb488ee39686f77003992f389a06c3da1bbd8022184804852b2fa641b" - }, - "@std/bytes@1.0.4": { - "integrity": "11a0debe522707c95c7b7ef89b478c13fb1583a7cfb9a85674cd2cc2e3a28abc" - }, - "@std/crypto@1.0.3": { - "integrity": "a2a32f51ddef632d299e3879cd027c630dcd4d1d9a5285d6e6788072f4e51e7f" - }, - "@std/encoding@1.0.5": { - "integrity": "ecf363d4fc25bd85bd915ff6733a7e79b67e0e7806334af15f4645c569fefc04" - }, - "@std/http@1.0.10": { - "integrity": "4e32d11493ab04e3ef09f104f0cb9beb4228b1d4b47c5469573c2c294c0d3692", - "dependencies": [ - "jsr:@std/encoding@^1.0.5" - ] - }, - "@std/io@0.224.9": { - "integrity": "4414664b6926f665102e73c969cfda06d2c4c59bd5d0c603fd4f1b1c840d6ee3", - "dependencies": [ - "jsr:@std/bytes@^1.0.2" - ] - }, - "@std/media-types@1.1.0": { - "integrity": "c9d093f0c05c3512932b330e3cc1fe1d627b301db33a4c2c2185c02471d6eaa4" - }, - "@std/path@1.0.8": { - "integrity": "548fa456bb6a04d3c1a1e7477986b6cffbce95102d0bb447c67c4ee70e0364be" - } + "@oak/oak@17.1.3": { + "integrity": "d89296c22db91681dd3a2a1e1fd14e258d0d5a9654de55637aee5b661c159f33", + "dependencies": [ + "jsr:@oak/commons", + "jsr:@std/assert", + "jsr:@std/bytes@1", + "jsr:@std/crypto", + "jsr:@std/http", + "jsr:@std/io", + "jsr:@std/media-types", + "jsr:@std/path", + "npm:path-to-regexp" + ] }, - "npm": { - "path-to-regexp@6.2.1": { - "integrity": "sha512-JLyh7xT1kizaEvcaXOQwOc2/Yhw6KZOvPf1S8401UyLk86CU79LN3vl7ztXGm/pZ+YjoyAJ4rxmHwbkBXJX+yw==", - "dependencies": {} - } + "@std/assert@1.0.8": { + "integrity": "ebe0bd7eb488ee39686f77003992f389a06c3da1bbd8022184804852b2fa641b" + }, + "@std/bytes@1.0.4": { + "integrity": "11a0debe522707c95c7b7ef89b478c13fb1583a7cfb9a85674cd2cc2e3a28abc" + }, + "@std/crypto@1.0.3": { + "integrity": "a2a32f51ddef632d299e3879cd027c630dcd4d1d9a5285d6e6788072f4e51e7f" + }, + "@std/encoding@1.0.5": { + "integrity": "ecf363d4fc25bd85bd915ff6733a7e79b67e0e7806334af15f4645c569fefc04" + }, + "@std/http@1.0.10": { + "integrity": "4e32d11493ab04e3ef09f104f0cb9beb4228b1d4b47c5469573c2c294c0d3692", + "dependencies": [ + "jsr:@std/encoding@^1.0.5" + ] + }, + "@std/io@0.224.9": { + "integrity": "4414664b6926f665102e73c969cfda06d2c4c59bd5d0c603fd4f1b1c840d6ee3", + "dependencies": [ + "jsr:@std/bytes@^1.0.2" + ] + }, + "@std/media-types@1.1.0": { + "integrity": "c9d093f0c05c3512932b330e3cc1fe1d627b301db33a4c2c2185c02471d6eaa4" + }, + "@std/path@1.0.8": { + "integrity": "548fa456bb6a04d3c1a1e7477986b6cffbce95102d0bb447c67c4ee70e0364be" } }, - "remote": {} + "npm": { + "@types/node@22.5.4": { + "integrity": "sha512-FDuKUJQm/ju9fT/SeX/6+gBzoPzlVCzfzmGkwKvRHQVxi4BntVbyIwf6a4Xn62mrvndLiml6z/UBXIdEVjQLXg==", + "dependencies": [ + "undici-types" + ] + }, + "path-to-regexp@6.2.1": { + "integrity": "sha512-JLyh7xT1kizaEvcaXOQwOc2/Yhw6KZOvPf1S8401UyLk86CU79LN3vl7ztXGm/pZ+YjoyAJ4rxmHwbkBXJX+yw==" + }, + "undici-types@6.19.8": { + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==" + } + } } diff --git a/web/main.ts b/web/main.ts index 20d6257..e98f8cc 100644 --- a/web/main.ts +++ b/web/main.ts @@ -1,5 +1,6 @@ import { Application, Router } from "jsr:@oak/oak"; + const app = new Application(); const router = new Router(); diff --git a/web/public/deno.jsonc b/web/public/deno.jsonc new file mode 100644 index 0000000..56e91e7 --- /dev/null +++ b/web/public/deno.jsonc @@ -0,0 +1,9 @@ +{ + "compilerOptions": { + "checkJs": false, + "lib": ["dom", "dom.iterable", "dom.asynciterable"], + }, + "fmt": { + "indentWidth": 4 + } +} diff --git a/web/public/index.html b/web/public/index.html index ffee46f..05ef0bc 100644 --- a/web/public/index.html +++ b/web/public/index.html @@ -1,9 +1,14 @@ - + - - - - -

slige

- + + + + + + +
+
+
+
+ diff --git a/web/public/index.js b/web/public/index.js new file mode 100644 index 0000000..2431495 --- /dev/null +++ b/web/public/index.js @@ -0,0 +1,179 @@ +const codeCoverageDiv = document.querySelector("#code-coverage"); +const flameGraphDiv = document.querySelector("#flame-graph"); + +function loadCodeCoverage(text, codeCoverageData) { + codeCoverageDiv.innerHTML = ` + +
${text}
+ + `; + + /** @type { HTMLCanvasElement } */ + const canvas = document.querySelector("#code-coverage-canvas"); + canvas.width = 1000; + canvas.height = 500; + + const ctx = canvas.getContext("2d"); + + ctx.font = "20px monospace"; + + const { width: chWidth } = ctx.measureText("-"); + const chHeight = 26; + + 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) { + const entry = entries.find((entry) => index >= entry.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; + if (text[index] == "\n") { + col = 1; + line += 1; + } + } + + const tooltip = document.getElementById("covers-tooltip"); + + canvas.addEventListener("mousemove", (e) => { + const col = Math.floor(e.offsetX / chWidth + 1); + const line = Math.floor(e.offsetY / chHeight + 1); + const key = `${line}-${col}`; + 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 = ` + + + `; + + /** @type { HTMLCanvasElement } */ + const canvas = document.querySelector("#flame-graph-canvas"); + canvas.width = 1000; + 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((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; + }); + canvas.addEventListener("mouseleave", () => { + tooltip.hidden = true; + }); +} + +const 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; +} +`; + +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":[]}]}]}`, +); + +loadCodeCoverage(codeData, codeCoverageData); +loadFlameGraph(flameGraphData, { + 0: "", + 12: "add", + 18: "main", +}); diff --git a/web/public/style.css b/web/public/style.css new file mode 100644 index 0000000..27d694d --- /dev/null +++ b/web/public/style.css @@ -0,0 +1,89 @@ +:root { + color-scheme: light dark; + font-family: sans; + + --bg-1: #2b2d31; + --bg-2: #313338; + --fg-2: #666666; +} + +* { + box-sizing: border-box; +} + +body { + margin: 0; + height: 100vh; + background-color: var(--bg-1); +} + +main { + margin-top: 20px; + margin-bottom: 20px; + margin-left: auto; + margin-right: auto; + max-width: 1500px; + width: 100%; +} + +#code-coverage { + width: 1000px; + height: 500px; + margin: 20px; + background-color: rgb(240, 220, 200); +} +#code-coverage pre { + background-color: none; +} +#code-coverage code { + font-family: monospace; + color: black; + font-weight: 600; + font-size: 20px; +} +#code-coverage canvas { + z-index: 1; + width: 1000px; + height: 500px; + position: absolute; + image-rendering: pixelated; +} +#code-coverage #covers-tooltip { + z-index: 2; + position: absolute; + top: 0; + left: 0; + padding: 3px; + border-radius: 3px; + background-color: var(--bg-2); + box-shadow: 2px 2px 2px black; + color: #eee; +} + +#flame-graph { + width: 1004px; + height: 504px; + margin: 20px; + background-color: var(--bg-2); + border: 2px solid rgb(240, 220, 200); + padding: 2px; +} +#flame-graph canvas { + z-index: 1; + width: 1000px; + height: 500px; + position: absolute; + image-rendering: pixelated; + transform: translate(-2px, -2px); +} +#flame-graph #flame-graph-tooltip { + z-index: 2; + position: absolute; + top: 0; + left: 0; + padding: 3px; + border-radius: 3px; + background-color: var(--bg-2); + box-shadow: 2px 2px 2px black; + color: #eee; +}