mirror of
https://git.sfja.dk/Mikkel/slige.git
synced 2025-01-18 17:56:30 +00:00
code cov/source code view
This commit is contained in:
parent
a115cdf78c
commit
5d029967ed
1
web/public/.gitignore
vendored
Normal file
1
web/public/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
dist/
|
@ -1,6 +1,11 @@
|
||||
{
|
||||
"tasks": {
|
||||
"bundle": "deno run -A bundle.ts",
|
||||
"dev": "deno run --watch -A bundle.ts"
|
||||
},
|
||||
"compilerOptions": {
|
||||
"lib": ["dom", "dom.iterable", "dom.asynciterable"]
|
||||
"checkJs": false,
|
||||
"lib": ["dom", "dom.iterable", "dom.asynciterable", "deno.ns"]
|
||||
},
|
||||
"fmt": {
|
||||
"indentWidth": 4
|
||||
|
188
web/public/dist/bundle.js
vendored
188
web/public/dist/bundle.js
vendored
@ -1,27 +1,32 @@
|
||||
// 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");
|
||||
function loadCodeCoverage(text, codeCoverageData, codeCoverageDiv) {
|
||||
const tooltip = document.createElement("span");
|
||||
tooltip.id = "covers-tooltip";
|
||||
codeCoverageDiv.append(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") {
|
||||
if (text[index] === "\n") {
|
||||
col = 1;
|
||||
line += 1;
|
||||
elements.push("\n");
|
||||
const newlineSpan = document.createElement("span");
|
||||
newlineSpan.innerText = "\n";
|
||||
elements.push(newlineSpan);
|
||||
continue;
|
||||
}
|
||||
const entry = entries.find((entry2) => index >= entry2.index);
|
||||
if (!entry) {
|
||||
throw new Error("unreachable");
|
||||
}
|
||||
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;
|
||||
span.dataset.covers = entry.covers.toString();
|
||||
elements.push(span);
|
||||
col += 1;
|
||||
}
|
||||
@ -30,16 +35,18 @@ function drawText(text, codeCoverageData) {
|
||||
const outside = x < boundingRect.left || x >= boundingRect.right || y < boundingRect.top || y >= boundingRect.bottom;
|
||||
return !outside;
|
||||
}
|
||||
testytestytesty.append(...elements);
|
||||
codeCoverageDiv.append(...elements);
|
||||
document.addEventListener("mousemove", (event) => {
|
||||
const [x, y] = [event.clientX, event.clientY];
|
||||
const outerBox = testytestytesty.getBoundingClientRect();
|
||||
const outerBox = codeCoverageDiv.getBoundingClientRect();
|
||||
if (!positionInBox([x, y], outerBox)) {
|
||||
console.log("+");
|
||||
return;
|
||||
}
|
||||
const element = elements.find((element2) => {
|
||||
if (!element2.dataset?.covers) {
|
||||
if (typeof element2 === "string") {
|
||||
return false;
|
||||
}
|
||||
if (!element2.dataset.covers) {
|
||||
return false;
|
||||
}
|
||||
const isIn = positionInBox([x, y], element2.getBoundingClientRect());
|
||||
@ -56,127 +63,8 @@ function drawText(text, codeCoverageData) {
|
||||
tooltip.innerText = `Ran ${covers} time${covers !== 1 ? "s" : ""}`;
|
||||
});
|
||||
}
|
||||
function loadCodeCoverage(text, codeCoverageData) {
|
||||
codeCoverageDiv.innerHTML = `
|
||||
<canvas id="code-coverage-canvas"></canvas>
|
||||
<pre><code>${text}</code></pre>
|
||||
<span id="covers-tooltip" hidden></span>
|
||||
`;
|
||||
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 = `
|
||||
<canvas id="flame-graph-canvas"></canvas>
|
||||
<span id="flame-graph-tooltip" hidden></span>
|
||||
`;
|
||||
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) {
|
||||
function main() {
|
||||
const codeData = `fn add(a, b) {
|
||||
+ a b
|
||||
}
|
||||
|
||||
@ -190,25 +78,41 @@ loop {
|
||||
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"]');
|
||||
const view = document.querySelector("#view");
|
||||
const renderFunctions = {
|
||||
"source-code": () => {
|
||||
const code = document.createElement("pre");
|
||||
code.innerHTML = codeData;
|
||||
view.replaceChildren(code);
|
||||
},
|
||||
"code-coverage": () => {
|
||||
const codeCoverageElement = document.createElement("pre");
|
||||
loadCodeCoverage(codeData, codeCoverageData, codeCoverageElement);
|
||||
const view2 = document.querySelector("#view");
|
||||
view2.replaceChildren(codeCoverageElement);
|
||||
},
|
||||
"flame-graph": () => {
|
||||
}
|
||||
};
|
||||
const viewRadios = document.querySelectorAll(
|
||||
'input[name="views"]'
|
||||
);
|
||||
for (const input of viewRadios) {
|
||||
input.addEventListener("input", (ev) => {
|
||||
console.log(ev);
|
||||
const target = ev.target;
|
||||
const value = target.value;
|
||||
renderFunctions[value]();
|
||||
});
|
||||
if (input.checked) {
|
||||
const value = input.value;
|
||||
renderFunctions[value]();
|
||||
}
|
||||
}
|
||||
loadCodeCoverage(codeData, codeCoverageData);
|
||||
loadFlameGraph(flameGraphData, {
|
||||
0: "<entry>",
|
||||
12: "add",
|
||||
18: "main"
|
||||
});
|
||||
drawText(codeData, codeCoverageData);
|
||||
}
|
||||
main();
|
||||
|
318
web/public/dist/bytes.esm.js
vendored
318
web/public/dist/bytes.esm.js
vendored
@ -1,318 +0,0 @@
|
||||
// https://deno.land/std@0.185.0/bytes/bytes_list.ts
|
||||
var BytesList = class {
|
||||
#len = 0;
|
||||
#chunks = [];
|
||||
constructor() {
|
||||
}
|
||||
/**
|
||||
* Total size of bytes
|
||||
*/
|
||||
size() {
|
||||
return this.#len;
|
||||
}
|
||||
/**
|
||||
* Push bytes with given offset infos
|
||||
*/
|
||||
add(value, start = 0, end = value.byteLength) {
|
||||
if (value.byteLength === 0 || end - start === 0) {
|
||||
return;
|
||||
}
|
||||
checkRange(start, end, value.byteLength);
|
||||
this.#chunks.push({
|
||||
value,
|
||||
end,
|
||||
start,
|
||||
offset: this.#len
|
||||
});
|
||||
this.#len += end - start;
|
||||
}
|
||||
/**
|
||||
* Drop head `n` bytes.
|
||||
*/
|
||||
shift(n) {
|
||||
if (n === 0) {
|
||||
return;
|
||||
}
|
||||
if (this.#len <= n) {
|
||||
this.#chunks = [];
|
||||
this.#len = 0;
|
||||
return;
|
||||
}
|
||||
const idx = this.getChunkIndex(n);
|
||||
this.#chunks.splice(0, idx);
|
||||
const [chunk] = this.#chunks;
|
||||
if (chunk) {
|
||||
const diff = n - chunk.offset;
|
||||
chunk.start += diff;
|
||||
}
|
||||
let offset = 0;
|
||||
for (const chunk2 of this.#chunks) {
|
||||
chunk2.offset = offset;
|
||||
offset += chunk2.end - chunk2.start;
|
||||
}
|
||||
this.#len = offset;
|
||||
}
|
||||
/**
|
||||
* Find chunk index in which `pos` locates by binary-search
|
||||
* returns -1 if out of range
|
||||
*/
|
||||
getChunkIndex(pos) {
|
||||
let max = this.#chunks.length;
|
||||
let min = 0;
|
||||
while (true) {
|
||||
const i = min + Math.floor((max - min) / 2);
|
||||
if (i < 0 || this.#chunks.length <= i) {
|
||||
return -1;
|
||||
}
|
||||
const { offset, start, end } = this.#chunks[i];
|
||||
const len = end - start;
|
||||
if (offset <= pos && pos < offset + len) {
|
||||
return i;
|
||||
} else if (offset + len <= pos) {
|
||||
min = i + 1;
|
||||
} else {
|
||||
max = i - 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Get indexed byte from chunks
|
||||
*/
|
||||
get(i) {
|
||||
if (i < 0 || this.#len <= i) {
|
||||
throw new Error("out of range");
|
||||
}
|
||||
const idx = this.getChunkIndex(i);
|
||||
const { value, offset, start } = this.#chunks[idx];
|
||||
return value[start + i - offset];
|
||||
}
|
||||
/**
|
||||
* Iterator of bytes from given position
|
||||
*/
|
||||
*iterator(start = 0) {
|
||||
const startIdx = this.getChunkIndex(start);
|
||||
if (startIdx < 0)
|
||||
return;
|
||||
const first = this.#chunks[startIdx];
|
||||
let firstOffset = start - first.offset;
|
||||
for (let i = startIdx; i < this.#chunks.length; i++) {
|
||||
const chunk = this.#chunks[i];
|
||||
for (let j = chunk.start + firstOffset; j < chunk.end; j++) {
|
||||
yield chunk.value[j];
|
||||
}
|
||||
firstOffset = 0;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Returns subset of bytes copied
|
||||
*/
|
||||
slice(start, end = this.#len) {
|
||||
if (end === start) {
|
||||
return new Uint8Array();
|
||||
}
|
||||
checkRange(start, end, this.#len);
|
||||
const result = new Uint8Array(end - start);
|
||||
const startIdx = this.getChunkIndex(start);
|
||||
const endIdx = this.getChunkIndex(end - 1);
|
||||
let written = 0;
|
||||
for (let i = startIdx; i <= endIdx; i++) {
|
||||
const {
|
||||
value: chunkValue,
|
||||
start: chunkStart,
|
||||
end: chunkEnd,
|
||||
offset: chunkOffset
|
||||
} = this.#chunks[i];
|
||||
const readStart = chunkStart + (i === startIdx ? start - chunkOffset : 0);
|
||||
const readEnd = i === endIdx ? end - chunkOffset + chunkStart : chunkEnd;
|
||||
const len = readEnd - readStart;
|
||||
result.set(chunkValue.subarray(readStart, readEnd), written);
|
||||
written += len;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
/**
|
||||
* Concatenate chunks into single Uint8Array copied.
|
||||
*/
|
||||
concat() {
|
||||
const result = new Uint8Array(this.#len);
|
||||
let sum = 0;
|
||||
for (const { value, start, end } of this.#chunks) {
|
||||
result.set(value.subarray(start, end), sum);
|
||||
sum += end - start;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
};
|
||||
function checkRange(start, end, len) {
|
||||
if (start < 0 || len < start || end < 0 || len < end || end < start) {
|
||||
throw new Error("invalid range");
|
||||
}
|
||||
}
|
||||
|
||||
// https://deno.land/std@0.185.0/bytes/concat.ts
|
||||
function concat(...buf) {
|
||||
let length = 0;
|
||||
for (const b of buf) {
|
||||
length += b.length;
|
||||
}
|
||||
const output = new Uint8Array(length);
|
||||
let index = 0;
|
||||
for (const b of buf) {
|
||||
output.set(b, index);
|
||||
index += b.length;
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
// https://deno.land/std@0.185.0/bytes/copy.ts
|
||||
function copy(src, dst, off = 0) {
|
||||
off = Math.max(0, Math.min(off, dst.byteLength));
|
||||
const dstBytesAvailable = dst.byteLength - off;
|
||||
if (src.byteLength > dstBytesAvailable) {
|
||||
src = src.subarray(0, dstBytesAvailable);
|
||||
}
|
||||
dst.set(src, off);
|
||||
return src.byteLength;
|
||||
}
|
||||
|
||||
// https://deno.land/std@0.185.0/bytes/ends_with.ts
|
||||
function endsWith(source, suffix) {
|
||||
for (let srci = source.length - 1, sfxi = suffix.length - 1; sfxi >= 0; srci--, sfxi--) {
|
||||
if (source[srci] !== suffix[sfxi])
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// https://deno.land/std@0.185.0/bytes/equals.ts
|
||||
function equalsNaive(a, b) {
|
||||
for (let i = 0; i < b.length; i++) {
|
||||
if (a[i] !== b[i])
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
function equals32Bit(a, b) {
|
||||
const len = a.length;
|
||||
const compressable = Math.floor(len / 4);
|
||||
const compressedA = new Uint32Array(a.buffer, 0, compressable);
|
||||
const compressedB = new Uint32Array(b.buffer, 0, compressable);
|
||||
for (let i = compressable * 4; i < len; i++) {
|
||||
if (a[i] !== b[i])
|
||||
return false;
|
||||
}
|
||||
for (let i = 0; i < compressedA.length; i++) {
|
||||
if (compressedA[i] !== compressedB[i])
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
function equals(a, b) {
|
||||
if (a.length !== b.length) {
|
||||
return false;
|
||||
}
|
||||
return a.length < 1e3 ? equalsNaive(a, b) : equals32Bit(a, b);
|
||||
}
|
||||
|
||||
// https://deno.land/std@0.185.0/bytes/index_of_needle.ts
|
||||
function indexOfNeedle(source, needle, start = 0) {
|
||||
if (start >= source.length) {
|
||||
return -1;
|
||||
}
|
||||
if (start < 0) {
|
||||
start = Math.max(0, source.length + start);
|
||||
}
|
||||
const s = needle[0];
|
||||
for (let i = start; i < source.length; i++) {
|
||||
if (source[i] !== s)
|
||||
continue;
|
||||
const pin = i;
|
||||
let matched = 1;
|
||||
let j = i;
|
||||
while (matched < needle.length) {
|
||||
j++;
|
||||
if (source[j] !== needle[j - pin]) {
|
||||
break;
|
||||
}
|
||||
matched++;
|
||||
}
|
||||
if (matched === needle.length) {
|
||||
return pin;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
// https://deno.land/std@0.185.0/bytes/includes_needle.ts
|
||||
function includesNeedle(source, needle, start = 0) {
|
||||
return indexOfNeedle(source, needle, start) !== -1;
|
||||
}
|
||||
|
||||
// https://deno.land/std@0.185.0/bytes/last_index_of_needle.ts
|
||||
function lastIndexOfNeedle(source, needle, start = source.length - 1) {
|
||||
if (start < 0) {
|
||||
return -1;
|
||||
}
|
||||
if (start >= source.length) {
|
||||
start = source.length - 1;
|
||||
}
|
||||
const e = needle[needle.length - 1];
|
||||
for (let i = start; i >= 0; i--) {
|
||||
if (source[i] !== e)
|
||||
continue;
|
||||
const pin = i;
|
||||
let matched = 1;
|
||||
let j = i;
|
||||
while (matched < needle.length) {
|
||||
j--;
|
||||
if (source[j] !== needle[needle.length - 1 - (pin - j)]) {
|
||||
break;
|
||||
}
|
||||
matched++;
|
||||
}
|
||||
if (matched === needle.length) {
|
||||
return pin - needle.length + 1;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
// https://deno.land/std@0.185.0/bytes/repeat.ts
|
||||
function repeat(source, count) {
|
||||
if (count === 0) {
|
||||
return new Uint8Array();
|
||||
}
|
||||
if (count < 0) {
|
||||
throw new RangeError("bytes: negative repeat count");
|
||||
}
|
||||
if (!Number.isInteger(count)) {
|
||||
throw new Error("bytes: repeat count must be an integer");
|
||||
}
|
||||
const nb = new Uint8Array(source.length * count);
|
||||
let bp = copy(source, nb);
|
||||
for (; bp < nb.length; bp *= 2) {
|
||||
copy(nb.slice(0, bp), nb, bp);
|
||||
}
|
||||
return nb;
|
||||
}
|
||||
|
||||
// https://deno.land/std@0.185.0/bytes/starts_with.ts
|
||||
function startsWith(source, prefix) {
|
||||
for (let i = 0, max = prefix.length; i < max; i++) {
|
||||
if (source[i] !== prefix[i])
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
export {
|
||||
BytesList,
|
||||
concat,
|
||||
copy,
|
||||
endsWith,
|
||||
equals,
|
||||
includesNeedle,
|
||||
indexOfNeedle,
|
||||
lastIndexOfNeedle,
|
||||
repeat,
|
||||
startsWith
|
||||
};
|
@ -26,11 +26,11 @@
|
||||
</div>
|
||||
</nav>
|
||||
<main id="view">
|
||||
<div id="code-coverage"></div>
|
||||
<div id="flame-graph"></div>
|
||||
<pre id="testytestytesty"></pre>
|
||||
<pre id="code-coverage"></pre>
|
||||
<div id="cover">Process is currently running</div>
|
||||
</main>
|
||||
<span id="covers-tooltip"></span>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
@ -1,59 +1,78 @@
|
||||
const codeCoverageDiv = document.querySelector("#code-coverage");
|
||||
const flameGraphDiv = document.querySelector("#flame-graph");
|
||||
|
||||
type CodeCovEntry = {
|
||||
index: number;
|
||||
line: number;
|
||||
col: number;
|
||||
covers: number;
|
||||
};
|
||||
function drawText(text: string, codeCoverageData: CodeCovEntry[]) {
|
||||
const tooltip = document.getElementById("covers-tooltip");
|
||||
|
||||
function loadCodeCoverage(
|
||||
text: string,
|
||||
codeCoverageData: CodeCovEntry[],
|
||||
codeCoverageDiv: HTMLPreElement,
|
||||
) {
|
||||
const tooltip = document.createElement("span");
|
||||
tooltip.id = "covers-tooltip";
|
||||
codeCoverageDiv.append(tooltip);
|
||||
const entries = codeCoverageData.toSorted((
|
||||
a: CodeCovEntry,
|
||||
b: CodeCovEntry,
|
||||
) => b.index - a.index);
|
||||
const charEntries = {};
|
||||
const elements = [];
|
||||
const charEntries: { [key: string]: CodeCovEntry } = {};
|
||||
const elements: HTMLElement[] = [];
|
||||
let line = 1;
|
||||
let col = 1;
|
||||
for (let index = 0; index < text.length; ++index) {
|
||||
if (text[index] == "\n") {
|
||||
if (text[index] === "\n") {
|
||||
col = 1;
|
||||
line += 1;
|
||||
elements.push("\n");
|
||||
const newlineSpan = document.createElement("span");
|
||||
newlineSpan.innerText = "\n";
|
||||
elements.push(newlineSpan);
|
||||
continue;
|
||||
}
|
||||
const entry = entries.find((entry) => index >= entry.index);
|
||||
if (!entry) {
|
||||
throw new Error("unreachable");
|
||||
}
|
||||
charEntries[`${line}-${col}`] = entry;
|
||||
|
||||
const color = (ratio) =>
|
||||
const color = (ratio: number) =>
|
||||
`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;
|
||||
span.dataset.covers = entry.covers.toString();
|
||||
elements.push(span);
|
||||
col += 1;
|
||||
}
|
||||
function positionInBox(position, boundingRect) {
|
||||
function positionInBox(
|
||||
position: [number, number],
|
||||
boundingRect: {
|
||||
left: number;
|
||||
top: number;
|
||||
right: number;
|
||||
bottom: number;
|
||||
},
|
||||
) {
|
||||
const [x, y] = position;
|
||||
const outside = x < boundingRect.left ||
|
||||
x >= boundingRect.right || y < boundingRect.top ||
|
||||
y >= boundingRect.bottom;
|
||||
return !outside;
|
||||
}
|
||||
testytestytesty.append(...elements);
|
||||
codeCoverageDiv.append(...elements);
|
||||
document.addEventListener("mousemove", (event) => {
|
||||
const [x, y] = [event.clientX, event.clientY];
|
||||
const outerBox = testytestytesty.getBoundingClientRect();
|
||||
const outerBox = codeCoverageDiv.getBoundingClientRect();
|
||||
if (!positionInBox([x, y], outerBox)) {
|
||||
console.log("+");
|
||||
return;
|
||||
}
|
||||
const element = elements.find((element) => {
|
||||
if (!element.dataset?.covers) {
|
||||
if (typeof element === "string") {
|
||||
return false;
|
||||
}
|
||||
if (!element.dataset.covers) {
|
||||
return false;
|
||||
}
|
||||
const isIn = positionInBox([x, y], element.getBoundingClientRect());
|
||||
@ -71,91 +90,34 @@ function drawText(text: string, codeCoverageData: CodeCovEntry[]) {
|
||||
});
|
||||
}
|
||||
|
||||
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 = 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((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;
|
||||
}
|
||||
|
||||
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) {
|
||||
// @ts-ignore: unsure of relevant types
|
||||
function loadFlameGraph(flameGraphData, fnNames, flameGraphDiv) {
|
||||
flameGraphDiv.innerHTML = `
|
||||
<canvas id="flame-graph-canvas"></canvas>
|
||||
<span id="flame-graph-tooltip" hidden></span>
|
||||
`;
|
||||
|
||||
/** @type { HTMLCanvasElement } */
|
||||
const canvas = document.querySelector("#flame-graph-canvas");
|
||||
const canvas = document.querySelector<HTMLCanvasElement>(
|
||||
"#flame-graph-canvas",
|
||||
)!;
|
||||
canvas.width = 1000;
|
||||
canvas.height = 500;
|
||||
|
||||
const ctx = canvas.getContext("2d");
|
||||
const ctx = canvas.getContext("2d")!;
|
||||
ctx.font = "16px monospace";
|
||||
|
||||
const nodes = [];
|
||||
type Node = {
|
||||
x: number;
|
||||
y: number;
|
||||
w: number;
|
||||
h: number;
|
||||
title: string;
|
||||
percent: string;
|
||||
};
|
||||
|
||||
const nodes: Node[] = [];
|
||||
|
||||
// @ts-ignore: unsure of relevant types
|
||||
function calculateNodeRects(node, depth, totalAcc, offsetAcc) {
|
||||
const x = (offsetAcc / totalAcc) * canvas.width;
|
||||
const y = canvas.height - 30 * depth - 30;
|
||||
@ -167,6 +129,7 @@ function loadFlameGraph(flameGraphData, fnNames) {
|
||||
nodes.push({ x, y, w, h, title, percent });
|
||||
|
||||
const totalChildrenAcc = node.children.reduce(
|
||||
// @ts-ignore: unsure of relevant types
|
||||
(acc, child) => acc + child.acc,
|
||||
0,
|
||||
);
|
||||
@ -195,7 +158,7 @@ function loadFlameGraph(flameGraphData, fnNames) {
|
||||
);
|
||||
}
|
||||
|
||||
const tooltip = document.getElementById("flame-graph-tooltip");
|
||||
const tooltip = document.getElementById("flame-graph-tooltip")!;
|
||||
|
||||
canvas.addEventListener("mousemove", (e) => {
|
||||
const x = e.offsetX;
|
||||
@ -219,7 +182,8 @@ function loadFlameGraph(flameGraphData, fnNames) {
|
||||
});
|
||||
}
|
||||
|
||||
const codeData = `\
|
||||
function main() {
|
||||
const codeData = `\
|
||||
fn add(a, b) {
|
||||
+ a b
|
||||
}
|
||||
@ -235,7 +199,6 @@ loop {
|
||||
}
|
||||
`;
|
||||
|
||||
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}]`,
|
||||
);
|
||||
@ -244,20 +207,49 @@ function main() {
|
||||
`{"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"]');
|
||||
type RenderFns = {
|
||||
"source-code": () => void;
|
||||
"code-coverage": () => void;
|
||||
"flame-graph": () => void;
|
||||
};
|
||||
|
||||
const view = document.querySelector("#view")!;
|
||||
const renderFunctions: RenderFns = {
|
||||
"source-code": () => {
|
||||
const code = document.createElement("pre");
|
||||
code.innerHTML = codeData;
|
||||
view.replaceChildren(code);
|
||||
},
|
||||
"code-coverage": () => {
|
||||
const codeCoverageElement = document.createElement("pre");
|
||||
loadCodeCoverage(codeData, codeCoverageData, codeCoverageElement);
|
||||
const view = document.querySelector("#view")!;
|
||||
view.replaceChildren(codeCoverageElement);
|
||||
},
|
||||
"flame-graph": () => {},
|
||||
};
|
||||
|
||||
const viewRadios: NodeListOf<HTMLInputElement> = document.querySelectorAll(
|
||||
'input[name="views"]',
|
||||
);
|
||||
for (const input of viewRadios) {
|
||||
input.addEventListener("input", (ev) => {
|
||||
console.log(ev);
|
||||
const target = ev.target as HTMLInputElement;
|
||||
const value = target.value as keyof RenderFns;
|
||||
renderFunctions[value]();
|
||||
});
|
||||
if (input.checked) {
|
||||
const value = input.value as keyof RenderFns;
|
||||
renderFunctions[value]();
|
||||
}
|
||||
}
|
||||
|
||||
loadCodeCoverage(codeData, codeCoverageData);
|
||||
loadFlameGraph(flameGraphData, {
|
||||
0: "<entry>",
|
||||
12: "add",
|
||||
18: "main",
|
||||
});
|
||||
drawText(codeData, codeCoverageData);
|
||||
// loadCodeCoverage(codeData, codeCoverageData);
|
||||
// loadFlameGraph(flameGraphData, {
|
||||
// 0: "<entry>",
|
||||
// 12: "add",
|
||||
// 18: "main",
|
||||
// });
|
||||
}
|
||||
|
||||
main();
|
||||
|
@ -77,6 +77,27 @@ main #cover {
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
#views-nav input {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#views-nav label {
|
||||
display: inline-block;
|
||||
padding: 0.4em;
|
||||
padding-bottom: 0.2em;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#view pre {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
#views-nav input:checked + label {
|
||||
background-color: var(--code-status);
|
||||
color: var(--black);
|
||||
}
|
||||
|
||||
|
||||
#views-layout {
|
||||
display: flex;
|
||||
margin: 0 auto;
|
||||
@ -85,28 +106,7 @@ main #cover {
|
||||
max-width: 1500px;
|
||||
}
|
||||
|
||||
#code-coverage {
|
||||
width: 1000px;
|
||||
height: 500px;
|
||||
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 {
|
||||
#covers-tooltip {
|
||||
z-index: 2;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
|
Loading…
Reference in New Issue
Block a user