This commit is contained in:
sfja 2024-11-21 04:12:07 +01:00
parent 6c11e01841
commit 4f9a671bdc
11 changed files with 401 additions and 88 deletions

View File

@ -295,8 +295,8 @@ public:
auto parse_val() -> Res<std::unique_ptr<Value>>;
private:
inline auto unexpected_tok_err(TokTyp expected, std::string_view msg)
-> Res<std::unique_ptr<Value>>
inline auto unexpected_tok_err(
TokTyp expected, std::string_view msg) -> Res<std::unique_ptr<Value>>
{
return Err {
.pos = this->cur.val().pos,
@ -339,7 +339,12 @@ public:
template <typename T, typename F>
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);
}
}

View File

@ -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);
}

View File

@ -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) {

View File

@ -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()

View File

@ -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<FGNode> nodes = { FGNode(0, 0) };

View File

@ -1,45 +1,45 @@
{
"version": "3",
"packages": {
"version": "4",
"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"
"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@^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"
"jsr:@std/assert",
"jsr:@std/bytes@1",
"jsr:@std/crypto",
"jsr:@std/encoding@1",
"jsr:@std/http",
"jsr:@std/media-types"
]
},
"@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"
"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"
]
},
"@std/assert@1.0.8": {
@ -74,11 +74,17 @@
}
},
"npm": {
"path-to-regexp@6.2.1": {
"integrity": "sha512-JLyh7xT1kizaEvcaXOQwOc2/Yhw6KZOvPf1S8401UyLk86CU79LN3vl7ztXGm/pZ+YjoyAJ4rxmHwbkBXJX+yw==",
"dependencies": {}
}
}
"@types/node@22.5.4": {
"integrity": "sha512-FDuKUJQm/ju9fT/SeX/6+gBzoPzlVCzfzmGkwKvRHQVxi4BntVbyIwf6a4Xn62mrvndLiml6z/UBXIdEVjQLXg==",
"dependencies": [
"undici-types"
]
},
"remote": {}
"path-to-regexp@6.2.1": {
"integrity": "sha512-JLyh7xT1kizaEvcaXOQwOc2/Yhw6KZOvPf1S8401UyLk86CU79LN3vl7ztXGm/pZ+YjoyAJ4rxmHwbkBXJX+yw=="
},
"undici-types@6.19.8": {
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw=="
}
}
}

View File

@ -1,5 +1,6 @@
import { Application, Router } from "jsr:@oak/oak";
const app = new Application();
const router = new Router();

9
web/public/deno.jsonc Normal file
View File

@ -0,0 +1,9 @@
{
"compilerOptions": {
"checkJs": false,
"lib": ["dom", "dom.iterable", "dom.asynciterable"],
},
"fmt": {
"indentWidth": 4
}
}

View File

@ -1,9 +1,14 @@
<!doctype html>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="style.css">
<script src="index.js" type="module" defer></script>
</head>
<body>
<h1>slige</h1>
<main>
<div id="code-coverage"></div>
<div id="flame-graph"></div>
</main>
</body>
</html>

179
web/public/index.js Normal file
View File

@ -0,0 +1,179 @@
const codeCoverageDiv = document.querySelector("#code-coverage");
const flameGraphDiv = document.querySelector("#flame-graph");
function loadCodeCoverage(text, codeCoverageData) {
codeCoverageDiv.innerHTML = `
<canvas id="code-coverage-canvas"></canvas>
<pre><code>${text}</code></pre>
<span id="covers-tooltip" hidden></span>
`;
/** @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 = `
<canvas id="flame-graph-canvas"></canvas>
<span id="flame-graph-tooltip" hidden></span>
`;
/** @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: "<entry>",
12: "add",
18: "main",
});

89
web/public/style.css Normal file
View File

@ -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;
}