Skip to content

Commit a066b8f

Browse files
committed
Various improvements
* Support for parameters * Better types * Re-designed validation to run once
1 parent fb48d3b commit a066b8f

File tree

13 files changed

+265
-125
lines changed

13 files changed

+265
-125
lines changed

example/params.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
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+
import { deepEqual, notEqual } from 'node:assert/strict';
17+
import { runSuite } from '../src/index.js';
18+
19+
type Ctx = {
20+
array: unknown[];
21+
};
22+
23+
const result = await runSuite<Ctx, Ctx['array'], [dep1: number, dep2: string]>({
24+
name: 'Array shallow copying',
25+
args: [1, 'a'],
26+
setup() {
27+
this.array = [1, 2, 3];
28+
},
29+
validate(fn) {
30+
this.array = [6, 7, 8];
31+
const result = fn.call(this, 1, 'a');
32+
notEqual(result, this.array);
33+
deepEqual(result, this.array);
34+
},
35+
functions: [
36+
{
37+
name: 'Array.from',
38+
fn(dep1, dep2) {
39+
dep1.toExponential(1);
40+
dep2.toLowerCase();
41+
return Array.from(this.array);
42+
},
43+
},
44+
],
45+
});
46+
47+
console.log(result);

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: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@apeleghq/benchmark",
3-
"version": "1.0.4",
3+
"version": "1.0.5",
44
"description": "A statistically rigorous benchmarking library with paired t-tests, baseline correction, and confidence intervals",
55
"type": "module",
66
"main": "./dist/index.cjs",

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ export type {
3737
ISuiteReport,
3838
ITrialMeasurement,
3939
ITrialResult,
40+
SuiteConfig,
4041
} from './types.js';
4142

4243
export * from './reporters/index.js';

