release: WRNexusJS 0.7.0
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
# @wrnexus/benchmark
|
||||
|
||||
Deterministic benchmark execution, percentiles, baseline comparisons, and regression budgets for builds, SSR, hydration, stores, and application hot paths.
|
||||
|
||||
```ts
|
||||
import { runBenchmark, assertBenchmarkBudget } from "@wrnexus/benchmark";
|
||||
const result = await runBenchmark("render", render, { iterations: 100 });
|
||||
assertBenchmarkBudget(result, baseline, { p95Percent: 5 });
|
||||
```
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"name": "@wrnexus/benchmark",
|
||||
"version": "0.7.0",
|
||||
"type": "module",
|
||||
"description": "Deterministic benchmark runner and performance regression budgets for WRNexusJS.",
|
||||
"main": "src/index.ts",
|
||||
"exports": {
|
||||
".": "./src/index.ts"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
export interface BenchmarkOptions {
|
||||
iterations?: number;
|
||||
warmup?: number;
|
||||
clock?: () => number;
|
||||
setup?: () => void | Promise<void>;
|
||||
teardown?: () => void | Promise<void>;
|
||||
}
|
||||
|
||||
export interface BenchmarkResult {
|
||||
name: string;
|
||||
iterations: number;
|
||||
totalMs: number;
|
||||
meanMs: number;
|
||||
minMs: number;
|
||||
maxMs: number;
|
||||
p50Ms: number;
|
||||
p95Ms: number;
|
||||
p99Ms: number;
|
||||
operationsPerSecond: number;
|
||||
samples: number[];
|
||||
}
|
||||
|
||||
export interface RegressionBudget {
|
||||
meanPercent?: number;
|
||||
p95Percent?: number;
|
||||
maxAbsoluteMs?: number;
|
||||
minOperationsPerSecond?: number;
|
||||
}
|
||||
|
||||
export interface RegressionViolation {
|
||||
metric: "meanMs" | "p95Ms" | "maxMs" | "operationsPerSecond";
|
||||
baseline?: number;
|
||||
current: number;
|
||||
limit: number;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export function percentile(values: readonly number[], quantile: number): number {
|
||||
if (!values.length) return 0;
|
||||
const sorted = [...values].sort((a, b) => a - b);
|
||||
const index = Math.min(sorted.length - 1, Math.max(0, Math.ceil(quantile * sorted.length) - 1));
|
||||
return sorted[index]!;
|
||||
}
|
||||
|
||||
export async function runBenchmark(
|
||||
name: string,
|
||||
operation: () => void | Promise<void>,
|
||||
options: BenchmarkOptions = {},
|
||||
): Promise<BenchmarkResult> {
|
||||
const iterations = Math.max(1, Math.floor(options.iterations ?? 100));
|
||||
const warmup = Math.max(0, Math.floor(options.warmup ?? Math.min(10, iterations)));
|
||||
const clock = options.clock ?? (() => performance.now());
|
||||
await options.setup?.();
|
||||
try {
|
||||
for (let index = 0; index < warmup; index++) await operation();
|
||||
const samples: number[] = [];
|
||||
for (let index = 0; index < iterations; index++) {
|
||||
const start = clock();
|
||||
await operation();
|
||||
samples.push(Math.max(0, clock() - start));
|
||||
}
|
||||
const totalMs = samples.reduce((sum, sample) => sum + sample, 0);
|
||||
const meanMs = totalMs / samples.length;
|
||||
return {
|
||||
name,
|
||||
iterations,
|
||||
totalMs,
|
||||
meanMs,
|
||||
minMs: Math.min(...samples),
|
||||
maxMs: Math.max(...samples),
|
||||
p50Ms: percentile(samples, 0.5),
|
||||
p95Ms: percentile(samples, 0.95),
|
||||
p99Ms: percentile(samples, 0.99),
|
||||
operationsPerSecond: meanMs === 0 ? Number.POSITIVE_INFINITY : 1000 / meanMs,
|
||||
samples,
|
||||
};
|
||||
} finally {
|
||||
await options.teardown?.();
|
||||
}
|
||||
}
|
||||
|
||||
export function compareBenchmark(
|
||||
current: BenchmarkResult,
|
||||
baseline: BenchmarkResult | undefined,
|
||||
budget: RegressionBudget = {},
|
||||
): RegressionViolation[] {
|
||||
const violations: RegressionViolation[] = [];
|
||||
if (baseline && budget.meanPercent !== undefined) {
|
||||
const limit = baseline.meanMs * (1 + budget.meanPercent / 100);
|
||||
if (current.meanMs > limit) {
|
||||
violations.push({
|
||||
metric: "meanMs",
|
||||
baseline: baseline.meanMs,
|
||||
current: current.meanMs,
|
||||
limit,
|
||||
message: `Mean latency regressed by more than ${budget.meanPercent}%.`,
|
||||
});
|
||||
}
|
||||
}
|
||||
if (baseline && budget.p95Percent !== undefined) {
|
||||
const limit = baseline.p95Ms * (1 + budget.p95Percent / 100);
|
||||
if (current.p95Ms > limit) {
|
||||
violations.push({
|
||||
metric: "p95Ms",
|
||||
baseline: baseline.p95Ms,
|
||||
current: current.p95Ms,
|
||||
limit,
|
||||
message: `P95 latency regressed by more than ${budget.p95Percent}%.`,
|
||||
});
|
||||
}
|
||||
}
|
||||
if (budget.maxAbsoluteMs !== undefined && current.maxMs > budget.maxAbsoluteMs) {
|
||||
violations.push({
|
||||
metric: "maxMs",
|
||||
current: current.maxMs,
|
||||
limit: budget.maxAbsoluteMs,
|
||||
message: `Maximum latency exceeds ${budget.maxAbsoluteMs} ms.`,
|
||||
});
|
||||
}
|
||||
if (
|
||||
budget.minOperationsPerSecond !== undefined &&
|
||||
current.operationsPerSecond < budget.minOperationsPerSecond
|
||||
) {
|
||||
violations.push({
|
||||
metric: "operationsPerSecond",
|
||||
current: current.operationsPerSecond,
|
||||
limit: budget.minOperationsPerSecond,
|
||||
message: `Throughput is below ${budget.minOperationsPerSecond} operations/second.`,
|
||||
});
|
||||
}
|
||||
return violations;
|
||||
}
|
||||
|
||||
export function assertBenchmarkBudget(
|
||||
current: BenchmarkResult,
|
||||
baseline: BenchmarkResult | undefined,
|
||||
budget: RegressionBudget,
|
||||
): void {
|
||||
const violations = compareBenchmark(current, baseline, budget);
|
||||
if (violations.length) {
|
||||
throw new Error(
|
||||
`Benchmark '${current.name}' failed:\n${violations.map((item) => `- ${item.message}`).join("\n")}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { compareBenchmark, percentile, runBenchmark } from "../src/index.ts";
|
||||
|
||||
describe("@wrnexus/benchmark", () => {
|
||||
test("calculates percentiles and deterministic samples", async () => {
|
||||
let now = 0;
|
||||
const result = await runBenchmark(
|
||||
"clock",
|
||||
() => {
|
||||
now += 2;
|
||||
},
|
||||
{
|
||||
iterations: 3,
|
||||
warmup: 0,
|
||||
clock: () => now,
|
||||
},
|
||||
);
|
||||
expect(result.samples).toEqual([2, 2, 2]);
|
||||
expect(result.p95Ms).toBe(2);
|
||||
expect(percentile([1, 2, 3, 4], 0.5)).toBe(2);
|
||||
});
|
||||
|
||||
test("reports regression budget violations", () => {
|
||||
const baseline = { meanMs: 10, p95Ms: 20 } as any;
|
||||
const current = { meanMs: 12, p95Ms: 30, maxMs: 40, operationsPerSecond: 50 } as any;
|
||||
expect(compareBenchmark(current, baseline, { meanPercent: 10, p95Percent: 20 })).toHaveLength(
|
||||
2,
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user