Files
WRNexusJS/packages/benchmark/src/index.ts
T
2026-08-01 10:04:42 +05:30

146 lines
4.2 KiB
TypeScript

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")}`,
);
}
}