src/report.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ import type {
2929
*/
3030
function getBaselineSamples(
3131
baselineName: string,
32-
trials: ITrialResult[],
32+
trials: readonly Readonly<ITrialResult>[],
3333
): number[] {
3434
return trials.map((t) => t.measurements[baselineName].perIterationMs);
3535
}
@@ -38,8 +38,8 @@ function getBaselineSamples(
3838

3939
function computeFunctionStats(
4040
name: string,
41-
trials: ITrialResult[],
42-
baselineSamples: number[],
41+
trials: readonly Readonly<ITrialResult>[],
42+
baselineSamples: readonly number[],
4343
): IFunctionStatistics {
4444
const rawSamples = trials.map((t) => t.measurements[name].perIterationMs);
4545
// Subtract the null-function time measured in the *same* trial
@@ -74,8 +74,8 @@ function computeFunctionStats(
7474
function computePairedComparison(
7575
nameA: string,
7676
nameB: string,
77-
trials: ITrialResult[],
78-
baselineSamples: number[],
77+
trials: readonly Readonly<ITrialResult>[],
78+
baselineSamples: readonly number[],
7979
): IPairedComparison {
8080
// Use baseline-corrected values so that relativeDifference reflects
8181
// actual computation time, not measurement overhead.
@@ -121,7 +121,7 @@ export function generateReport(
121121
name: string,
122122
config: ISuiteReport['config'],
123123
trials: ITrialResult[],
124-
functionNames: string[],
124+
functionNames: readonly string[],
125125
baselineName: string,
126126
): ISuiteReport {
127127
const baselineSamples = getBaselineSamples(baselineName, trials);

src/reporters/advanced.ts

Lines changed: 27 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ function csig(p: number): string {
121121
}
122122

123123
/** Coefficient of variation as a percentage. */
124-
function cvPct(f: IFunctionStatistics): number {
124+
function cvPct(f: Readonly<IFunctionStatistics>): number {
125125
return f.mean > 0 ? (f.stdDev / f.mean) * 100 : 0;
126126
}
127127

@@ -157,7 +157,7 @@ function tpBar(ratio: number, w: number, rank: number): string {
157157
* Mini sparkline histogram of the sample distribution.
158158
* Bins are rendered with Unicode block elements ▁–█.
159159
*/
160-
function sparkline(samples: number[], w: number = 18): string {
160+
function sparkline(samples: readonly number[], w: number = 18): string {
161161
if (samples.length < 2) return '';
162162
const sorted = [...samples].sort((a, b) => a - b);
163163
const min = sorted[0];
@@ -193,7 +193,7 @@ function sparkline(samples: number[], w: number = 18): string {
193193
* - Median: bold white `│`
194194
*/
195195
function miniBox(
196-
f: IFunctionStatistics,
196+
f: Readonly<IFunctionStatistics>,
197197
lo: number,
198198
hi: number,
199199
w: number = 32,
@@ -255,7 +255,7 @@ function secLine(label: string, totalW: number = 76): string {
255255
// Sections
256256
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
257257

258-
function renderHeader(suite: ISuiteReport): string[] {
258+
function renderHeader(suite: Readonly<ISuiteReport>): string[] {
259259
const L: string[] = [];
260260
const { config } = suite;
261261

@@ -296,8 +296,8 @@ function renderHeader(suite: ISuiteReport): string[] {
296296
// ─── Winner announcement ─────────────────────────────────────────────────────
297297

298298
function renderWinner(
299-
fns: IFunctionStatistics[],
300-
comps: IPairedComparison[],
299+
fns: readonly Readonly<IFunctionStatistics>[],
300+
comps: readonly Readonly<IPairedComparison>[],
301301
): string[] {
302302
if (fns.length < 2) return [];
303303

@@ -353,7 +353,9 @@ function renderWinner(
353353

354354
// ─── Leaderboard ─────────────────────────────────────────────────────────────
355355

356-
function renderLeaderboard(fns: IFunctionStatistics[]): string[] {
356+
function renderLeaderboard(
357+
fns: readonly Readonly<IFunctionStatistics>[],
358+
): string[] {
357359
const L: string[] = [];
358360
const fastest = fns[0];
359361

@@ -440,7 +442,9 @@ function renderLeaderboard(fns: IFunctionStatistics[]): string[] {
440442

441443
// ─── Distribution (box plots + sparklines) ───────────────────────────────────
442444

443-
function renderDistribution(fns: IFunctionStatistics[]): string[] {
445+
function renderDistribution(
446+
fns: readonly Readonly<IFunctionStatistics>[],
447+
): string[] {
444448
const L: string[] = [];
445449

446450
L.push(secLine('Distribution'));
@@ -498,7 +502,9 @@ function renderDistribution(fns: IFunctionStatistics[]): string[] {
498502

499503
// ─── Detailed statistics table ───────────────────────────────────────────────
500504

501-
function renderDetailedStats(fns: IFunctionStatistics[]): string[] {
505+
function renderDetailedStats(
506+
fns: readonly Readonly<IFunctionStatistics>[],
507+
): string[] {
502508
const L: string[] = [];
503509

504510
L.push(secLine('Detailed Statistics'));
@@ -552,13 +558,13 @@ function renderDetailedStats(fns: IFunctionStatistics[]): string[] {
552558
// ─── Pairwise comparisons ────────────────────────────────────────────────────
553559

554560
function renderComparisons(
555-
fns: IFunctionStatistics[],
556-
comps: IPairedComparison[],
561+
fns: readonly Readonly<IFunctionStatistics>[],
562+
comps: readonly Readonly<IPairedComparison>[],
557563
): string[] {
558564
if (comps.length === 0) return [];
559565

560566
const L: string[] = [];
561-
const byName: Record<string, IFunctionStatistics> = {};
567+
const byName: Record<string, Readonly<IFunctionStatistics>> = {};
562568
for (const f of fns) byName[f.name] = f;
563569

564570
L.push(secLine('Pairwise Comparisons (paired t-test)'));
@@ -630,8 +636,8 @@ function renderComparisons(
630636
// ─── Speed matrix ────────────────────────────────────────────────────────────
631637

632638
function renderMatrix(
633-
fns: IFunctionStatistics[],
634-
comps: IPairedComparison[],
639+
fns: readonly Readonly<IFunctionStatistics>[],
640+
comps: readonly Readonly<IPairedComparison>[],
635641
): string[] {
636642
// Only render for a reasonable number of functions
637643
if (fns.length < 3 || fns.length > 8) return [];
@@ -713,8 +719,8 @@ function renderMatrix(
713719
// ─── Measurement overhead / baseline ─────────────────────────────────────────
714720

715721
function renderBaseline(
716-
baseline: IFunctionStatistics | undefined,
717-
fastest: IFunctionStatistics,
722+
baseline: Readonly<IFunctionStatistics> | undefined,
723+
fastest: Readonly<IFunctionStatistics>,
718724
): string[] {
719725
if (!baseline) return [];
720726

@@ -812,7 +818,7 @@ function renderFooter(): string[] {
812818
* | Speed matrix | Developers (3-8 fns) |
813819
* | Measurement overhead | Advanced users |
814820
*/
815-
export function formatReport(suite: ISuiteReport): string {
821+
export function formatReport(suite: Readonly<ISuiteReport>): string {
816822
// ── Separate baseline from benchmarks, sort fastest → slowest ──────
817823
const baseline = suite.functions.find((f) => f.name === suite.baselineName);
818824
const fns = suite.functions
@@ -851,7 +857,10 @@ export interface IReporterOptions {
851857
output?: (s: string) => void;
852858
}
853859

854-
function printReport(suite: ISuiteReport, opts?: IReporterOptions): void {
860+
function printReport(
861+
suite: Readonly<ISuiteReport>,
862+
opts?: Readonly<IReporterOptions>,
863+
): void {
855864
const out =
856865
opts?.output ??
857866
(console.log as Exclude<IReporterOptions['output'], undefined>);

src/reporters/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,4 @@
1515

1616
export { default as advanced } from './advanced.js';
1717
export { default as simple } from './simple.js';
18+
export { default as xunit } from './xunit.js';

src/reporters/simple.ts

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,10 @@ export interface IReporterOptions {
5858
* - A bar chart and paired-comparison table are shown when there
5959
* are two or more user functions.
6060
*/
61-
function report(suite: ISuiteReport, opts?: IReporterOptions): void {
61+
function report(
62+
suite: Readonly<ISuiteReport>,
63+
opts?: Readonly<IReporterOptions>,
64+
): void {
6265
const out =
6366
opts?.output ??
6467
(console.log as Exclude<IReporterOptions['output'], undefined>);
@@ -145,7 +148,10 @@ function report(suite: ISuiteReport, opts?: IReporterOptions): void {
145148
/* Section renderers */
146149
/* ================================================================== */
147150

148-
function emitHeader(ln: (s?: string) => void, suite: ISuiteReport): void {
151+
function emitHeader(
152+
ln: (s?: string) => void,
153+
suite: Readonly<ISuiteReport>,
154+
): void {
149155
const { config: c } = suite;
150156
ln(` ${pc.bold(pc.cyan(suite.name))}`);
151157
ln(
@@ -162,9 +168,9 @@ function emitHeader(ln: (s?: string) => void, suite: ISuiteReport): void {
162168

163169
function emitTable(
164170
ln: (s?: string) => void,
165-
fns: IFunctionStatistics[],
166-
fastest: IFunctionStatistics,
167-
unit: Unit,
171+
fns: readonly Readonly<IFunctionStatistics>[],
172+
fastest: Readonly<IFunctionStatistics>,
173+
unit: Readonly<Unit>,
168174
showRel: boolean,
169175
): void {
170176
const nameW = Math.max(...fns.map((f) => f.name.length));
@@ -213,9 +219,9 @@ function emitTable(
213219

214220
function emitChart(
215221
ln: (s?: string) => void,
216-
fns: IFunctionStatistics[],
217-
fastest: IFunctionStatistics,
218-
unit: Unit,
222+
fns: readonly Readonly<IFunctionStatistics>[],
223+
fastest: Readonly<IFunctionStatistics>,
224+
unit: Readonly<Unit>,
219225
W: number,
220226
): void {
221227
const nameW = Math.max(...fns.map((f) => f.name.length));
@@ -245,9 +251,9 @@ function emitChart(
245251

246252
function emitComparisons(
247253
ln: (s?: string) => void,
248-
comps: IPairedComparison[],
249-
fastest: IFunctionStatistics,
250-
unit: Unit,
254+
comps: readonly Readonly<IPairedComparison>[],
255+
fastest: Readonly<IFunctionStatistics>,
256+
unit: Readonly<Unit>,
251257
W: number,
252258
): void {
253259
ln(` ${pc.bold('Comparisons')} ${pc.dim('(paired t-test, α = 0.05)')}`);
@@ -296,8 +302,8 @@ function emitComparisons(
296302

297303
function emitBaseline(
298304
ln: (s?: string) => void,
299-
baseline: IFunctionStatistics,
300-
unit: Unit,
305+
baseline: Readonly<IFunctionStatistics>,
306+
unit: Readonly<Unit>,
301307
): void {
302308
const raw = baseline.rawSamples;
303309
const avg = mean(raw);

src/reporters/xunit.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ const escapeXml = (s: string) => {
3434
};
3535

3636
const safeXml = (
37-
template: TemplateStringsArray,
37+
template: Readonly<TemplateStringsArray>,
3838
...substitutions: unknown[]
3939
): string => {
4040
const escapedSubstitutions = substitutions.map((substitution) =>
@@ -71,7 +71,7 @@ const sign = (n: number): string => (n >= 0 ? '+' : '');
7171

7272
// ── <system-out> plain-text summaries ───────────────────────────────────
7373

74-
function fnSummaryText(f: IFunctionStatistics): string {
74+
function fnSummaryText(f: Readonly<IFunctionStatistics>): string {
7575
return [
7676
safeXml`Benchmark: ${f.name}`,
7777
safeXml``,
@@ -88,7 +88,7 @@ function fnSummaryText(f: IFunctionStatistics): string {
8888
].join('\n');
8989
}
9090

91-
function cmpSummaryText(c: IPairedComparison): string {
91+
function cmpSummaryText(c: Readonly<IPairedComparison>): string {
9292
const dir =
9393
c.meanDifference > 0
9494
? 'slower than'
@@ -125,7 +125,7 @@ function cmpSummaryText(c: IPairedComparison): string {
125125
*/
126126
function buildFnCase(
127127
suite: string,
128-
f: IFunctionStatistics,
128+
f: Readonly<IFunctionStatistics>,
129129
wallClockMs: number,
130130
): string {
131131
return [
@@ -166,7 +166,7 @@ function buildFnCase(
166166
*
167167
* All t-test fields land in `vendor:benchmark.*` with full precision.
168168
*/
169-
function buildCmpCase(suite: string, c: IPairedComparison): string {
169+
function buildCmpCase(suite: string, c: Readonly<IPairedComparison>): string {
170170
return [
171171
safeXml` <testcase name="${c.a + ' vs ' + c.b}" classname="${suite + '.comparisons'}" time="0">`,
172172
safeXml` <properties>`,
@@ -214,7 +214,7 @@ function buildCmpCase(suite: string, c: IPairedComparison): string {
214214
* `vendor:benchmark.*` `<property>` elements with full JS numeric
215215
* precision (lossless IEEE-754 round-trip via default `toString`).
216216
*/
217-
function formatJUnitXml(report: ISuiteReport): string {
217+
function formatJUnitXml(report: Readonly<ISuiteReport>): string {
218218
const timestamp = new Date().toISOString();
219219

220220
// ── Pre-compute per-function wall-clock totals from trial data ──

0 commit comments

Comments
 (0)