diff --git a/web/public/bundle.ts b/web/public/bundle.ts new file mode 100644 index 0000000..28b3820 --- /dev/null +++ b/web/public/bundle.ts @@ -0,0 +1,12 @@ +import * as esbuild from "npm:esbuild"; +import { denoPlugins } from "jsr:@luca/esbuild-deno-loader"; + +await esbuild.build({ + plugins: [...denoPlugins()], + entryPoints: ["./src/index.ts"], + outfile: "./dist/bundle.js", + bundle: true, + format: "esm", +}); + +esbuild.stop(); diff --git a/web/public/deno.jsonc b/web/public/deno.jsonc index 56e91e7..801f699 100644 --- a/web/public/deno.jsonc +++ b/web/public/deno.jsonc @@ -1,7 +1,6 @@ { - "compilerOptions": { - "checkJs": false, - "lib": ["dom", "dom.iterable", "dom.asynciterable"], + "compilerOptions": { + "lib": ["dom", "dom.iterable", "dom.asynciterable"] }, "fmt": { "indentWidth": 4 diff --git a/web/public/deno.lock b/web/public/deno.lock new file mode 100644 index 0000000..51942a2 --- /dev/null +++ b/web/public/deno.lock @@ -0,0 +1,129 @@ +{ + "version": "4", + "specifiers": { + "jsr:@luca/esbuild-deno-loader@*": "0.11.0", + "jsr:@std/bytes@^1.0.2": "1.0.4", + "jsr:@std/encoding@^1.0.5": "1.0.5", + "jsr:@std/path@^1.0.6": "1.0.8", + "npm:esbuild@*": "0.20.2", + "npm:esbuild@0.20.2": "0.20.2" + }, + "jsr": { + "@luca/esbuild-deno-loader@0.11.0": { + "integrity": "c05a989aa7c4ee6992a27be5f15cfc5be12834cab7ff84cabb47313737c51a2c", + "dependencies": [ + "jsr:@std/bytes", + "jsr:@std/encoding", + "jsr:@std/path" + ] + }, + "@std/bytes@1.0.4": { + "integrity": "11a0debe522707c95c7b7ef89b478c13fb1583a7cfb9a85674cd2cc2e3a28abc" + }, + "@std/encoding@1.0.5": { + "integrity": "ecf363d4fc25bd85bd915ff6733a7e79b67e0e7806334af15f4645c569fefc04" + }, + "@std/path@1.0.8": { + "integrity": "548fa456bb6a04d3c1a1e7477986b6cffbce95102d0bb447c67c4ee70e0364be" + } + }, + "npm": { + "@esbuild/aix-ppc64@0.20.2": { + "integrity": "sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==" + }, + "@esbuild/android-arm64@0.20.2": { + "integrity": "sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==" + }, + "@esbuild/android-arm@0.20.2": { + "integrity": "sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==" + }, + "@esbuild/android-x64@0.20.2": { + "integrity": "sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==" + }, + "@esbuild/darwin-arm64@0.20.2": { + "integrity": "sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==" + }, + "@esbuild/darwin-x64@0.20.2": { + "integrity": "sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==" + }, + "@esbuild/freebsd-arm64@0.20.2": { + "integrity": "sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==" + }, + "@esbuild/freebsd-x64@0.20.2": { + "integrity": "sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==" + }, + "@esbuild/linux-arm64@0.20.2": { + "integrity": "sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==" + }, + "@esbuild/linux-arm@0.20.2": { + "integrity": "sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==" + }, + "@esbuild/linux-ia32@0.20.2": { + "integrity": "sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==" + }, + "@esbuild/linux-loong64@0.20.2": { + "integrity": "sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==" + }, + "@esbuild/linux-mips64el@0.20.2": { + "integrity": "sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==" + }, + "@esbuild/linux-ppc64@0.20.2": { + "integrity": "sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==" + }, + "@esbuild/linux-riscv64@0.20.2": { + "integrity": "sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==" + }, + "@esbuild/linux-s390x@0.20.2": { + "integrity": "sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==" + }, + "@esbuild/linux-x64@0.20.2": { + "integrity": "sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==" + }, + "@esbuild/netbsd-x64@0.20.2": { + "integrity": "sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==" + }, + "@esbuild/openbsd-x64@0.20.2": { + "integrity": "sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==" + }, + "@esbuild/sunos-x64@0.20.2": { + "integrity": "sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==" + }, + "@esbuild/win32-arm64@0.20.2": { + "integrity": "sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==" + }, + "@esbuild/win32-ia32@0.20.2": { + "integrity": "sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==" + }, + "@esbuild/win32-x64@0.20.2": { + "integrity": "sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==" + }, + "esbuild@0.20.2": { + "integrity": "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==", + "dependencies": [ + "@esbuild/aix-ppc64", + "@esbuild/android-arm", + "@esbuild/android-arm64", + "@esbuild/android-x64", + "@esbuild/darwin-arm64", + "@esbuild/darwin-x64", + "@esbuild/freebsd-arm64", + "@esbuild/freebsd-x64", + "@esbuild/linux-arm", + "@esbuild/linux-arm64", + "@esbuild/linux-ia32", + "@esbuild/linux-loong64", + "@esbuild/linux-mips64el", + "@esbuild/linux-ppc64", + "@esbuild/linux-riscv64", + "@esbuild/linux-s390x", + "@esbuild/linux-x64", + "@esbuild/netbsd-x64", + "@esbuild/openbsd-x64", + "@esbuild/sunos-x64", + "@esbuild/win32-arm64", + "@esbuild/win32-ia32", + "@esbuild/win32-x64" + ] + } + } +} diff --git a/web/public/dist/bundle.js b/web/public/dist/bundle.js new file mode 100644 index 0000000..e7c790d --- /dev/null +++ b/web/public/dist/bundle.js @@ -0,0 +1,214 @@ +// src/index.ts +var codeCoverageDiv = document.querySelector("#code-coverage"); +var flameGraphDiv = document.querySelector("#flame-graph"); +function drawText(text, codeCoverageData) { + const tooltip = document.getElementById("covers-tooltip"); + const entries = codeCoverageData.toSorted((a, b) => b.index - a.index); + const charEntries = {}; + const elements = []; + let line = 1; + let col = 1; + for (let index = 0; index < text.length; ++index) { + if (text[index] == "\n") { + col = 1; + line += 1; + elements.push("\n"); + continue; + } + const entry = entries.find((entry2) => index >= entry2.index); + charEntries[`${line}-${col}`] = entry; + const color = (ratio) => `rgba(${255 - 255 * ratio}, ${255 * ratio}, 125, 0.5)`; + const span = document.createElement("span"); + span.style.backgroundColor = color(Math.min(entry.covers / 25, 1)); + span.innerText = text[index]; + span.dataset.covers = entry.covers; + elements.push(span); + col += 1; + } + function positionInBox(position, boundingRect) { + const [x, y] = position; + const outside = x < boundingRect.left || x >= boundingRect.right || y < boundingRect.top || y >= boundingRect.bottom; + return !outside; + } + testytestytesty.append(...elements); + document.addEventListener("mousemove", (event) => { + const [x, y] = [event.clientX, event.clientY]; + const outerBox = testytestytesty.getBoundingClientRect(); + if (!positionInBox([x, y], outerBox)) { + console.log("+"); + return; + } + const element = elements.find((element2) => { + if (!element2.dataset?.covers) { + return false; + } + const isIn = positionInBox([x, y], element2.getBoundingClientRect()); + return isIn; + }); + if (!element) { + tooltip.hidden = true; + return; + } + const covers = parseInt(element.dataset.covers); + tooltip.hidden = false; + tooltip.style.left = `${event.clientX + 20}px`; + tooltip.style.top = `${event.clientY + 20}px`; + tooltip.innerText = `Ran ${covers} time${covers !== 1 ? "s" : ""}`; + }); +} +function loadCodeCoverage(text, codeCoverageData) { + codeCoverageDiv.innerHTML = ` + +
${text}
+ + `; + const canvas = document.querySelector("#code-coverage-canvas"); + canvas.width = 1e3; + canvas.height = 500; + const ctx = canvas.getContext("2d"); + ctx.font = "20px monospace"; + const { width: chWidth } = ctx.measureText("-"); + const chHeight = 23; + const color = (ratio) => `rgba(${255 - 255 * ratio}, ${255 * ratio}, 125, 0.5)`; + const entries = codeCoverageData.toSorted((a, b) => b.index - a.index); + const charEntries = {}; + let line = 1; + let col = 1; + for (let index = 0; index < text.length; ++index) { + if (text[index] == "\n") { + col = 1; + line += 1; + continue; + } + const entry = entries.find((entry2) => index >= entry2.index); + charEntries[`${line}-${col}`] = entry; + ctx.fillStyle = color(Math.min(entry.covers / 25, 1)); + ctx.fillRect( + (col - 1) * chWidth, + (line - 1) * chHeight, + chWidth, + chHeight + ); + col += 1; + } + const tooltip = document.getElementById("covers-tooltip"); + canvas.addEventListener("mousemove", (e) => { + const col2 = Math.floor(e.offsetX / chWidth + 1); + const line2 = Math.floor(e.offsetY / chHeight + 1); + const key = `${line2}-${col2}`; + if (!(key in charEntries)) { + tooltip.hidden = true; + return; + } + const entry = charEntries[key]; + tooltip.innerText = `Ran ${entry.covers} time${entry.covers !== 1 ? "s" : ""}`; + tooltip.style.left = `${e.clientX + 20}px`; + tooltip.style.top = `${e.clientY + 20}px`; + tooltip.hidden = false; + }); + canvas.addEventListener("mouseleave", () => { + tooltip.hidden = true; + }); +} +function loadFlameGraph(flameGraphData, fnNames) { + flameGraphDiv.innerHTML = ` + + + `; + const canvas = document.querySelector("#flame-graph-canvas"); + canvas.width = 1e3; + canvas.height = 500; + const ctx = canvas.getContext("2d"); + ctx.font = "16px monospace"; + const nodes = []; + function calculateNodeRects(node, depth, totalAcc, offsetAcc) { + const x = offsetAcc / totalAcc * canvas.width; + const y = canvas.height - 30 * depth - 30; + const w = (node.acc + 1) / totalAcc * canvas.width; + const h = 30; + const title = fnNames[node.fn]; + const percent = `${(node.acc / totalAcc * 100).toFixed(1)}%`; + nodes.push({ x, y, w, h, title, percent }); + const totalChildrenAcc = node.children.reduce( + (acc, child) => acc + child.acc, + 0 + ); + let newOffsetAcc = offsetAcc + (node.acc - totalChildrenAcc) / 2; + for (const child of node.children) { + calculateNodeRects(child, depth + 1, totalAcc, newOffsetAcc); + newOffsetAcc += child.acc; + } + } + calculateNodeRects(flameGraphData, 0, flameGraphData.acc, 0); + for (const node of nodes) { + const { x, y, w, h, title } = node; + ctx.fillStyle = "rgb(255, 125, 0)"; + ctx.fillRect( + x + 1, + y + 1, + w - 2, + h - 2 + ); + ctx.fillStyle = "black"; + ctx.fillText( + title, + x + (w - 10) / 2 - ctx.measureText(title).width / 2 + 5, + y + 20 + ); + } + const tooltip = document.getElementById("flame-graph-tooltip"); + canvas.addEventListener("mousemove", (e) => { + const x = e.offsetX; + const y = e.offsetY; + const node = nodes.find( + (node2) => x >= node2.x && x < node2.x + node2.w && y >= node2.y && y < node2.y + node2.h + ); + if (!node) { + tooltip.hidden = true; + return; + } + tooltip.innerText = `${node.title} ${node.percent}`; + tooltip.style.left = `${e.clientX + 20}px`; + tooltip.style.top = `${e.clientY + 20}px`; + tooltip.hidden = false; + }); + canvas.addEventListener("mouseleave", () => { + tooltip.hidden = true; + }); +} +var codeData = `fn add(a, b) { + + a b +} + +let result = 0; +let i = 0; +loop { + if >= i 10 { + break; + } + result = add(result, 5); + i = + i 1; +} +`; +function main() { + const codeCoverageData = JSON.parse( + `[{"index":0,"line":1,"col":1,"covers":2},{"index":28,"line":5,"col":1,"covers":1},{"index":44,"line":6,"col":1,"covers":1},{"index":55,"line":7,"col":1,"covers":1},{"index":66,"line":8,"col":5,"covers":11},{"index":104,"line":11,"col":5,"covers":10},{"index":19,"line":2,"col":5,"covers":10},{"index":133,"line":12,"col":5,"covers":10},{"index":87,"line":9,"col":9,"covers":1}]` + ); + const flameGraphData = JSON.parse( + `{"fn":0,"acc":257,"parent":0,"children":[{"fn":18,"acc":251,"parent":0,"children":[{"fn":12,"acc":30,"parent":1,"children":[]}]}]}` + ); + const viewRadios = document.querySelectorAll('input[name="views"]'); + for (const input of viewRadios) { + input.addEventListener("input", (ev) => { + console.log(ev); + }); + } + loadCodeCoverage(codeData, codeCoverageData); + loadFlameGraph(flameGraphData, { + 0: "", + 12: "add", + 18: "main" + }); + drawText(codeData, codeCoverageData); +} +main(); diff --git a/web/public/dist/bytes.esm.js b/web/public/dist/bytes.esm.js new file mode 100644 index 0000000..893ea5f --- /dev/null +++ b/web/public/dist/bytes.esm.js @@ -0,0 +1,318 @@ +// 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 +}; diff --git a/web/public/index.html b/web/public/index.html index c0ea51d..c0f4bce 100644 --- a/web/public/index.html +++ b/web/public/index.html @@ -3,13 +3,35 @@ - + - -
-
-
-
-

+    
+        
+
+
Status: Running +
+
+ +
+
+
+

+                    
Process is currently running
+
+
+
diff --git a/web/public/index.js b/web/public/src/index.ts similarity index 83% rename from web/public/index.js rename to web/public/src/index.ts index 7a9d9e1..eb0fa47 100644 --- a/web/public/index.js +++ b/web/public/src/index.ts @@ -1,9 +1,18 @@ const codeCoverageDiv = document.querySelector("#code-coverage"); const flameGraphDiv = document.querySelector("#flame-graph"); -function drawText(text, codeCoverageData) { +type CodeCovEntry = { + index: number; + line: number; + col: number; + covers: number; +}; +function drawText(text: string, codeCoverageData: CodeCovEntry[]) { const tooltip = document.getElementById("covers-tooltip"); - const entries = codeCoverageData.toSorted((a, b) => b.index - a.index); + const entries = codeCoverageData.toSorted(( + a: CodeCovEntry, + b: CodeCovEntry, + ) => b.index - a.index); const charEntries = {}; const elements = []; let line = 1; @@ -79,7 +88,7 @@ function loadCodeCoverage(text, codeCoverageData) { ctx.font = "20px monospace"; const { width: chWidth } = ctx.measureText("-"); - const chHeight = 26; + const chHeight = 23; const color = (ratio) => `rgba(${255 - 255 * ratio}, ${255 * ratio}, 125, 0.5)`; @@ -226,18 +235,29 @@ loop { } `; -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}]`, -); +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 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", -}); -drawText(codeData, codeCoverageData); + const viewRadios = document.querySelectorAll('input[name="views"]'); + for (const input of viewRadios) { + input.addEventListener("input", (ev) => { + console.log(ev); + }); + } + + loadCodeCoverage(codeData, codeCoverageData); + loadFlameGraph(flameGraphData, { + 0: "", + 12: "add", + 18: "main", + }); + drawText(codeData, codeCoverageData); +} + +main(); diff --git a/web/public/style.css b/web/public/style.css index c1a4d71..8f726f0 100644 --- a/web/public/style.css +++ b/web/public/style.css @@ -5,6 +5,11 @@ --bg-1: #2b2d31; --bg-2: #313338; --fg-2: #666666; + + --black: #211F1C; + --black-transparent: #211F1Caa; + --white: #ECEBE9; + --code-status: var(--white); } * { @@ -14,22 +19,75 @@ body { margin: 0; height: 100vh; - background-color: var(--bg-1); + background-color: var(--black); + color: var(--white); +} + +body.status-error { + --code-status: #FF595E; +} + +body.status-waiting { + --code-status: #E3B23C; +} + +body.status-done { + --code-status: #63A46C; } main { - margin-top: 20px; - margin-bottom: 20px; - margin-left: auto; - margin-right: auto; + position: relative; + flex: 1; + padding: 1rem; +} +main > :not(#cover) { + margin: 0 auto; +} + +main #cover { + position: absolute; + inset: 0; + display: flex; + justify-content: center; + align-items: center; + background-color: var(--black-transparent); + font-size: 2.5em; + + border-radius: 0.25rem; + border: 2px solid var(--code-status); +} + +.status-header { + font-size: 1.75rem; + padding: 1rem; + background-color: var(--code-status); + color: var(--black); +} +.status-header-content { + margin: 0 auto; + max-width: 1500px; +} + +#views-nav { + display: flex; + flex-direction: column; + padding: 1rem; + border-radius: 0.25rem; + border: 2px solid var(--code-status); + gap: 0.5rem; +} + +#views-layout { + display: flex; + margin: 0 auto; + padding: 1rem; + gap: 1rem; max-width: 1500px; - width: 100%; } #code-coverage { width: 1000px; height: 500px; - margin: 20px; background-color: rgb(240, 220, 200); } #code-coverage pre { @@ -63,7 +121,6 @@ main { #flame-graph { width: 1004px; height: 504px; - margin: 20px; background-color: var(--bg-2); border: 2px solid rgb(240, 220, 200); padding: 2px;