Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 52 additions & 7 deletions benchmarks/core.bench.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,57 @@
import { bench, describe } from "vitest";
/// <reference types="node" />

import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { arch } from "node:os";
import { dirname, resolve } from "node:path";
import { describe, test } from "vitest";
import {
type BenchmarkBaseline,
buildBaseline,
compareToBaseline,
formatComparison,
formatTable,
recordBenchmark,
} from "./report.js";
import { coreWorkloads } from "./workloads.js";

const options = { time: 750, warmupTime: 250 };
// Vitest 5 moved timing from the per-`bench()` options to the run options; the
// values mirror the pre-5.0 defaults so historical baselines stay comparable.
const runOptions = { time: 750, warmupTime: 250 };

// Vitest 5 dropped the `--outputJson`/`--compare` CLI flags; baseline and
// comparison are selected through env vars set by the npm scripts.
const baselineOut = process.env.BENCH_BASELINE_OUT;
const baselineIn = process.env.BENCH_BASELINE_IN;

describe("core workloads", () => {
for (const [name, workload] of Object.entries(coreWorkloads)) {
bench(name, () => {
workload();
}, options);
}
test("core", async ({ bench }) => {
const records = [];
for (const [name, workload] of Object.entries(coreWorkloads)) {
const result = await bench(name, () => {
workload();
}).run(runOptions);
records.push(recordBenchmark(name, result));
}

if (baselineIn) {
const baseline = JSON.parse(readFileSync(resolve(baselineIn), "utf8")) as BenchmarkBaseline;
process.stdout.write(`\n${formatComparison(compareToBaseline(records, baseline))}\n`);
return;
}

process.stdout.write(`\n${formatTable(records)}\n`);

if (baselineOut) {
const path = resolve(baselineOut);
mkdirSync(dirname(path), { recursive: true });
const baseline = buildBaseline(records, {
node: process.version,
arch: arch(),
platform: process.platform,
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
});
writeFileSync(path, `${JSON.stringify(baseline, null, 2)}\n`);
process.stdout.write(`\nbaseline written to ${baselineOut}\n`);
}
});
});
139 changes: 139 additions & 0 deletions benchmarks/report.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
/// <reference types="node" />

// Vitest 5 removed the built-in benchmark table, `--outputJson`, and `--compare`.
// `bench.compare()` now only runs the registrations and hands back their raw
// statistics; formatting, baselines, and comparison are the caller's job. These
// pure helpers keep that logic out of the `.bench.ts` file so it can be tested.

// Minimal shape of the tinybench statistics we consume; kept local so the
// helpers do not couple to vitest's evolving benchmark types.
export interface BenchStatistics {
mean: number;
rme: number;
}

export interface BenchResultLike {
latency: BenchStatistics;
throughput: BenchStatistics;
}

export interface RecordedBenchmark {
name: string;
/** Mean wall-clock latency per operation, in milliseconds. */
latencyMeanMs: number;
/** Mean throughput, in operations per second. */
throughputMean: number;
/** Relative margin of error on latency, as a percentage. */
rme: number;
}

export interface BenchmarkBaseline {
schemaVersion: 1;
createdAt: string;
node: string;
arch: string;
platform: string;
timezone: string;
benchmarks: Record<string, RecordedBenchmark>;
}

export function recordBenchmark(name: string, result: BenchResultLike): RecordedBenchmark {
return {
name,
latencyMeanMs: result.latency.mean,
throughputMean: result.throughput.mean,
rme: result.latency.rme,
};
}

function padEnd(value: string, width: number): string {
return value.length >= width ? value : value + " ".repeat(width - value.length);
}

function padStart(value: string, width: number): string {
return value.length >= width ? value : " ".repeat(width - value.length) + value;
}

function formatNumber(value: number, fractionDigits: number): string {
if (!Number.isFinite(value)) return "n/a";
return value.toLocaleString("en-US", {
minimumFractionDigits: fractionDigits,
maximumFractionDigits: fractionDigits,
});
}

