mirror of
https://git.sfja.dk/Mikkel/slige.git
synced 2025-01-18 22:46: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": {
|
"compilerOptions": {
|
||||||
"lib": ["dom", "dom.iterable", "dom.asynciterable"]
|
"checkJs": false,
|
||||||
|
"lib": ["dom", "dom.iterable", "dom.asynciterable", "deno.ns"]
|
||||||
},
|
},
|
||||||
"fmt": {
|
"fmt": {
|
||||||
"indentWidth": 4
|
"indentWidth": 4
|
||||||
|
188
web/public/dist/bundle.js
vendored
188
web/public/dist/bundle.js
vendored
@ -1,27 +1,32 @@
|
|||||||
// src/index.ts
|
// src/index.ts
|
||||||
var codeCoverageDiv = document.querySelector("#code-coverage");
|
function loadCodeCoverage(text, codeCoverageData, codeCoverageDiv) {
|
||||||
var flameGraphDiv = document.querySelector("#flame-graph");
|
const tooltip = document.createElement("span");
|
||||||
function drawText(text, codeCoverageData) {
|
tooltip.id = "covers-tooltip";
|
||||||
const tooltip = document.getElementById("covers-tooltip");
|
codeCoverageDiv.append(tooltip);
|
||||||
const entries = codeCoverageData.toSorted((a, b) => b.index - a.index);
|
const entries = codeCoverageData.toSorted((a, b) => b.index - a.index);
|
||||||
const charEntries = {};
|
const charEntries = {};
|
||||||
const elements = [];
|
const elements = [];
|
||||||
let line = 1;
|
let line = 1;
|
||||||
let col = 1;
|
let col = 1;
|
||||||
for (let index = 0; index < text.length; ++index) {
|
for (let index = 0; index < text.length; ++index) {
|
||||||
if (text[index] == "\n") {
|
if (text[index] === "\n") {
|
||||||
col = 1;
|
col = 1;
|
||||||
line += 1;
|
line += 1;
|
||||||
elements.push("\n");
|
const newlineSpan = document.createElement("span");
|
||||||
|
newlineSpan.innerText = "\n";
|
||||||
|
elements.push(newlineSpan);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const entry = entries.find((entry2) => index >= entry2.index);
|
const entry = entries.find((entry2) => index >= entry2.index);
|
||||||
|
if (!entry) {
|
||||||
|
throw new Error("unreachable");
|
||||||
|
}
|
||||||
charEntries[`${line}-${col}`] = entry;
|
charEntries[`${line}-${col}`] = entry;
|
||||||
const color = (ratio) => `rgba(${255 - 255 * ratio}, ${255 * ratio}, 125, 0.5)`;
|
const color = (ratio) => `rgba(${255 - 255 * ratio}, ${255 * ratio}, 125, 0.5)`;
|
||||||
const span = document.createElement("span");
|
const span = document.createElement("span");
|
||||||
span.style.backgroundColor = color(Math.min(entry.covers / 25, 1));
|
span.style.backgroundColor = color(Math.min(entry.covers / 25, 1));
|
||||||
span.innerText = text[index];
|
span.innerText = text[index];
|
||||||
span.dataset.covers = entry.covers;
|
span.dataset.covers = entry.covers.toString();
|
||||||
elements.push(span);
|
elements.push(span);
|
||||||
col += 1;
|
col += 1;
|
||||||
}
|
}
|
||||||
@ -30,16 +35,18 @@ function drawText(text, codeCoverageData) {
|
|||||||
const outside = x < boundingRect.left || x >= boundingRect.right || y < boundingRect.top || y >= boundingRect.bottom;
|
const outside = x < boundingRect.left || x >= boundingRect.right || y < boundingRect.top || y >= boundingRect.bottom;
|
||||||
return !outside;
|
return !outside;
|
||||||
}
|
}
|
||||||
testytestytesty.append(...elements);
|
codeCoverageDiv.append(...elements);
|
||||||
document.addEventListener("mousemove", (event) => {
|
document.addEventListener("mousemove", (event) => {
|
||||||
const [x, y] = [event.clientX, event.clientY];
|
const [x, y] = [event.clientX, event.clientY];
|
||||||
const outerBox = testytestytesty.getBoundingClientRect();
|
const outerBox = codeCoverageDiv.getBoundingClientRect();
|
||||||
if (!positionInBox([x, y], outerBox)) {
|
if (!positionInBox([x, y], outerBox)) {
|
||||||
console.log("+");
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const element = elements.find((element2) => {
|
const element = elements.find((element2) => {
|
||||||
if (!element2.dataset?.covers) {
|
if (typeof element2 === "string") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!element2.dataset.covers) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const isIn = positionInBox([x, y], element2.getBoundingClientRect());
|
const isIn = positionInBox([x, y], element2.getBoundingClientRect());
|
||||||
@ -56,127 +63,8 @@ function drawText(text, codeCoverageData) {
|
|||||||
tooltip.innerText = `Ran ${covers} time${covers !== 1 ? "s" : ""}`;
|
tooltip.innerText = `Ran ${covers} time${covers !== 1 ? "s" : ""}`;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
function loadCodeCoverage(text, codeCoverageData) {
|
function main() {
|
||||||
codeCoverageDiv.innerHTML = `
|
const codeData = `fn add(a, b) {
|
||||||
<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) {
|
|
||||||
+ a b
|
+ a b
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -190,25 +78,41 @@ loop {
|
|||||||
i = + i 1;
|
i = + i 1;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
function main() {
|
|
||||||
const codeCoverageData = JSON.parse(
|
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}]`
|
`[{"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(
|
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":[]}]}]}`
|
`{"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) {
|
for (const input of viewRadios) {
|
||||||
input.addEventListener("input", (ev) => {
|
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();
|
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>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
<main id="view">
|
<main id="view">
|
||||||
<div id="code-coverage"></div>
|
|
||||||
<div id="flame-graph"></div>
|
<div id="flame-graph"></div>
|
||||||
<pre id="testytestytesty"></pre>
|
<pre id="code-coverage"></pre>
|
||||||
<div id="cover">Process is currently running</div>
|
<div id="cover">Process is currently running</div>
|
||||||
</main>
|
</main>
|
||||||
|
<span id="covers-tooltip"></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
|
@ -1,59 +1,78 @@
|
|||||||
const codeCoverageDiv = document.querySelector("#code-coverage");
|
|
||||||
const flameGraphDiv = document.querySelector("#flame-graph");
|
|
||||||
|
|
||||||
type CodeCovEntry = {
|
type CodeCovEntry = {
|
||||||
index: number;
|
index: number;
|
||||||
line: number;
|
line: number;
|
||||||
col: number;
|
col: number;
|
||||||
covers: 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((
|
const entries = codeCoverageData.toSorted((
|
||||||
a: CodeCovEntry,
|
a: CodeCovEntry,
|
||||||
b: CodeCovEntry,
|
b: CodeCovEntry,
|
||||||
) => b.index - a.index);
|
) => b.index - a.index);
|
||||||
const charEntries = {};
|
const charEntries: { [key: string]: CodeCovEntry } = {};
|
||||||
const elements = [];
|
const elements: HTMLElement[] = [];
|
||||||
let line = 1;
|
let line = 1;
|
||||||
let col = 1;
|
let col = 1;
|
||||||
for (let index = 0; index < text.length; ++index) {
|
for (let index = 0; index < text.length; ++index) {
|
||||||
if (text[index] == "\n") {
|
if (text[index] === "\n") {
|
||||||
col = 1;
|
col = 1;
|
||||||
line += 1;
|
line += 1;
|
||||||
elements.push("\n");
|
const newlineSpan = document.createElement("span");
|
||||||
|
newlineSpan.innerText = "\n";
|
||||||
|
elements.push(newlineSpan);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const entry = entries.find((entry) => index >= entry.index);
|
const entry = entries.find((entry) => index >= entry.index);
|
||||||
|
if (!entry) {
|
||||||
|
throw new Error("unreachable");
|
||||||
|
}
|
||||||
charEntries[`${line}-${col}`] = entry;
|
charEntries[`${line}-${col}`] = entry;
|
||||||
|
|
||||||
const color = (ratio) =>
|
const color = (ratio: number) =>
|
||||||
`rgba(${255 - 255 * ratio}, ${255 * ratio}, 125, 0.5)`;
|
`rgba(${255 - 255 * ratio}, ${255 * ratio}, 125, 0.5)`;
|
||||||
|
|
||||||
const span = document.createElement("span");
|
const span = document.createElement("span");
|
||||||
span.style.backgroundColor = color(Math.min(entry.covers / 25, 1));
|
span.style.backgroundColor = color(Math.min(entry.covers / 25, 1));
|
||||||
span.innerText = text[index];
|
span.innerText = text[index];
|
||||||
span.dataset.covers = entry.covers;
|
span.dataset.covers = entry.covers.toString();
|
||||||
elements.push(span);
|
elements.push(span);
|
||||||
col += 1;
|
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 [x, y] = position;
|
||||||
const outside = x < boundingRect.left ||
|
const outside = x < boundingRect.left ||
|
||||||
x >= boundingRect.right || y < boundingRect.top ||
|
x >= boundingRect.right || y < boundingRect.top ||
|
||||||
y >= boundingRect.bottom;
|
y >= boundingRect.bottom;
|
||||||
return !outside;
|
return !outside;
|
||||||
}
|
}
|
||||||
testytestytesty.append(...elements);
|
codeCoverageDiv.append(...elements);
|
||||||
document.addEventListener("mousemove", (event) => {
|
document.addEventListener("mousemove", (event) => {
|
||||||
const [x, y] = [event.clientX, event.clientY];
|
const [x, y] = [event.clientX, event.clientY];
|
||||||
const outerBox = testytestytesty.getBoundingClientRect();
|
const outerBox = codeCoverageDiv.getBoundingClientRect();
|
||||||
if (!positionInBox([x, y], outerBox)) {
|
if (!positionInBox([x, y], outerBox)) {
|
||||||
console.log("+");
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const element = elements.find((element) => {
|
const element = elements.find((element) => {
|
||||||
if (!element.dataset?.covers) {
|
if (typeof element === "string") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!element.dataset.covers) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const isIn = positionInBox([x, y], element.getBoundingClientRect());
|
const isIn = positionInBox([x, y], element.getBoundingClientRect());
|
||||||
@ -71,91 +90,34 @@ function drawText(text: string, codeCoverageData: CodeCovEntry[]) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadCodeCoverage(text, codeCoverageData) {
|
// @ts-ignore: unsure of relevant types
|
||||||
codeCoverageDiv.innerHTML = `
|
function loadFlameGraph(flameGraphData, fnNames, flameGraphDiv) {
|
||||||
<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) {
|
|
||||||
flameGraphDiv.innerHTML = `
|
flameGraphDiv.innerHTML = `
|
||||||
<canvas id="flame-graph-canvas"></canvas>
|
<canvas id="flame-graph-canvas"></canvas>
|
||||||
<span id="flame-graph-tooltip" hidden></span>
|
<span id="flame-graph-tooltip" hidden></span>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
/** @type { HTMLCanvasElement } */
|
const canvas = document.querySelector<HTMLCanvasElement>(
|
||||||
const canvas = document.querySelector("#flame-graph-canvas");
|
"#flame-graph-canvas",
|
||||||
|
)!;
|
||||||
canvas.width = 1000;
|
canvas.width = 1000;
|
||||||
canvas.height = 500;
|
canvas.height = 500;
|
||||||
|
|
||||||
const ctx = canvas.getContext("2d");
|
const ctx = canvas.getContext("2d")!;
|
||||||
ctx.font = "16px monospace";
|
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) {
|
function calculateNodeRects(node, depth, totalAcc, offsetAcc) {
|
||||||
const x = (offsetAcc / totalAcc) * canvas.width;
|
const x = (offsetAcc / totalAcc) * canvas.width;
|
||||||
const y = canvas.height - 30 * depth - 30;
|
const y = canvas.height - 30 * depth - 30;
|
||||||
@ -167,6 +129,7 @@ function loadFlameGraph(flameGraphData, fnNames) {
|
|||||||
nodes.push({ x, y, w, h, title, percent });
|
nodes.push({ x, y, w, h, title, percent });
|
||||||
|
|
||||||
const totalChildrenAcc = node.children.reduce(
|
const totalChildrenAcc = node.children.reduce(
|
||||||
|
// @ts-ignore: unsure of relevant types
|
||||||
(acc, child) => acc + child.acc,
|
(acc, child) => acc + child.acc,
|
||||||
0,
|
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) => {
|
canvas.addEventListener("mousemove", (e) => {
|
||||||
const x = e.offsetX;
|
const x = e.offsetX;
|
||||||
@ -219,7 +182,8 @@ function loadFlameGraph(flameGraphData, fnNames) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const codeData = `\
|
function main() {
|
||||||
|
const codeData = `\
|
||||||
fn add(a, b) {
|
fn add(a, b) {
|
||||||
+ a b
|
+ a b
|
||||||
}
|
}
|
||||||
@ -235,7 +199,6 @@ loop {
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
function main() {
|
|
||||||
const codeCoverageData = JSON.parse(
|
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}]`,
|
`[{"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":[]}]}]}`,
|
`{"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) {
|
for (const input of viewRadios) {
|
||||||
input.addEventListener("input", (ev) => {
|
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);
|
// loadCodeCoverage(codeData, codeCoverageData);
|
||||||
loadFlameGraph(flameGraphData, {
|
// loadFlameGraph(flameGraphData, {
|
||||||
0: "<entry>",
|
// 0: "<entry>",
|
||||||
12: "add",
|
// 12: "add",
|
||||||
18: "main",
|
// 18: "main",
|
||||||
});
|
// });
|
||||||
drawText(codeData, codeCoverageData);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
main();
|
main();
|
||||||
|
@ -77,6 +77,27 @@ main #cover {
|
|||||||
gap: 0.5rem;
|
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 {
|
#views-layout {
|
||||||
display: flex;
|
display: flex;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
@ -85,28 +106,7 @@ main #cover {
|
|||||||
max-width: 1500px;
|
max-width: 1500px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#code-coverage {
|
#covers-tooltip {
|
||||||
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 {
|
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
|
Loading…
Reference in New Issue
Block a user