Skip to content

Commit 3a2c32b

Browse files
committed
Improvements
1 parent b8b3beb commit 3a2c32b

File tree

13 files changed

+361
-163
lines changed

13 files changed

+361
-163
lines changed

esbuild.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ const buildOptionsBase: esbuild.BuildOptions = {
2323
entryPoints: [
2424
'./src/index.ts',
2525
'./src/stats.ts',
26+
'./src/types.ts',
2627
'./src/reporters/advanced.ts',
2728
'./src/reporters/simple.ts',
2829
'./src/reporters/xunit.ts',

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@apeleghq/benchmark",
3-
"version": "1.0.2",
3+
"version": "1.0.3",
44
"description": "A statistically rigorous benchmarking library with paired t-tests, baseline correction, and confidence intervals",
55
"type": "module",
66
"main": "./dist/index.cjs",
@@ -27,6 +27,16 @@
2727
"default": "./dist/stats.cjs"
2828
}
2929
},
30+
"./types": {
31+
"import": {
32+
"types": "./dist/types.d.ts",
33+
"default": "./dist/types.mjs"
34+
},
35+
"require": {
36+
"types": "./dist/types.d.cts",
37+
"default": "./dist/types.cjs"
38+
}
39+
},
3040
"./reporters/advanced": {
3141
"import": {
3242
"types": "./dist/reporters/advanced.d.ts",

src/errors.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/* Copyright © 2026 Apeleg Limited. All rights reserved.
2+
*
3+
* Licensed under the Apache License, Version 2.0 (the "License") with LLVM
4+
* exceptions; you may not use this file except in compliance with the
5+
* License. You may obtain a copy of the License at
6+
*
7+
* http://llvm.org/foundation/relicensing/LICENSE.txt
8+
*
9+
* Unless required by applicable law or agreed to in writing, software
10+
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11+
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
* See the License for the specific language governing permissions and
13+
* limitations under the License.
14+
*/
15+
16+
export class BenchmarkError extends Error {}
17+
18+
export class BenchmarkAbortedError extends BenchmarkError {}
19+
20+
export class BenchmarkDuplicateNameError extends BenchmarkError {}
21+
22+
export class BenchmarkEmptyError extends BenchmarkError {}
23+
24+
export class BenchmarkRunnerError extends BenchmarkError {
25+
benchName: string;
26+
trial: number;
27+
28+
constructor(
29+
message: string,
30+
cause: unknown,
31+
benchName: string,
32+
trial: number,
33+
) {
34+
super(message, { cause: cause });
35+
36+
this.benchName = benchName;
37+
this.trial = trial;
38+
}
39+
}

src/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,17 @@ export { generateReport } from './report.js';
2222
// Stats utilities (for custom consumer-side analysis)
2323
export * as stats from './stats.js';
2424

25+
// Error types
26+
export * from './errors.js';
27+
2528
// Types
2629
export { NULL_FUNCTION_NAME } from './types.js';
2730
export type {
2831
ContextFn,
2932
IBenchmarkFn,
3033
IFunctionStatistics,
3134
IPairedComparison,
35+
IRunProgress,
3236
ISuiteConfig,
3337
ISuiteReport,
3438
ITrialMeasurement,

src/report.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ function computeFunctionStats(
4848
const n = samples.length;
4949
const sd = stats.stdDev(samples);
5050
const se = stats.sem(samples);
51-
const tCrit = stats.tCritical005TwoTailed(Math.max(n - 1, 1));
51+
const tCrit = stats.tQuantile975(Math.max(n - 1, 1));
5252

5353
return {
5454
name,
@@ -97,7 +97,7 @@ function computePairedComparison(
9797

9898
const tStat = seD > 0 ? meanD / seD : 0;
9999
const pValue = seD > 0 ? stats.tDistPValue(tStat, df) : 1;
100-
const tCrit = stats.tCritical005TwoTailed(df);
100+
const tCrit = stats.tQuantile975(df);
101101

102102
const meanBadj = stats.mean(adjB);
103103

src/runner.ts

Lines changed: 90 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,12 @@
1313
* limitations under the License.
1414
*/
1515

16+
import {
17+
BenchmarkAbortedError,
18+
BenchmarkDuplicateNameError,
19+
BenchmarkEmptyError,
20+
BenchmarkRunnerError,
21+
} from './errors.js';
1622
import { generateReport } from './report.js';
1723
import type {
1824
IBenchmarkFn,
@@ -21,20 +27,11 @@ import type {
2127
ITrialMeasurement,
2228
ITrialResult,
2329
} from './types.js';
24-
import { NULL_FUNCTION_NAME } from './types.js';
30+
import { IRunProgress, NULL_FUNCTION_NAME } from './types.js';
31+
import { shuffled } from './utils.js';
2532

2633
// ── Helpers ─────────────────────────────────────────────────────────────
2734

28-
/** Fisher-Yates shuffle (returns a new array). */
29-
function shuffled<T>(array: readonly T[]): T[] {
30-
const out = [...array];
31-
for (let i = out.length - 1; i > 0; i--) {
32-
const j = Math.floor(Math.random() * (i + 1));
33-
[out[i], out[j]] = [out[j], out[i]];
34-
}
35-
return out;
36-
}
37-
3835
/** Invoke an optional sync-or-async callback with a `this` context. */
3936
async function invoke<TC extends object, TA extends unknown[] = never[]>(
4037
fn: ((this: TC, ...args: TA) => unknown | PromiseLike<unknown>) | undefined,
@@ -86,7 +83,9 @@ async function measureTime<TC extends object>(
8683
)
8784
await r;
8885
}
89-
return performance.now() - start;
86+
const end = performance.now();
87+
88+
return end - start;
9089
}
9190

9291
// ── Suite ───────────────────────────────────────────────────────────────
@@ -142,20 +141,28 @@ export class Suite<TC extends object = Record<string, unknown>, TR = unknown> {
142141
/** Register a benchmark function. Returns `this` for chaining. */
143142
add(fn: IBenchmarkFn<TC, TR>): this {
144143
if (this._fns.some((f) => f.name === fn.name)) {
145-
throw new Error(`Duplicate benchmark name: "${fn.name}"`);
144+
throw new BenchmarkDuplicateNameError(
145+
`Duplicate benchmark name: "${fn.name}"`,
146+
);
146147
}
147148
this._fns.push(fn);
148149
return this;
149150
}
150151

151152
/** Execute all trials and return a {@link ISuiteReport}. */
152-
async run(): Promise<ISuiteReport> {
153+
async run(opts?: {
154+
eventTarget?: EventTarget;
155+
signal?: AbortSignal;
156+
}): Promise<ISuiteReport> {
153157
if (this._fns.length === 0) {
154-
throw new Error(
158+
throw new BenchmarkEmptyError(
155159
'Suite has no benchmark functions — call .add() before .run()',
156160
);
157161
}
158162

163+
const eventTarget = opts?.eventTarget;
164+
const signal = opts?.signal;
165+
159166
// Inject the null baseline — an empty function that captures the
160167
// overhead of the measurement loop (call dispatch, thenable check,
161168
// loop counter). It participates in shuffling like every other
@@ -176,44 +183,70 @@ export class Suite<TC extends object = Record<string, unknown>, TR = unknown> {
176183
const measurements: Record<string, ITrialMeasurement> = {};
177184

178185
for (const bench of order) {
179-
if (
180-
bench.name !== NULL_FUNCTION_NAME &&
181-
(this._suiteValidate || bench.setup)
182-
) {
183-
const validateCtx = {} as TC;
184-
await invoke(
185-
this._suiteValidate,
186-
validateCtx,
187-
bench.fn as IBenchmarkFn<TC, TR>['fn'],
186+
try {
187+
if (signal?.aborted) {
188+
throw new BenchmarkAbortedError('Aborted');
189+
}
190+
if (eventTarget) {
191+
eventTarget.dispatchEvent(
192+
new CustomEvent<IRunProgress>('progress', {
193+
detail: {
194+
trial: t + 1,
195+
totalTrials: this._trials,
196+
currentFunction: bench.name,
197+
},
198+
}),
199+
);
200+
// Allow event to propagate
201+
await new Promise((resolve) => setTimeout(resolve, 0));
202+
}
203+
204+
if (
205+
bench.name !== NULL_FUNCTION_NAME &&
206+
(this._suiteValidate || bench.setup)
207+
) {
208+
const validateCtx = {} as TC;
209+
await invoke(
210+
this._suiteValidate,
211+
validateCtx,
212+
bench.fn as IBenchmarkFn<TC, TR>['fn'],
213+
);
214+
await invoke(
215+
bench.validate,
216+
validateCtx,
217+
bench.fn as IBenchmarkFn<TC, TR>['fn'],
218+
);
219+
}
220+
221+
const ctx = {} as TC;
222+
await invoke(this._suiteSetup, ctx);
223+
await invoke(bench.setup, ctx);
224+
225+
const totalMs = await measureTime(
226+
bench.fn,
227+
ctx,
228+
this._warmup,
229+
this._iterations,
188230
);
189-
await invoke(
190-
bench.validate,
191-
validateCtx,
192-
bench.fn as IBenchmarkFn<TC, TR>['fn'],
231+
232+
await invoke(bench.teardown, ctx);
233+
await invoke(this._suiteTeardown, ctx);
234+
235+
executionOrder.push(bench.name);
236+
measurements[bench.name] = {
237+
name: bench.name,
238+
totalMs,
239+
iterations: this._iterations,
240+
perIterationMs: totalMs / this._iterations,
241+
};
242+
} catch (e) {
243+
throw new BenchmarkRunnerError(
244+
`Error in ${bench.name}`,
245+
e,
246+
bench.name,
247+
t,
193248
);
194249
}
195-
196-
const ctx = {} as TC;
197-
await invoke(this._suiteSetup, ctx);
198-
await invoke(bench.setup, ctx);
199-
200-
const totalMs = await measureTime(
201-
bench.fn,
202-
ctx,
203-
this._warmup,
204-
this._iterations,
205-
);
206-
207-
await invoke(bench.teardown, ctx);
208-
await invoke(this._suiteTeardown, ctx);
209-
210-
executionOrder.push(bench.name);
211-
measurements[bench.name] = {
212-
name: bench.name,
213-
totalMs,
214-
iterations: this._iterations,
215-
perIterationMs: totalMs / this._iterations,
216-
};
217250
}
218251

219252
trials.push({ trialIndex: t, executionOrder, measurements });
@@ -243,10 +276,14 @@ export async function runSuite<
243276
TC extends object = Record<string, unknown>,
244277
TR = unknown,
245278
>(
246-
config: ISuiteConfig<TC, TR> & { functions: IBenchmarkFn<TC, TR>[] },
279+
config: ISuiteConfig<TC, TR> & {
280+
functions: IBenchmarkFn<TC, TR>[];
281+
eventTarget?: EventTarget;
282+
signal?: AbortSignal;
283+
},
247284
): Promise<ISuiteReport> {
248-
const { functions, ...suiteConfig } = config;
285+
const { functions, eventTarget, signal, ...suiteConfig } = config;
249286
const suite = new Suite<TC, TR>(suiteConfig);
250287
for (const fn of functions) suite.add(fn);
251-
return suite.run();
288+
return suite.run({ eventTarget, signal });
252289
}

0 commit comments

Comments
 (0)