export function formatTable(records: RecordedBenchmark[]): string {
const header = ["name", "ops/sec", "mean ms", "±rme"];
const rows = records.map((record) => [
record.name,
formatNumber(record.throughputMean, 0),
formatNumber(record.latencyMeanMs, 4),
`±${formatNumber(record.rme, 2)}%`,
]);
const widths = header.map((cell, column) =>
Math.max(cell.length, ...rows.map((row) => row[column]!.length)),
);
const render = (cells: string[]) =>
cells
.map((cell, column) => (column === 0 ? padEnd(cell, widths[column]!) : padStart(cell, widths[column]!)))
.join(" ");
return [render(header), render(widths.map((width) => "-".repeat(width))), ...rows.map(render)].join("\n");
}

export interface ComparisonRow {
name: string;
current: RecordedBenchmark;
baseline?: RecordedBenchmark;
/** Signed percentage change in throughput vs. baseline; positive is faster. */
throughputDeltaPct?: number;
}

export function compareToBaseline(
current: RecordedBenchmark[],
baseline: BenchmarkBaseline,
): ComparisonRow[] {
return current.map((record) => {
const previous = baseline.benchmarks[record.name];
if (!previous || previous.throughputMean === 0) {
return { name: record.name, current: record, baseline: previous };
}
const throughputDeltaPct =
((record.throughputMean - previous.throughputMean) / previous.throughputMean) * 100;
return { name: record.name, current: record, baseline: previous, throughputDeltaPct };
});
}

export function formatComparison(rows: ComparisonRow[]): string {
const header = ["name", "ops/sec", "baseline", "change"];
const body = rows.map((row) => [
row.name,
formatNumber(row.current.throughputMean, 0),
row.baseline ? formatNumber(row.baseline.throughputMean, 0) : "new",
row.throughputDeltaPct === undefined
? "—"
: `${row.throughputDeltaPct >= 0 ? "+" : ""}${formatNumber(row.throughputDeltaPct, 2)}%`,
]);
const widths = header.map((cell, column) =>
Math.max(cell.length, ...body.map((cells) => cells[column]!.length)),
);
const render = (cells: string[]) =>
cells
.map((cell, column) => (column === 0 ? padEnd(cell, widths[column]!) : padStart(cell, widths[column]!)))
.join(" ");
return [render(header), render(widths.map((width) => "-".repeat(width))), ...body.map(render)].join("\n");
}

export function buildBaseline(
records: RecordedBenchmark[],
meta: Pick<BenchmarkBaseline, "node" | "arch" | "platform" | "timezone"> & { createdAt?: string },
): BenchmarkBaseline {
return {
schemaVersion: 1,
createdAt: meta.createdAt ?? new Date().toISOString(),
node: meta.node,
arch: meta.arch,
platform: meta.platform,
timezone: meta.timezone,
benchmarks: Object.fromEntries(records.map((record) => [record.name, record])),
};
}
3 changes: 2 additions & 1 deletion docs/TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -174,12 +174,13 @@ Keep the generalized property and add the minimized example to the nearest ordin
## Benchmarks and profiles

```bash
npm run bench
npm run bench:baseline
npm run bench:compare
npm run profile:core
```

Compare benchmarks only on the same machine, Node version, architecture, timezone, and power state. Profiles are written to `.artifacts/profiles/`; load the `.cpuprofile` in a V8-compatible viewer. Shared benchmark workloads live in `benchmarks/workloads.ts`.
`npm run bench` prints a table of throughput, mean latency, and margin of error. `bench:baseline` writes those results to `.artifacts/benchmarks/baseline.json`, and `bench:compare` reports the throughput change of a fresh run against that baseline. Compare benchmarks only on the same machine, Node version, architecture, timezone, and power state. Profiles are written to `.artifacts/profiles/`; load the `.cpuprofile` in a V8-compatible viewer. Shared benchmark workloads live in `benchmarks/workloads.ts`.

## Change-specific minimums

Expand Down
Loading
Loading