Merge pull request #742 from vercel-labs/perf/rebased/harness

Add a runtime benchmark suite that times compiled programs against Node
This commit is contained in:
Malte Ubl
2026-10-08 13:44:54 -05:00
committed by GitHub
16 changed files with 1466 additions and 0 deletions
+27
View File
@@ -0,0 +1,27 @@
# Runtime benchmarks
Application-shaped workloads for measuring the speed of compiled executables. Each workload is deterministic, generates its own input (or reads a generated file), and prints a summary that must match Node.js byte-for-byte before it is timed.
| Workload | Exercises |
| --- | --- |
| `json-records` | `JSON.stringify`, typed `JSON.parse`, Map aggregation |
| `regex-logs` | regex `exec` with groups, `test`, global `replace` |
| `ast-interp` | tokenizer, recursive-descent parser, class dispatch |
| `records-sort` | comparator sorts, group-by, `reduce`/`filter` |
| `template-render` | template literals, escaping, `map`/`join` |
| `functional-pipeline` | `filter`/`map`/`reduce`/`flatMap` chains, closures, small records |
| `async-pipeline` | `async`/`await`, `Promise.all` fan-out |
| `word-graph` | string-keyed Map/Set, BFS |
| `alloc-trees` | allocation-heavy trees, immutable object-spread updates |
| `numeric-kernels` | sieve, hashing, `number[][]` matmul, `Float64Array` |
| `validate-errors` | throw/catch on a fraction of inputs, small call chains |
| `csv-numbers` | `split`, `Number`, `parseInt`, `toFixed` |
| `log-summary`, `inventory-report` | the multi-module applications from `benchmarks/builds`, on 300k-line inputs |
Compare two checkouts (each with a built CLI and native artifacts for the host):
```console
$ pnpm bench:runtime --baseline=../scriptc-main --candidate=. --runs=15 --json=result.json
```
Without `--baseline`, the runner reports absolute medians. Baseline and candidate launches are interleaved, and each workload reports the median ratio with a bootstrap 95% confidence interval. `faster`/`slower` verdicts require the whole interval on one side of 1.0. A per-host lock serializes the timed phase so concurrent runs on one machine do not distort each other. Executables that time out or diverge from Node are reported, never timed.
+108
View File
@@ -0,0 +1,108 @@
{
"workloads": [
{
"name": "json-records",
"entry": "workloads/json-records.ts",
"args": [
"4"
]
},
{
"name": "regex-logs",
"entry": "workloads/regex-logs.ts",
"args": [
"1"
]
},
{
"name": "ast-interp",
"entry": "workloads/ast-interp.ts",
"args": [
"5"
]
},
{
"name": "records-sort",
"entry": "workloads/records-sort.ts",
"args": [
"1"
]
},
{
"name": "template-render",
"entry": "workloads/template-render.ts",
"args": [
"1"
]
},
{
"name": "functional-pipeline",
"entry": "workloads/functional-pipeline.ts",
"args": [
"1"
]
},
{
"name": "async-pipeline",
"entry": "workloads/async-pipeline.ts",
"args": [
"1"
]
},
{
"name": "word-graph",
"entry": "workloads/word-graph.ts",
"args": [
"2"
]
},
{
"name": "alloc-trees",
"entry": "workloads/alloc-trees.ts",
"args": [
"1"
]
},
{
"name": "numeric-kernels",
"entry": "workloads/numeric-kernels.ts",
"args": [
"0.5"
]
},
{
"name": "validate-errors",
"entry": "workloads/validate-errors.ts",
"args": [
"1"
]
},
{
"name": "csv-numbers",
"entry": "workloads/csv-numbers.ts",
"args": [
"1"
]
},
{
"name": "log-summary",
"entry": "../builds/log-summary/main.ts",
"input": "log-summary",
"args": [
"{input}",
"--min-status",
"200",
"--limit",
"12"
]
},
{
"name": "inventory-report",
"entry": "../builds/inventory-report/main.ts",
"input": "inventory-report",
"args": [
"{input}"
]
}
]
}
@@ -0,0 +1,46 @@
// Allocation-heavy code: binary trees built and discarded, plus a
// reducer that produces immutable state updates with object spread.
class TreeNode {
readonly left: TreeNode | null;
readonly right: TreeNode | null;
constructor(left: TreeNode | null, right: TreeNode | null) {
this.left = left;
this.right = right;
}
check(): number {
return 1 + (this.left === null ? 0 : this.left.check()) + (this.right === null ? 0 : this.right.check());
}
}
function build(depth: number): TreeNode {
return depth === 0 ? new TreeNode(null, null) : new TreeNode(build(depth - 1), build(depth - 1));
}
interface State {
count: number;
total: number;
last: string;
history: number[];
}
interface Action {
type: string;
amount: number;
}
function reduce(state: State, action: Action): State {
if (action.type === "add") return { ...state, count: state.count + 1, total: state.total + action.amount, last: action.type };
if (action.type === "reset") return { ...state, total: 0, last: action.type, history: [...state.history.slice(-4), state.total] };
return { ...state, last: action.type };
}
const scale = Number(process.argv[2] ?? "1");
const maxDepth = 14;
let checks = 0;
for (let depth = 4; depth <= maxDepth; depth += 2) {
const iterations = Math.floor((1 << (maxDepth - depth + 4)) * scale);
for (let i = 0; i < iterations; i++) checks += build(depth).check();
}
const longLived = build(maxDepth);
let state: State = { count: 0, total: 0, last: "", history: [] };
for (let i = 0; i < Math.floor(300000 * scale); i++) {
state = reduce(state, { type: i % 97 === 0 ? "reset" : i % 3 === 0 ? "noop" : "add", amount: i % 13 });
}
console.log("checks", checks, "long", longLived.check(), "state", state.count, state.total, state.history.join(","));
+199
View File
@@ -0,0 +1,199 @@
// A tokenizer, recursive-descent parser, and class-based tree-walking
// evaluator for a small expression language with variables.
abstract class Expr {
abstract evaluate(env: Map<string, number>): number;
}
class Num extends Expr {
readonly value: number;
constructor(value: number) {
super();
this.value = value;
}
evaluate(_env: Map<string, number>): number {
return this.value;
}
}
class Var extends Expr {
readonly name: string;
constructor(name: string) {
super();
this.name = name;
}
evaluate(env: Map<string, number>): number {
return env.get(this.name) ?? 0;
}
}
class Binary extends Expr {
readonly op: string;
readonly left: Expr;
readonly right: Expr;
constructor(op: string, left: Expr, right: Expr) {
super();
this.op = op;
this.left = left;
this.right = right;
}
evaluate(env: Map<string, number>): number {
const l = this.left.evaluate(env);
const r = this.right.evaluate(env);
switch (this.op) {
case "+":
return l + r;
case "-":
return l - r;
case "*":
return l * r;
case "/":
return r === 0 ? 0 : l / r;
default:
return l % (r === 0 ? 1 : r);
}
}
}
class Call extends Expr {
readonly name: string;
readonly args: Expr[];
constructor(name: string, args: Expr[]) {
super();
this.name = name;
this.args = args;
}
evaluate(env: Map<string, number>): number {
const values = this.args.map((arg) => arg.evaluate(env));
if (this.name === "max") return Math.max(...values);
if (this.name === "min") return Math.min(...values);
return values.reduce((a, b) => a + b, 0);
}
}
type TokenKind = "num" | "ident" | "op" | "lparen" | "rparen" | "comma" | "end";
interface Token {
kind: TokenKind;
text: string;
}
function isDigit(c: number): boolean {
return (c >= 48 && c <= 57) || c === 46;
}
function isIdent(c: number): boolean {
return (c >= 97 && c <= 122) || c === 95;
}
function tokenize(source: string): Token[] {
const tokens: Token[] = [];
let i = 0;
while (i < source.length) {
const c = source.charCodeAt(i);
if (c === 32) {
i++;
} else if (c >= 48 && c <= 57) {
const start = i;
while (i < source.length && isDigit(source.charCodeAt(i))) i++;
tokens.push({ kind: "num", text: source.slice(start, i) });
} else if (isIdent(c)) {
const start = i;
while (i < source.length && isIdent(source.charCodeAt(i))) i++;
tokens.push({ kind: "ident", text: source.slice(start, i) });
} else if (c === 40) {
tokens.push({ kind: "lparen", text: "(" });
i++;
} else if (c === 41) {
tokens.push({ kind: "rparen", text: ")" });
i++;
} else if (c === 44) {
tokens.push({ kind: "comma", text: "," });
i++;
} else {
tokens.push({ kind: "op", text: source[i]! });
i++;
}
}
tokens.push({ kind: "end", text: "" });
return tokens;
}
class Parser {
private pos = 0;
private readonly tokens: Token[];
constructor(tokens: Token[]) {
this.tokens = tokens;
}
private peek(): Token {
return this.tokens[this.pos]!;
}
private next(): Token {
return this.tokens[this.pos++]!;
}
parseExpr(): Expr {
let left = this.parseTerm();
while (this.peek().kind === "op" && (this.peek().text === "+" || this.peek().text === "-")) {
const op = this.next().text;
left = new Binary(op, left, this.parseTerm());
}
return left;
}
private parseTerm(): Expr {
let left = this.parseFactor();
while (this.peek().kind === "op" && (this.peek().text === "*" || this.peek().text === "/" || this.peek().text === "%")) {
const op = this.next().text;
left = new Binary(op, left, this.parseFactor());
}
return left;
}
private parseFactor(): Expr {
const token = this.next();
if (token.kind === "num") return new Num(Number(token.text));
if (token.kind === "lparen") {
const inner = this.parseExpr();
this.next();
return inner;
}
if (this.peek().kind === "lparen") {
this.next();
const args: Expr[] = [];
while (this.peek().kind !== "rparen") {
args.push(this.parseExpr());
if (this.peek().kind === "comma") this.next();
}
this.next();
return new Call(token.text, args);
}
return new Var(token.text);
}
}
let seed = 7;
function random(): number {
seed = (Math.imul(seed, 1103515245) + 12345) >>> 0;
return seed / 4294967296;
}
const names = ["alpha", "beta", "gamma", "delta", "x", "y"];
const ops = ["+", "-", "*", "/", "%"];
function generate(depth: number): string {
const r = random();
if (depth === 0 || r < 0.2) return r < 0.1 ? String(Math.floor(random() * 100)) : names[Math.floor(random() * names.length)]!;
if (r < 0.3) return `max(${generate(depth - 1)}, ${generate(depth - 1)}, ${generate(depth - 1)})`;
if (r < 0.4) return `(${generate(depth - 1)})`;
return `${generate(depth - 1)} ${ops[Math.floor(random() * ops.length)]!} ${generate(depth - 1)}`;
}
const scale = Number(process.argv[2] ?? "1");
const sources: string[] = [];
for (let i = 0; i < Math.floor(400 * scale); i++) sources.push(generate(7));
const env = new Map<string, number>();
let total = 0;
let chars = 0;
for (const source of sources) {
const tree = new Parser(tokenize(source)).parseExpr();
chars += source.length;
for (let step = 0; step < 40; step++) {
env.set("alpha", step);
env.set("beta", step * 2 + 1);
env.set("gamma", 3);
env.set("delta", step % 7);
env.set("x", 1.5);
env.set("y", -2);
const value = tree.evaluate(env);
if (Number.isFinite(value)) total += value;
}
}
console.log("sources", sources.length, "chars", chars, "total", total.toFixed(6));
@@ -0,0 +1,47 @@
// Async application code: request handlers awaiting cached and computed
// values, Promise.all fan-out, and sequential awaits in loops.
interface User {
id: number;
name: string;
score: number;
}
const cache = new Map<number, User>();
async function loadUser(id: number): Promise<User> {
const cached = cache.get(id);
if (cached !== undefined) return cached;
const user = { id, name: "user-" + id, score: (id * 37) % 101 };
cache.set(id, user);
return user;
}
async function authorize(user: User): Promise<boolean> {
return user.score % 7 !== 0;
}
async function handle(requestId: number): Promise<number> {
const user = await loadUser(requestId % 3000);
if (!(await authorize(user))) return 0;
const requests: Promise<User>[] = [loadUser((requestId + 1) % 3000), loadUser((requestId + 7) % 3000), loadUser((requestId + 13) % 3000)];
const friends = await Promise.all(requests);
let sum = user.score;
for (const friend of friends) sum += friend.score;
return sum;
}
async function batch(start: number, size: number): Promise<number> {
let total = 0;
for (let i = 0; i < size; i++) total += await handle(start + i);
return total;
}
async function main(): Promise<void> {
const scale = Number(process.argv[2] ?? "1");
const batches = Math.floor(200 * scale);
let total = 0;
for (let round = 0; round < batches; round += 10) {
const work: Promise<number>[] = [];
for (let b = 0; b < 10; b++) work.push(batch((round + b) * 100, 100));
const results = await Promise.all(work);
for (const r of results) total += r;
}
console.log("handled", batches * 100, "total", total);
}
void main();
@@ -0,0 +1,43 @@
// CSV parsing and numeric formatting: split, Number(), parseInt,
// toFixed, String(n), and string assembly of a report.
let seed = 1001;
function random(): number {
seed = (Math.imul(seed, 1103515245) + 12345) >>> 0;
return seed / 4294967296;
}
function makeCsv(rows: number): string {
const lines: string[] = ["date,store,units,unit_price,discount"];
for (let i = 0; i < rows; i++) {
const day = 1 + (i % 28);
lines.push(
`2026-${String(1 + (i % 12)).padStart(2, "0")}-${String(day).padStart(2, "0")},store-${Math.floor(random() * 40)},${Math.floor(random() * 50)},${(random() * 200).toFixed(2)},${(random() * 0.3).toFixed(3)}`,
);
}
return lines.join("\n");
}
const scale = Number(process.argv[2] ?? "1");
const csv = makeCsv(Math.floor(150000 * scale));
const totals = new Map<string, number>();
const months: number[] = new Array<number>(13).fill(0);
let units = 0;
let formatted = 0;
for (let round = 0; round < 2; round++) {
const lines = csv.split("\n");
for (let i = 1; i < lines.length; i++) {
const cols = lines[i]!.split(",");
const month = parseInt(cols[0]!.slice(5, 7), 10);
const count = Number(cols[2]);
const price = Number(cols[3]);
const discount = Number(cols[4]);
const revenue = count * price * (1 - discount);
units += count;
months[month] = months[month]! + revenue;
totals.set(cols[1]!, (totals.get(cols[1]!) ?? 0) + revenue);
formatted += (revenue.toFixed(2) + "|" + String(count)).length;
}
}
const stores = [...totals.keys()].sort();
const report = stores.map((s) => `${s}: ${totals.get(s)!.toFixed(2)}`).join("\n");
console.log(report.split("\n").slice(0, 5).join("\n"));
console.log("units", units, "formatted", formatted, "jan", months[1]!.toFixed(2), "dec", months[12]!.toFixed(2));
@@ -0,0 +1,56 @@
// Functional collection pipelines: map/filter/reduce/flatMap chains,
// closures capturing configuration, and small intermediate objects.
interface Event {
user: number;
kind: string;
value: number;
time: number;
}
interface Point {
x: number;
y: number;
}
interface Scored {
user: number;
score: number;
}
let seed = 4242;
function random(): number {
seed = (Math.imul(seed, 1103515245) + 12345) >>> 0;
return seed / 4294967296;
}
const kinds = ["view", "click", "purchase", "share"];
function makeEvents(count: number): Event[] {
const events: Event[] = [];
for (let i = 0; i < count; i++) {
events.push({ user: Math.floor(random() * 5000), kind: kinds[Math.floor(random() * kinds.length)]!, value: Math.floor(random() * 1000), time: i });
}
return events;
}
function makeScorer(weight: number, bonus: number): (e: Event) => number {
return (e) => (e.kind === "purchase" ? e.value * weight + bonus : e.kind === "click" ? weight : 0);
}
const scale = Number(process.argv[2] ?? "1");
const events = makeEvents(Math.floor(200000 * scale));
let total = 0;
for (let round = 0; round < 5; round++) {
const scorer = makeScorer(round + 1, round * 10);
const scored: Scored[] = events
.filter((e) => e.time % 5 !== round)
.map((e) => ({ user: e.user, score: scorer(e) }))
.filter((s) => s.score > 0);
total += scored.reduce((sum, s) => sum + s.score, 0);
const pairs = events.slice(0, 20000).flatMap((e) => (e.kind === "share" ? [e.user, e.value] : []));
total += pairs.length;
const points: Point[] = events.slice(0, 50000).map((e) => ({ x: e.value / 10, y: e.user / 100 }));
const centroid = points.reduce((acc: Point, p: Point): Point => ({ x: acc.x + p.x, y: acc.y + p.y }), { x: 0, y: 0 });
total += Math.round(centroid.x + centroid.y);
const some = events.some((e) => e.value === 999 - round);
const every = events.every((e) => e.value >= 0);
const found = events.findIndex((e) => e.user === 4999 - round);
total += (some ? 1 : 0) + (every ? 1 : 0) + found;
}
console.log("total", total);
@@ -0,0 +1,102 @@
// JSON round trips of typed records: build, stringify, parse back into a
// declared shape, aggregate, and stringify the summary.
interface LineItem {
sku: string;
quantity: number;
price: number;
}
interface Order {
id: number;
customer: string;
region: string;
priority: boolean;
items: LineItem[];
tags: string[];
}
interface Summary {
region: string;
orders: number;
revenue: number;
topSku: string;
}
interface RegionTotals {
orders: number;
revenue: number;
skus: Map<string, number>;
}
let seed = 12345;
function random(): number {
seed = (Math.imul(seed, 1103515245) + 12345) >>> 0;
return seed / 4294967296;
}
const regions = ["north", "south", "east", "west", "central"];
function makeOrders(count: number): Order[] {
const orders: Order[] = [];
for (let i = 0; i < count; i++) {
const items: LineItem[] = [];
const n = 1 + Math.floor(random() * 6);
for (let j = 0; j < n; j++) {
items.push({
sku: "sku-" + Math.floor(random() * 500),
quantity: 1 + Math.floor(random() * 9),
price: Math.round(random() * 10000) / 100,
});
}
orders.push({
id: i,
customer: "customer-" + Math.floor(random() * 2000),
region: regions[i % regions.length]!,
priority: random() < 0.2,
items,
tags: random() < 0.5 ? ["web"] : ["store", "promo"],
});
}
return orders;
}
function summarize(orders: Order[]): Summary[] {
const byRegion = new Map<string, RegionTotals>();
for (const order of orders) {
let entry = byRegion.get(order.region);
if (entry === undefined) {
entry = { orders: 0, revenue: 0, skus: new Map<string, number>() };
byRegion.set(order.region, entry);
}
entry.orders++;
for (const item of order.items) {
entry.revenue += item.quantity * item.price;
entry.skus.set(item.sku, (entry.skus.get(item.sku) ?? 0) + item.quantity);
}
}
const result: Summary[] = [];
for (const [region, entry] of byRegion) {
let topSku = "";
let topCount = -1;
for (const [sku, count] of entry.skus) {
if (count > topCount || (count === topCount && sku < topSku)) {
topSku = sku;
topCount = count;
}
}
result.push({ region, orders: entry.orders, revenue: Math.round(entry.revenue * 100) / 100, topSku });
}
result.sort((a, b) => (a.region < b.region ? -1 : a.region > b.region ? 1 : 0));
return result;
}
const scale = Number(process.argv[2] ?? "1");
const orders = makeOrders(Math.floor(4000 * scale));
let checksum = 0;
let last = "";
for (let round = 0; round < 6; round++) {
const text = JSON.stringify(orders);
checksum += text.length;
const parsed = JSON.parse(text) as Order[];
const summary = summarize(parsed);
last = JSON.stringify(summary);
checksum += last.length;
}
console.log(last);
console.log("checksum", checksum);
@@ -0,0 +1,74 @@
// Integer- and float-heavy kernels: sieve, hashing, matrix multiply on
// number[][], histogram on plain arrays, and a Float64Array smoothing pass.
function sieve(limit: number): number {
const composite: boolean[] = new Array<boolean>(limit + 1).fill(false);
let count = 0;
for (let i = 2; i <= limit; i++) {
if (composite[i]) continue;
count++;
for (let j = i * i; j <= limit; j += i) composite[j] = true;
}
return count;
}
function fnv(values: number[]): number {
let h = 2166136261;
for (let i = 0; i < values.length; i++) {
h ^= values[i]! & 0xff;
h = Math.imul(h, 16777619) >>> 0;
}
return h;
}
function matmul(a: number[][], b: number[][], n: number): number[][] {
const c: number[][] = [];
for (let i = 0; i < n; i++) {
const row: number[] = new Array<number>(n).fill(0);
const ai = a[i]!;
for (let k = 0; k < n; k++) {
const aik = ai[k]!;
const bk = b[k]!;
for (let j = 0; j < n; j++) row[j] = row[j]! + aik * bk[j]!;
}
c.push(row);
}
return c;
}
const scale = Number(process.argv[2] ?? "1");
const primes = sieve(Math.floor(2000000 * scale));
const data: number[] = [];
let x = 1;
for (let i = 0; i < Math.floor(1000000 * scale); i++) {
x = (Math.imul(x, 48271) + 11) & 0x7fffffff;
data.push(x % 1000);
}
let hash = 0;
for (let r = 0; r < 4; r++) hash = (hash + fnv(data)) >>> 0;
const histogram: number[] = new Array<number>(100).fill(0);
for (let i = 0; i < data.length; i++) {
const bucket = data[i]! % 100;
histogram[bucket] = histogram[bucket]! + 1;
}
const n = Math.floor(120 * Math.sqrt(scale));
const a: number[][] = [];
const b: number[][] = [];
for (let i = 0; i < n; i++) {
const ra: number[] = [];
const rb: number[] = [];
for (let j = 0; j < n; j++) {
ra.push(((i * j) % 7) - 3);
rb.push(((i + j) % 5) - 2);
}
a.push(ra);
b.push(rb);
}
const c = matmul(a, b, n);
let trace = 0;
for (let i = 0; i < n; i++) trace += c[i]![i]!;
const signal = new Float64Array(data.length);
for (let i = 0; i < data.length; i++) signal[i] = data[i]! / 7;
let smooth = 0;
for (let pass = 0; pass < 3; pass++) {
for (let i = 1; i < signal.length - 1; i++) signal[i] = (signal[i - 1]! + signal[i]! * 2 + signal[i + 1]!) / 4;
}
for (let i = 0; i < signal.length; i += 1000) smooth += signal[i]!;
console.log("primes", primes, "hash", hash, "hist", histogram[0], histogram[99], "trace", trace, "smooth", smooth.toFixed(6));
@@ -0,0 +1,80 @@
// Business-record processing: multi-key sorts with comparator closures,
// group-by into Maps, and reductions over the groups.
interface Employee {
id: number;
name: string;
department: string;
level: number;
salary: number;
startYear: number;
}
let seed = 2024;
function random(): number {
seed = (Math.imul(seed, 1103515245) + 12345) >>> 0;
return seed / 4294967296;
}
const departments = ["eng", "sales", "support", "finance", "legal", "design", "ops"];
const first = ["ana", "bo", "cy", "dee", "eli", "fay", "gus", "hal", "ivy", "jon"];
const last = ["smith", "jones", "lee", "khan", "garcia", "chen", "novak", "silva"];
function makeEmployees(count: number): Employee[] {
const result: Employee[] = [];
for (let i = 0; i < count; i++) {
result.push({
id: i,
name: first[Math.floor(random() * first.length)]! + " " + last[Math.floor(random() * last.length)]!,
department: departments[Math.floor(random() * departments.length)]!,
level: 1 + Math.floor(random() * 7),
salary: 40000 + Math.floor(random() * 160000),
startYear: 1995 + Math.floor(random() * 30),
});
}
return result;
}
function compareBy(keys: string[]): (a: Employee, b: Employee) => number {
return (a, b) => {
for (const key of keys) {
let d = 0;
if (key === "department") d = a.department < b.department ? -1 : a.department > b.department ? 1 : 0;
else if (key === "level") d = b.level - a.level;
else if (key === "salary") d = b.salary - a.salary;
else if (key === "name") d = a.name < b.name ? -1 : a.name > b.name ? 1 : 0;
else d = a.id - b.id;
if (d !== 0) return d;
}
return 0;
};
}
const scale = Number(process.argv[2] ?? "1");
const employees = makeEmployees(Math.floor(60000 * scale));
let checksum = 0;
const orders = [
["department", "level", "salary", "id"],
["name", "id"],
["salary", "id"],
["level", "name", "id"],
];
for (const order of orders) {
const sorted = employees.slice().sort(compareBy(order));
for (let i = 0; i < sorted.length; i += 997) checksum = (checksum * 31 + sorted[i]!.id) % 1000000007;
}
const groups = new Map<string, Employee[]>();
for (const e of employees) {
const key = e.department + "/" + e.level;
const group = groups.get(key);
if (group === undefined) groups.set(key, [e]);
else group.push(e);
}
const lines: string[] = [];
for (const [key, group] of groups) {
const total = group.reduce((sum, e) => sum + e.salary, 0);
const veterans = group.filter((e) => e.startYear < 2010).length;
const top = group.reduce((best, e) => (e.salary > best.salary ? e : best), group[0]!);
lines.push(`${key} n=${group.length} avg=${Math.round(total / group.length)} vets=${veterans} top=${top.id}`);
}
lines.sort();
for (const line of lines.slice(0, 12)) console.log(line);
console.log("groups", lines.length, "checksum", checksum);
@@ -0,0 +1,49 @@
// Regex-driven log processing: match with capture groups, test, and replace.
let seed = 99;
function random(): number {
seed = (Math.imul(seed, 1103515245) + 12345) >>> 0;
return seed / 4294967296;
}
const methods = ["GET", "POST", "PUT", "DELETE"];
const paths = ["/api/users", "/api/orders", "/static/app.js", "/health", "/api/items/search"];
function makeLines(count: number): string[] {
const lines: string[] = [];
for (let i = 0; i < count; i++) {
const ip = `10.${Math.floor(random() * 255)}.${Math.floor(random() * 255)}.${Math.floor(random() * 255)}`;
const method = methods[Math.floor(random() * methods.length)]!;
const path = paths[Math.floor(random() * paths.length)]! + (random() < 0.3 ? "?id=" + Math.floor(random() * 1000) : "");
const status = random() < 0.9 ? 200 : random() < 0.5 ? 404 : 500;
const ms = Math.floor(random() * 900);
const agent = random() < 0.5 ? "Mozilla/5.0 (Macintosh)" : "curl/8.4.0";
lines.push(`${ip} - - [07/Oct/2026:12:${String(i % 60).padStart(2, "0")}:00 +0000] "${method} ${path} HTTP/1.1" ${status} ${ms} "${agent}"`);
}
return lines;
}
const linePattern = /^(\d+\.\d+\.\d+\.\d+) - - \[([^\]]+)\] "(\w+) ([^ ?"]+)(?:\?([^ "]*))? HTTP\/1\.1" (\d{3}) (\d+) "([^"]*)"$/;
const botPattern = /curl|bot|spider/i;
const digits = /\d+/g;
const scale = Number(process.argv[2] ?? "1");
const lines = makeLines(Math.floor(30000 * scale));
const counts = new Map<string, number>();
let errors = 0;
let bots = 0;
let totalMs = 0;
let redactedLength = 0;
for (let round = 0; round < 3; round++) {
for (const line of lines) {
const m = linePattern.exec(line);
if (m === null) continue;
const key = m[3]! + " " + m[4]!;
counts.set(key, (counts.get(key) ?? 0) + 1);
if (m[6] !== "200") errors++;
if (botPattern.test(m[8]!)) bots++;
totalMs += Number(m[7]);
redactedLength += line.replace(digits, "#").length;
}
}
const keys = [...counts.keys()].sort();
for (const key of keys) console.log(key, counts.get(key));
console.log("errors", errors, "bots", bots, "ms", totalMs, "redacted", redactedLength);
@@ -0,0 +1,73 @@
// Server-side HTML rendering: template literals, escaping, joins, and
// conditional fragments over a list of typed view models.
interface Product {
id: number;
title: string;
description: string;
price: number;
tags: string[];
inStock: boolean;
rating: number;
}
let seed = 31337;
function random(): number {
seed = (Math.imul(seed, 1103515245) + 12345) >>> 0;
return seed / 4294967296;
}
const words = ["fast", "durable", "<b>bold</b>", "eco", '"quoted"', "light", "smart", "R&D", "classic", "modern"];
function sentence(n: number): string {
const parts: string[] = [];
for (let i = 0; i < n; i++) parts.push(words[Math.floor(random() * words.length)]!);
return parts.join(" ");
}
function makeProducts(count: number): Product[] {
const products: Product[] = [];
for (let i = 0; i < count; i++) {
products.push({
id: i,
title: sentence(3),
description: sentence(20),
price: Math.round(random() * 100000) / 100,
tags: [words[i % words.length]!, words[(i * 7) % words.length]!],
inStock: random() < 0.8,
rating: Math.round(random() * 50) / 10,
});
}
return products;
}
function escapeHtml(text: string): string {
return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
}
function stars(rating: number): string {
let out = "";
for (let i = 1; i <= 5; i++) out += i <= Math.round(rating) ? "*" : "-";
return out;
}
function renderProduct(p: Product): string {
const tags = p.tags.map((t) => `<li class="tag">${escapeHtml(t)}</li>`).join("");
return `<article id="p-${p.id}" class="${p.inStock ? "in" : "out"}">
<h2>${escapeHtml(p.title)}</h2>
<p>${escapeHtml(p.description)}</p>
<span class="price">$${p.price.toFixed(2)}</span>
<span class="rating" title="${p.rating}">${stars(p.rating)}</span>
<ul>${tags}</ul>
${p.inStock ? "<button>Add to cart</button>" : "<em>Sold out</em>"}
</article>`;
}
function renderPage(products: Product[], page: number): string {
const body = products.map(renderProduct).join("\n");
return `<!doctype html><html><head><title>Page ${page}</title></head><body>${body}</body></html>`;
}
const scale = Number(process.argv[2] ?? "1");
const products = makeProducts(Math.floor(2000 * scale));
let bytes = 0;
let hash = 0;
for (let page = 0; page < 10; page++) {
const html = renderPage(products, page);
bytes += html.length;
for (let i = 0; i < html.length; i += 101) hash = (hash * 33 + html.charCodeAt(i)) % 2147483647;
}
console.log("bytes", bytes, "hash", hash);
@@ -0,0 +1,76 @@
// Validation layers that throw and catch errors for a fraction of inputs,
// with deep call chains of small functions on the success path.
class ValidationError extends Error {
readonly field: string;
constructor(field: string, message: string) {
super(message);
this.field = field;
}
}
interface Signup {
email: string;
age: number;
name: string;
password: string;
}
let seed = 8080;
function random(): number {
seed = (Math.imul(seed, 1103515245) + 12345) >>> 0;
return seed / 4294967296;
}
function makeSignup(i: number): Signup {
const bad = random();
return {
email: bad < 0.05 ? "broken" : `user${i}@example.com`,
age: bad > 0.97 ? -1 : 18 + Math.floor(random() * 60),
name: bad > 0.95 && bad <= 0.97 ? " " : "Name " + i,
password: random() < 0.03 ? "short" : "correct horse battery " + i,
};
}
function requireText(field: string, value: string, min: number): string {
if (value.length < min) throw new ValidationError(field, `${field} must have at least ${min} characters`);
return value;
}
function requireEmail(value: string): string {
const at = value.indexOf("@");
if (at <= 0 || value.indexOf(".", at) < 0) throw new ValidationError("email", "invalid email");
return value.toLowerCase();
}
function requireRange(field: string, value: number, min: number, max: number): number {
if (value < min || value > max) throw new ValidationError(field, `${field} out of range`);
return value;
}
function normalize(input: Signup): Signup {
return {
email: requireEmail(input.email),
age: requireRange("age", input.age, 13, 120),
name: requireText("name", input.name.trim(), 1),
password: requireText("password", input.password, 8),
};
}
function score(s: Signup): number {
return s.email.length + s.age + s.name.length + s.password.length;
}
const scale = Number(process.argv[2] ?? "1");
const inputs: Signup[] = [];
for (let i = 0; i < Math.floor(200000 * scale); i++) inputs.push(makeSignup(i));
const failures = new Map<string, number>();
let accepted = 0;
let total = 0;
for (let round = 0; round < 3; round++) {
for (const input of inputs) {
try {
const s = normalize(input);
accepted++;
total += score(s);
} catch (error) {
if (error instanceof ValidationError) failures.set(error.field, (failures.get(error.field) ?? 0) + 1);
else throw error;
}
}
}
console.log("accepted", accepted, "total", total);
for (const key of [...failures.keys()].sort()) console.log(key, failures.get(key));
@@ -0,0 +1,61 @@
// Map/Set-heavy text and graph processing: word frequencies, bigrams,
// and breadth-first search over a string-keyed adjacency map.
let seed = 555;
function random(): number {
seed = (Math.imul(seed, 1103515245) + 12345) >>> 0;
return seed / 4294967296;
}
const syllables = ["ka", "lo", "mi", "ne", "ru", "sa", "to", "vi", "ze", "po", "qu", "ex"];
function word(): string {
const n = 1 + Math.floor(random() * 3);
let w = "";
for (let i = 0; i < n; i++) w += syllables[Math.floor(random() * syllables.length)]!;
return w;
}
const scale = Number(process.argv[2] ?? "1");
const text: string[] = [];
for (let i = 0; i < Math.floor(300000 * scale); i++) text.push(word());
const freq = new Map<string, number>();
for (const w of text) freq.set(w, (freq.get(w) ?? 0) + 1);
const bigrams = new Map<string, number>();
for (let i = 1; i < text.length; i++) {
const key = text[i - 1]! + " " + text[i]!;
bigrams.set(key, (bigrams.get(key) ?? 0) + 1);
}
const top = [...freq.entries()].sort((a, b) => b[1] - a[1] || (a[0] < b[0] ? -1 : 1)).slice(0, 5);
console.log("words", freq.size, "bigrams", bigrams.size, "top", top.map((e) => e[0] + ":" + e[1]).join(","));
const graph = new Map<string, string[]>();
for (let i = 1; i < text.length; i += 3) {
const from = text[i - 1]!;
const to = text[i]!;
const edges = graph.get(from);
if (edges === undefined) graph.set(from, [to]);
else if (edges.length < 8) edges.push(to);
}
let reachable = 0;
let depthSum = 0;
const starts = [...graph.keys()].sort().slice(0, 60);
for (const start of starts) {
const seen = new Set<string>([start]);
let frontier = [start];
let depth = 0;
while (frontier.length > 0) {
const next: string[] = [];
for (const node of frontier) {
for (const neighbor of graph.get(node) ?? []) {
if (!seen.has(neighbor)) {
seen.add(neighbor);
next.push(neighbor);
}
}
}
frontier = next;
depth++;
}
reachable += seen.size;
depthSum += depth;
}
console.log("reachable", reachable, "depth", depthSum);
+1
View File
@@ -9,6 +9,7 @@
"build:fresh": "rm -rf packages/compiler/dist packages/cli/dist node_modules/.cache/scriptc-tsc && pnpm build",
"build:native-compiler": "node --max-old-space-size=8192 --import tsx scripts/build-native-cli.mts",
"bench:builds": "node scripts/bench-builds.mjs",
"bench:runtime": "node scripts/bench-runtime.mjs",
"bench:ast-access": "node scripts/bench-ast-access.mjs",
"bench:backend-analysis": "node scripts/bench-backend-analysis.mjs",
"test": "vitest run",
+424
View File
@@ -0,0 +1,424 @@
/* Runtime performance A/B harness for compiled programs.
*
* Builds every workload in benchmarks/runtime/workloads.json with one or two
* scriptc checkouts, verifies each executable against Node byte-for-byte
* (stdout, stderr, exit status), then times interleaved launches and reports
* per-workload medians with bootstrap confidence intervals for the
* candidate/baseline ratio. Executables that diverge from Node are never
* timed. A per-host advisory lock serializes measurements so concurrent
* agents on one machine do not disturb each other's numbers. */
import assert from "node:assert/strict";
import { spawnSync } from "node:child_process";
import {
existsSync,
mkdirSync,
mkdtempSync,
readFileSync,
rmSync,
statSync,
writeFileSync,
} from "node:fs";
import { tmpdir } from "node:os";
import { dirname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { parseArgs } from "node:util";
const repoRoot = fileURLToPath(new URL("..", import.meta.url));
const suiteRoot = join(repoRoot, "benchmarks/runtime");
const { values } = parseArgs({
allowNegative: true,
options: {
candidate: { type: "string", default: repoRoot },
baseline: { type: "string" },
workloads: { type: "string" },
runs: { type: "string", default: "15" },
warmup: { type: "string", default: "2" },
timeout: { type: "string", default: "120" },
json: { type: "string" },
"no-lock": { type: "boolean", default: false },
node: { type: "boolean", default: true },
"keep": { type: "boolean", default: false },
help: { type: "boolean", default: false },
},
});
if (values.help) {
console.log(`Usage: node scripts/bench-runtime.mjs [--candidate=<checkout>] [--baseline=<checkout>]
[--workloads=a,b] [--runs=15] [--warmup=2] [--timeout=120] [--json=<file>] [--no-lock] [--no-node] [--keep]
Each checkout must have a built CLI (packages/cli/dist/bootstrap.js) and native
artifacts for this host. Without --baseline, reports absolute times only. Node is
timed in the same interleaved loop as the reference to beat (--no-node skips it).`);
process.exit(0);
}
const runs = Number(values.runs);
const warmup = Number(values.warmup);
const timeoutMs = Number(values.timeout) * 1000;
assert.ok(Number.isInteger(runs) && runs >= 3 && runs <= 200, "--runs must be 3..200");
assert.ok(Number.isInteger(warmup) && warmup >= 0 && warmup <= 20, "--warmup must be 0..20");
const manifest = JSON.parse(readFileSync(join(suiteRoot, "workloads.json"), "utf8"));
const selected = values.workloads ? new Set(values.workloads.split(",")) : null;
const workloads = manifest.workloads.filter((w) => selected === null || selected.has(w.name));
if (selected) {
for (const name of selected)
assert.ok(
manifest.workloads.some((w) => w.name === name),
`unknown workload ${name}`,
);
}
const work = mkdtempSync(
join(process.platform === "win32" ? tmpdir() : "/tmp", "scriptc-bench-runtime-"),
);
/* ── inputs for file-processing applications ──────────────────────────── */
function generateInput(kind) {
const path = join(work, `${kind}.input`);
if (kind === "log-summary") {
writeFileSync(
path,
Array.from(
{ length: 300_000 },
(_, i) =>
`/route/${i % 37}?request=${i} ${i % 13 === 0 ? 500 : 200} ${i % 250} ${200 + (i % 4000)}`,
).join("\n") + "\n",
);
} else if (kind === "inventory-report") {
writeFileSync(
path,
Array.from({ length: 300_000 }, (_, i) =>
["sku-" + i, "category-" + (i % 17), "product " + i, i % 40, 199 + (i % 5000)].join("\t"),
).join("\n") + "\n",
);
} else throw new Error(`unknown input kind ${kind}`);
return path;
}
/* ── advisory lock ─────────────────────────────────────────────────────── */
const lockDir = join(
process.platform === "win32" ? tmpdir() : "/tmp",
"scriptc-bench-runtime.lock",
);
function acquireLock() {
const started = Date.now();
let announced = false;
for (;;) {
try {
mkdirSync(lockDir);
writeFileSync(join(lockDir, "pid"), String(process.pid));
return;
} catch (error) {
if (error.code !== "EEXIST") throw error;
let owner = NaN;
try {
owner = Number(readFileSync(join(lockDir, "pid"), "utf8"));
} catch {}
let alive = false;
if (Number.isInteger(owner)) {
try {
process.kill(owner, 0);
alive = true;
} catch {}
}
if (!alive && Date.now() - started > 2000) {
rmSync(lockDir, { recursive: true, force: true });
continue;
}
if (!announced) {
process.stderr.write(`waiting for benchmark lock held by pid ${owner}\n`);
announced = true;
}
spawnSync("sleep", ["2"]);
}
}
}
function releaseLock() {
rmSync(lockDir, { recursive: true, force: true });
}
/* ── process helpers ───────────────────────────────────────────────────── */
function run(command, args, options = {}) {
const start = process.hrtime.bigint();
const result = spawnSync(command, args, {
encoding: "utf8",
maxBuffer: 64 * 1024 * 1024,
timeout: options.timeout ?? timeoutMs,
env: options.env ?? process.env,
cwd: options.cwd,
});
const ms = Number(process.hrtime.bigint() - start) / 1e6;
if (result.error && result.error.code !== "ETIMEDOUT") throw result.error;
return {
stdout: result.stdout ?? "",
stderr: result.stderr ?? "",
status: result.status,
signal: result.signal,
timedOut: result.error?.code === "ETIMEDOUT" || result.signal === "SIGTERM",
ms,
};
}
function peakRss(binary, args) {
if (process.platform !== "darwin" && process.platform !== "linux") return null;
if (!existsSync("/usr/bin/time")) return null;
const timeArgs =
process.platform === "darwin" ? ["-l", binary, ...args] : ["-f", "rss_kib=%M", binary, ...args];
const result = run("/usr/bin/time", timeArgs);
const match =
process.platform === "darwin"
? /(\d+)\s+maximum resident set size/.exec(result.stderr)
: /rss_kib=(\d+)/.exec(result.stderr);
if (!match) return null;
return Number(match[1]) * (process.platform === "darwin" ? 1 : 1024);
}
/* ── statistics ────────────────────────────────────────────────────────── */
function median(xs) {
const s = [...xs].sort((a, b) => a - b);
const m = s.length >> 1;
return s.length % 2 ? s[m] : (s[m - 1] + s[m]) / 2;
}
function makeRandom(seed) {
let state = seed >>> 0;
return () => {
state = (Math.imul(state, 1664525) + 1013904223) >>> 0;
return state / 4294967296;
};
}
function bootstrapRatio(candidate, baseline, resamples = 2000) {
const random = makeRandom(0x5eed);
const ratios = [];
const pick = (xs) =>
Array.from({ length: xs.length }, () => xs[Math.floor(random() * xs.length)]);
for (let i = 0; i < resamples; i++) ratios.push(median(pick(candidate)) / median(pick(baseline)));
ratios.sort((a, b) => a - b);
return {
low: ratios[Math.floor(resamples * 0.025)],
high: ratios[Math.floor(resamples * 0.975)],
};
}
/* ── build ─────────────────────────────────────────────────────────────── */
function cliFor(root) {
const cli = join(resolve(root), "packages/cli/dist/bootstrap.js");
statSync(cli);
return cli;
}
function revision(root) {
const r = run("git", ["-C", root, "rev-parse", "--short", "HEAD"], { timeout: 10_000 });
const dirty = run("git", ["-C", root, "status", "--porcelain", "--untracked-files=no"], {
timeout: 10_000,
});
return r.stdout.trim() + (dirty.stdout.trim() ? "+dirty" : "");
}
function build(label, root, workload) {
const cli = cliFor(root);
const out = join(work, label, workload.name);
mkdirSync(dirname(out), { recursive: true });
const cache = join(work, label, ".cache");
mkdirSync(cache, { recursive: true });
const env = { ...process.env, SCRIPTC_CACHE_DIR: cache };
delete env.SCRIPTC_NO_CACHE;
delete env.SCRIPTC_TIMING;
const entry = join(suiteRoot, workload.entry);
const result = run(process.execPath, [cli, "build", entry, "--optimization=release", "-o", out], {
env,
timeout: 900_000,
});
if (result.status !== 0)
return {
ok: false,
error: (result.stderr || result.stdout).slice(0, 4000),
buildMs: result.ms,
};
return { ok: true, binary: out, bytes: statSync(out).size, buildMs: result.ms };
}
/* ── main ──────────────────────────────────────────────────────────────── */
const contenders = [
...(values.baseline ? [{ label: "baseline", root: resolve(values.baseline) }] : []),
{ label: "candidate", root: resolve(values.candidate) },
];
const report = {
schema: 1,
date: new Date().toISOString(),
host: { platform: process.platform, arch: process.arch, node: process.version },
runs,
contenders: contenders.map((c) => ({ ...c, revision: revision(c.root) })),
workloads: [],
};
try {
const inputs = new Map();
for (const w of workloads)
if (w.input && !inputs.has(w.input)) inputs.set(w.input, generateInput(w.input));
// Builds and the Node oracle run outside the lock: they do not need a
// quiet machine, and holding the lock while compiling would serialize
// every agent's slowest phase.
const prepared = [];
for (const w of workloads) {
const args = w.args.map((a) => (a === "{input}" ? inputs.get(w.input) : a));
// The application workloads are ESM without a package.json "type"; Node's
// reparse warning is an oracle artifact, not program output.
const oracle = run(process.execPath, [
"--disable-warning=MODULE_TYPELESS_PACKAGE_JSON",
join(suiteRoot, w.entry),
...args,
]);
const entry = { name: w.name, args, results: {} };
if (oracle.status !== 0 || oracle.timedOut) {
entry.error = `node oracle failed (status ${oracle.status}): ${oracle.stderr.slice(0, 2000)}`;
prepared.push(entry);
continue;
}
for (const c of contenders) {
const built = build(c.label, c.root, w);
const result = { build_ms: Math.round(built.buildMs) };
entry.results[c.label] = result;
if (!built.ok) {
result.status = "build-failed";
result.error = built.error;
continue;
}
result.bytes = built.bytes;
result.binary = built.binary;
const check = run(built.binary, args);
if (check.timedOut) {
result.status = "timeout";
continue;
}
if (
check.stdout !== oracle.stdout ||
check.stderr !== oracle.stderr ||
check.status !== oracle.status
) {
result.status = "mismatch";
result.error = `expected status ${oracle.status}, got ${check.status}; stdout ${check.stdout === oracle.stdout ? "matches" : "differs"}; stderr ${check.stderr === oracle.stderr ? "matches" : "differs"}`;
continue;
}
result.status = "ok";
result.samples = [];
}
entry.node_ms = Math.round(oracle.ms);
entry.nodeCommand = [
"--disable-warning=MODULE_TYPELESS_PACKAGE_JSON",
join(suiteRoot, w.entry),
...args,
];
prepared.push(entry);
process.stderr.write(`prepared ${w.name}\n`);
}
if (!values["no-lock"]) acquireLock();
try {
for (const entry of prepared) {
const live = contenders.filter((c) => entry.results[c.label]?.status === "ok");
if (live.length === 0) continue;
for (const c of live)
for (let i = 0; i < warmup; i++) run(entry.results[c.label].binary, entry.args);
// Node is the reference the project aims to beat, so it is timed in the
// same interleaved loop (whole-process wall time, startup included).
const nodeSamples = [];
if (values.node) run(process.execPath, entry.nodeCommand);
const random = makeRandom(entry.name.length * 7919);
for (let r = 0; r < runs; r++) {
const order = [...live];
if (order.length === 2 && random() < 0.5) order.reverse();
for (const c of order) {
const result = entry.results[c.label];
const sample = run(result.binary, entry.args);
if (sample.timedOut || sample.status !== 0) {
result.status = sample.timedOut ? "timeout" : "unstable-exit";
break;
}
result.samples.push(Math.round(sample.ms * 100) / 100);
}
if (values.node) nodeSamples.push(run(process.execPath, entry.nodeCommand).ms);
}
if (nodeSamples.length > 0) entry.node_ms = Math.round(median(nodeSamples) * 100) / 100;
const reference = entry.results.candidate;
if (reference?.status === "ok" && reference.samples.length > 0 && nodeSamples.length > 0)
entry.vs_node = Math.round((median(reference.samples) / entry.node_ms) * 1000) / 1000;
for (const c of live) {
const result = entry.results[c.label];
if (result.status !== "ok") continue;
result.median_ms = Math.round(median(result.samples) * 100) / 100;
result.peak_rss_bytes = peakRss(result.binary, entry.args);
}
const base = entry.results.baseline;
const cand = entry.results.candidate;
if (base?.status === "ok" && cand?.status === "ok") {
const ci = bootstrapRatio(cand.samples, base.samples);
entry.ratio = Math.round((cand.median_ms / base.median_ms) * 10000) / 10000;
entry.ci95 = [Math.round(ci.low * 10000) / 10000, Math.round(ci.high * 10000) / 10000];
entry.verdict = ci.high < 1 ? "faster" : ci.low > 1 ? "slower" : "neutral";
entry.size_ratio = Math.round((cand.bytes / base.bytes) * 10000) / 10000;
}
process.stderr.write(`measured ${entry.name}\n`);
}
} finally {
if (!values["no-lock"]) releaseLock();
}
for (const entry of prepared) {
for (const result of Object.values(entry.results)) delete result.binary;
delete entry.nodeCommand;
report.workloads.push(entry);
}
const vsNode = report.workloads.filter((w) => w.vs_node !== undefined).map((w) => w.vs_node);
if (vsNode.length > 0)
report.geomean_vs_node =
Math.round(Math.exp(vsNode.reduce((s, r) => s + Math.log(r), 0) / vsNode.length) * 1000) /
1000;
const ratios = report.workloads.filter((w) => w.ratio !== undefined).map((w) => w.ratio);
if (ratios.length > 0)
report.geomean_ratio =
Math.round(Math.exp(ratios.reduce((s, r) => s + Math.log(r), 0) / ratios.length) * 10000) /
10000;
} finally {
if (!values.keep) rmSync(work, { recursive: true, force: true });
}
if (values.json) writeFileSync(values.json, JSON.stringify(report, null, 2) + "\n");
/* ── summary table ─────────────────────────────────────────────────────── */
const pct = (r) => `${r < 1 ? "" : "+"}${((r - 1) * 100).toFixed(1)}%`;
const lines = [];
const xNode = (w) => (w.vs_node === undefined ? "-" : `${w.vs_node.toFixed(2)}x`);
if (values.baseline) {
lines.push(
"| workload | baseline ms | candidate ms | change | 95% CI | verdict | size | node ms | candidate/node |",
);
lines.push("| --- | ---: | ---: | ---: | --- | --- | ---: | ---: | ---: |");
} else {
lines.push("| workload | median ms | status | bytes | peak RSS MiB | node ms | scriptc/node |");
lines.push("| --- | ---: | --- | ---: | ---: | ---: | ---: |");
}
for (const w of report.workloads) {
const b = w.results.baseline;
const c = w.results.candidate;
if (w.error) {
lines.push(`| ${w.name} | ${w.error.split("\n")[0]} |`);
} else if (values.baseline) {
const show = (r) => (r?.status === "ok" ? r.median_ms.toFixed(1) : (r?.status ?? "-"));
lines.push(
`| ${w.name} | ${show(b)} | ${show(c)} | ${w.ratio ? pct(w.ratio) : "-"} | ${w.ci95 ? `${pct(w.ci95[0])} .. ${pct(w.ci95[1])}` : "-"} | ${w.verdict ?? "-"} | ${w.size_ratio ? pct(w.size_ratio) : "-"} | ${w.node_ms} | ${xNode(w)} |`,
);
} else {
lines.push(
`| ${w.name} | ${c?.median_ms?.toFixed(1) ?? "-"} | ${c?.status ?? "-"} | ${c?.bytes ?? "-"} | ${c?.peak_rss_bytes ? (c.peak_rss_bytes / 1048576).toFixed(1) : "-"} | ${w.node_ms} | ${xNode(w)} |`,
);
}
}
if (report.geomean_ratio !== undefined)
lines.push("", `geomean change: ${pct(report.geomean_ratio)}`);
if (report.geomean_vs_node !== undefined)
lines.push(
`geomean candidate/node: ${report.geomean_vs_node.toFixed(3)}x (lower is better; goal: well below 1)`,
);
console.log(lines.join("\n"));
const broken = report.workloads.some(
(w) => w.error || Object.values(w.results).some((r) => r.status !== "ok"),
);
process.exitCode = broken ? 1 : 0;