diff --git a/.changeset/odd-rice-follow.md b/.changeset/odd-rice-follow.md new file mode 100644 index 00000000..acdd7297 --- /dev/null +++ b/.changeset/odd-rice-follow.md @@ -0,0 +1,5 @@ +--- +"@nodesecure/js-x-ray": patch +--- + +Store benchmark results in a readable Markdown table (report.md) instead of raw JSON. diff --git a/workspaces/js-x-ray/benchmark/markdown.ts b/workspaces/js-x-ray/benchmark/markdown.ts new file mode 100644 index 00000000..3e0c7a89 --- /dev/null +++ b/workspaces/js-x-ray/benchmark/markdown.ts @@ -0,0 +1,97 @@ +const KB = 1_024; +const MB = 1_024 ** 2; +const GB = 1_024 ** 3; + +export interface BenchmarkReport { + timestamp: string; + runtime: string; + cpu: { name: string; freq: number; }; + benchmarks: Array<{ + name: string; + stats: { + min: number; + max: number; + p25: number; + p50: number; + p75: number; + p99: number; + p999: number; + avg: number; + ticks: number; + heap?: { avg: number; }; + gc?: { avg: number; }; + }; + }>; +} + +/** + * mitata reports timings in nanoseconds. Picks the most readable unit. + */ +function formatDuration(nanoseconds: number): string { + if (nanoseconds < 1_000) { + return `${nanoseconds.toFixed(2)} ns`; + } + if (nanoseconds < 1_000_000) { + return `${(nanoseconds / 1_000).toFixed(2)} µs`; + } + if (nanoseconds < 1_000_000_000) { + return `${(nanoseconds / 1_000_000).toFixed(2)} ms`; + } + + return `${(nanoseconds / 1_000_000_000).toFixed(2)} s`; +} + +/** + * heap stats are reported in bytes. + */ +function formatBytes(bytes: number): string { + if (bytes < KB) { + return `${bytes.toFixed(0)} B`; + } + if (bytes < MB) { + return `${(bytes / KB).toFixed(2)} KB`; + } + if (bytes < GB) { + return `${(bytes / MB).toFixed(2)} MB`; + } + + return `${(bytes / GB).toFixed(2)} GB`; +} + +export function toMarkdown(report: BenchmarkReport): string { + const header = [ + "Benchmark", "min", "max", "p25", "p50", "p75", "p99", "p999", "avg", "samples", "heap (avg)", "gc (avg)" + ]; + + const lines = [ + "# Benchmark Report", + "", + `- **Timestamp:** ${report.timestamp}`, + `- **Runtime:** ${report.runtime}`, + `- **CPU:** ${report.cpu.name} (~${report.cpu.freq.toFixed(2)} GHz)`, + "", + `| ${header.join(" | ")} |`, + `| ${header.map(() => "---").join(" | ")} |` + ]; + + for (const { name, stats } of report.benchmarks) { + const row = [ + name, + formatDuration(stats.min), + formatDuration(stats.max), + formatDuration(stats.p25), + formatDuration(stats.p50), + formatDuration(stats.p75), + formatDuration(stats.p99), + formatDuration(stats.p999), + formatDuration(stats.avg), + String(stats.ticks), + stats.heap ? formatBytes(stats.heap.avg) : "—", + stats.gc ? formatDuration(stats.gc.avg) : "—" + ]; + + lines.push(`| ${row.join(" | ")} |`); + } + + return lines.join("\n") + "\n"; +} diff --git a/workspaces/js-x-ray/benchmark/report.json b/workspaces/js-x-ray/benchmark/report.json deleted file mode 100644 index 6d136049..00000000 --- a/workspaces/js-x-ray/benchmark/report.json +++ /dev/null @@ -1,269 +0,0 @@ -{ - "timestamp": "2026-05-31T02:15:13.460Z", - "runtime": "node", - "cpu": { - "name": "AMD EPYC 9V74 80-Core Processor", - "freq": 2.7471334664153297 - }, - "benchmarks": [ - { - "stats": { - "kind": "fn", - "min": 183105, - "max": 1147855, - "p25": 214843, - "p50": 244467, - "p75": 352250, - "p99": 701234, - "p999": 1014545, - "avg": 291370.8957286432, - "ticks": 2388, - "heap": { - "_": 1910, - "total": 510270624, - "min": 3552, - "max": 2654200, - "avg": 267157.39476439793 - } - }, - "args": {}, - "name": "Small File (jscrush.js - 1.03KB)" - }, - { - "stats": { - "kind": "fn", - "min": 506141, - "max": 2216802, - "p25": 562886, - "p50": 610708, - "p75": 863087, - "p99": 1313024, - "p999": 1939304, - "avg": 705367.7884615385, - "ticks": 988, - "heap": { - "_": 805, - "total": 390169824, - "min": 728, - "max": 2189064, - "avg": 484683.01118012425 - } - }, - "args": {}, - "name": "Small File (npm-audit.js - 1.46KB)" - }, - { - "stats": { - "kind": "fn", - "min": 1499955, - "max": 3834603, - "p25": 1586144, - "p50": 1658173, - "p75": 1987717, - "p99": 3077856, - "p999": 3414622, - "avg": 1837781.1909814323, - "ticks": 377, - "heap": { - "_": 369, - "total": 259100864, - "min": 41664, - "max": 2024536, - "avg": 702170.3631436315 - } - }, - "args": {}, - "name": "Small File (forbes-skimmer.js - 2.15KB)" - }, - { - "stats": { - "kind": "fn", - "min": 1341766, - "max": 4508005, - "p25": 1421306, - "p50": 1530881, - "p75": 1821729, - "p99": 3333930, - "p999": 4413593, - "avg": 1696185.4152334153, - "ticks": 407, - "heap": { - "_": 395, - "total": 283735208, - "min": 85408, - "max": 2429176, - "avg": 718316.982278481 - } - }, - "args": {}, - "name": "Small File (rate-map.js - 2.21KB)" - }, - { - "stats": { - "kind": "fn", - "min": 1465161.9999980927, - "max": 4237409, - "p25": 1540856, - "p50": 1684772, - "p75": 1992725, - "p99": 3112839, - "p999": 3596565, - "avg": 1827438.6392573146, - "ticks": 377, - "heap": { - "_": 370, - "total": 346951592, - "min": 1352, - "max": 2411688, - "avg": 937707.0054054054 - } - }, - "args": {}, - "name": "Small File (event-stream.js - 3.76KB)" - }, - { - "stats": { - "kind": "fn", - "min": 1058901, - "max": 2879427, - "p25": 1131230, - "p50": 1231591, - "p75": 1407035, - "p99": 2127117, - "p999": 2798075.0000009537, - "avg": 1313450.7670454618, - "ticks": 528, - "heap": { - "_": 518, - "total": 401908224, - "min": 35368, - "max": 1996120, - "avg": 775884.6023166024 - } - }, - "args": {}, - "name": "Small File (modrrnize.js - 9.28KB)" - }, - { - "stats": { - "kind": "fn", - "min": 1071451, - "max": 3882735, - "p25": 1169298, - "p50": 1256548, - "p75": 1410751, - "p99": 2367748, - "p999": 2879458, - "avg": 1334653.4566473989, - "ticks": 519, - "heap": { - "_": 515, - "total": 394701408, - "min": 42672, - "max": 2611568, - "avg": 766410.5009708737 - } - }, - "args": {}, - "name": "Small File (smith.js - 9.28KB)" - }, - { - "stats": { - "kind": "fn", - "min": 2170172, - "max": 4759103, - "p25": 2250172, - "p50": 2579077, - "p75": 2685596, - "p99": 4296487, - "p999": 4705912, - "avg": 2582440.4210526315, - "ticks": 266, - "heap": { - "_": 263, - "total": 367423664, - "min": 4048, - "max": 2785840, - "avg": 1397048.1520912547 - } - }, - "args": {}, - "name": "Medium File (kopiluwak.js - 15.45KB)" - }, - { - "stats": { - "kind": "fn", - "min": 73442120, - "max": 79203007, - "p25": 75315575, - "p50": 75690579, - "p75": 76291331, - "p99": 78556516, - "p999": 78556516, - "avg": 76256814.6, - "ticks": 10, - "heap": { - "_": 14, - "total": 461557832, - "min": 26429840, - "max": 35289552, - "avg": 32968416.57142857 - }, - "gc": { - "total": 202504783, - "min": 13314961, - "max": 18337921, - "avg": 14464627.357142856 - } - }, - "args": {}, - "name": "Large File (obfuscate.js - 89.57KB)" - }, - { - "stats": { - "kind": "fn", - "min": 208023, - "max": 1729049, - "p25": 249334, - "p50": 286030, - "p75": 384237, - "p99": 714985, - "p999": 1129077, - "avg": 330014.17719714966, - "ticks": 2105, - "heap": { - "_": 1659, - "total": 452366448, - "min": 1320, - "max": 1263872, - "avg": 272674.16998191684 - } - }, - "args": {}, - "name": "jscrush.js" - }, - { - "stats": { - "kind": "fn", - "min": 39495558, - "max": 51692329, - "p25": 44062612, - "p50": 45607625, - "p75": 46064511, - "p99": 48203065, - "p999": 48203065, - "avg": 45576307.36363637, - "ticks": 11, - "heap": { - "_": 11, - "total": 233409312, - "min": 19239064, - "max": 33042392, - "avg": 21219028.363636363 - } - }, - "args": {}, - "name": "obfuscate.js" - } - ] -} \ No newline at end of file diff --git a/workspaces/js-x-ray/benchmark/report.md b/workspaces/js-x-ray/benchmark/report.md new file mode 100644 index 00000000..107ece89 --- /dev/null +++ b/workspaces/js-x-ray/benchmark/report.md @@ -0,0 +1,19 @@ +# Benchmark Report + +- **Timestamp:** 2026-06-22T19:58:11.748Z +- **Runtime:** node +- **CPU:** unknown (~3.08 GHz) + +| Benchmark | min | max | p25 | p50 | p75 | p99 | p999 | avg | samples | heap (avg) | gc (avg) | +| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | +| Small File (jscrush.js - 1.06KB) | 235.54 µs | 931.33 µs | 248.63 µs | 259.33 µs | 273.92 µs | 477.83 µs | 814.42 µs | 270.78 µs | 2586 | 270.78 KB | — | +| Small File (npm-audit.js - 1.51KB) | 600.63 µs | 1.57 ms | 631.00 µs | 644.04 µs | 660.63 µs | 1.03 ms | 1.13 ms | 661.85 µs | 1056 | 429.21 KB | — | +| Small File (forbes-skimmer.js - 2.20KB) | 1.50 ms | 3.49 ms | 1.53 ms | 1.55 ms | 1.60 ms | 2.82 ms | 3.12 ms | 1.61 ms | 430 | 666.61 KB | — | +| Small File (rate-map.js - 2.30KB) | 1.24 ms | 3.03 ms | 1.28 ms | 1.30 ms | 1.35 ms | 2.51 ms | 2.83 ms | 1.37 ms | 508 | 702.45 KB | — | +| Small File (event-stream.js - 3.84KB) | 1.74 ms | 3.23 ms | 1.78 ms | 1.81 ms | 1.86 ms | 2.44 ms | 2.89 ms | 1.85 ms | 374 | 878.08 KB | — | +| Small File (modrrnize.js - 9.31KB) | 1.05 ms | 2.34 ms | 1.08 ms | 1.10 ms | 1.14 ms | 1.82 ms | 2.14 ms | 1.15 ms | 607 | 757.93 KB | — | +| Small File (smith.js - 9.31KB) | 1.06 ms | 2.40 ms | 1.08 ms | 1.09 ms | 1.12 ms | 1.71 ms | 2.23 ms | 1.13 ms | 616 | 744.71 KB | — | +| Medium File (kopiluwak.js - 15.53KB) | 2.39 ms | 5.21 ms | 2.42 ms | 2.43 ms | 2.50 ms | 4.53 ms | 5.09 ms | 2.54 ms | 271 | 1.34 MB | — | +| Large File (obfuscate.js - 89.57KB) | 77.03 ms | 83.01 ms | 77.54 ms | 78.61 ms | 80.19 ms | 82.61 ms | 82.61 ms | 79.44 ms | 11 | 31.31 MB | 7.84 ms | +| jscrush.js | 248.00 µs | 931.00 µs | 263.17 µs | 275.21 µs | 290.88 µs | 527.92 µs | 890.13 µs | 288.19 µs | 2432 | 267.47 KB | — | +| obfuscate.js | 52.78 ms | 81.45 ms | 53.29 ms | 56.26 ms | 60.27 ms | 66.76 ms | 66.76 ms | 59.41 ms | 12 | 20.47 MB | — | diff --git a/workspaces/js-x-ray/benchmark/write-snapshot.ts b/workspaces/js-x-ray/benchmark/write-snapshot.ts index 028a4b49..23037221 100644 --- a/workspaces/js-x-ray/benchmark/write-snapshot.ts +++ b/workspaces/js-x-ray/benchmark/write-snapshot.ts @@ -3,8 +3,9 @@ import { writeFileSync } from "node:fs"; // Import Internal Dependencies import { benchmark } from "./bench.ts"; +import { toMarkdown } from "./markdown.ts"; -const kReportURL = new URL("report.json", import.meta.url); +const kReportMarkdownURL = new URL("report.md", import.meta.url); const results = await benchmark(); @@ -18,6 +19,7 @@ const relevantResults = { if (!trial.stats) { return []; } + const { samples, debug, ...rest } = trial.stats; return { @@ -29,4 +31,5 @@ const relevantResults = { }) }; -writeFileSync(kReportURL, JSON.stringify(relevantResults, null, 2)); +// Human-readable snapshot as a Markdown table. +writeFileSync(kReportMarkdownURL, toMarkdown(relevantResults)); diff --git a/workspaces/js-x-ray/test/markdown.spec.ts b/workspaces/js-x-ray/test/markdown.spec.ts new file mode 100644 index 00000000..0e6d62d3 --- /dev/null +++ b/workspaces/js-x-ray/test/markdown.spec.ts @@ -0,0 +1,109 @@ +// Import Node.js Dependencies +import assert from "node:assert"; +import { describe, test } from "node:test"; + +// Import Internal Dependencies +import { toMarkdown } from "../benchmark/markdown.ts"; + +describe("toMarkdown", () => { + const baseReport = { + timestamp: "2025-01-15T12:00:00.000Z", + runtime: "node v24.0.0", + cpu: { name: "Apple M1", freq: 3.20 } + }; + + test("should render the header section with metadata", () => { + const md = toMarkdown({ ...baseReport, benchmarks: [] }); + + assert.ok(md.startsWith("# Benchmark Report\n")); + assert.ok(md.includes("- **Timestamp:** 2025-01-15T12:00:00.000Z")); + assert.ok(md.includes("- **Runtime:** node v24.0.0")); + assert.ok(md.includes("- **CPU:** Apple M1 (~3.20 GHz)")); + }); + + test("should render the table header and separator rows", () => { + const md = toMarkdown({ ...baseReport, benchmarks: [] }); + const lines = md.split("\n"); + + const headerLine = lines.find((l) => l.startsWith("| Benchmark")); + assert.ok(headerLine); + assert.ok(headerLine.includes("heap (avg)")); + assert.ok(headerLine.includes("gc (avg)")); + + const separatorLine = lines.find((l) => l.startsWith("| ---")); + assert.ok(separatorLine); + }); + + test("should use — fallback when heap and gc are absent", () => { + const md = toMarkdown({ + ...baseReport, + benchmarks: [{ + name: "test-bench", + stats: { + min: 100, max: 200, p25: 120, p50: 150, + p75: 180, p99: 195, p999: 199, avg: 150, + ticks: 1000 + } + }] + }); + + const dataLine = md.split("\n").find((l) => l.includes("test-bench")); + assert.ok(dataLine); + + const cells = dataLine.split("|").map((c) => c.trim()).filter(Boolean); + // heap (avg) is column 11, gc (avg) is column 12 + assert.strictEqual(cells[10], "—"); + assert.strictEqual(cells[11], "—"); + }); + + test("should render heap and gc values when present", () => { + const md = toMarkdown({ + ...baseReport, + benchmarks: [{ + name: "test-bench", + stats: { + min: 1_000_000, max: 2_000_000, + p25: 1_200_000, p50: 1_500_000, + p75: 1_800_000, p99: 1_950_000, + p999: 1_990_000, avg: 1_500_000, + ticks: 500, + heap: { avg: 5_242_880 }, + gc: { avg: 50_000 } + } + }] + }); + + const dataLine = md.split("\n").find((l) => l.includes("test-bench")); + assert.ok(dataLine); + + const cells = dataLine.split("|").map((c) => c.trim()).filter(Boolean); + assert.strictEqual(cells[10], "5.00 MB"); + assert.strictEqual(cells[11], "50.00 µs"); + }); + + test("should render multiple benchmark rows", () => { + const stats = { + min: 100, max: 200, p25: 120, p50: 150, + p75: 180, p99: 195, p999: 199, avg: 150, + ticks: 1000 + }; + + const md = toMarkdown({ + ...baseReport, + benchmarks: [ + { name: "bench-a", stats }, + { name: "bench-b", stats } + ] + }); + + const lines = md.split("\n"); + assert.ok(lines.some((l) => l.includes("bench-a"))); + assert.ok(lines.some((l) => l.includes("bench-b"))); + }); + + test("should end with a trailing newline", () => { + const md = toMarkdown({ ...baseReport, benchmarks: [] }); + + assert.ok(md.endsWith("\n")); + }); +});