Skip to content

Commit 4fcd45f

Browse files
feat!: fold warmup step into benchmark run (#150)
Signed-off-by: Jérôme Benoit <jerome.benoit@piment-noir.org>
1 parent d9a238a commit 4fcd45f

File tree

7 files changed

+149
-134
lines changed

7 files changed

+149
-134
lines changed

README.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,6 @@ bench
4444
console.log('I am slower');
4545
});
4646

47-
await bench.warmup(); // make results more reliable
4847
await bench.run();
4948

5049
console.log(bench.name);
@@ -111,6 +110,11 @@ export interface Options {
111110
*/
112111
throws?: boolean;
113112

113+
/**
114+
* warmup benchmark @default true
115+
*/
116+
warmup?: boolean;
117+
114118
/**
115119
* warmup time (milliseconds) @default 100
116120
*/
@@ -136,7 +140,6 @@ export type Hook = (task: Task, mode: 'warmup' | 'run') => void | Promise<void>;
136140
```
137141

138142
- `async run()`: run the added tasks that were registered using the `add` method
139-
- `async warmup()`: warmup the benchmark tasks
140143
- `reset()`: reset each task and remove its result
141144
- `add(name: string, fn: Fn, opts?: FnOpts)`: add a benchmark task to the task map
142145
- `Fn`: `() => unknown | Promise<unknown>`
@@ -392,7 +395,6 @@ It may make your benchmarks slower, check [#42](https://github.com/tinylibs/tiny
392395
```ts
393396
bench.threshold = 10; // The maximum number of concurrent tasks to run. Defaults to Number.POSITIVE_INFINITY.
394397
bench.concurrency = 'task'; // The concurrency mode to determine how tasks are run.
395-
await bench.warmup();
396398
await bench.run();
397399
```
398400

src/bench.ts

Lines changed: 42 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@ export default class Bench extends EventTarget {
5252

5353
throws = false;
5454

55+
warmup = true;
56+
5557
warmupTime = defaultMinimumWarmupTime;
5658

5759
warmupIterations = defaultMinimumWarmupIterations;
@@ -70,6 +72,7 @@ export default class Bench extends EventTarget {
7072
super();
7173
this.name = options.name;
7274
this.now = options.now ?? this.now;
75+
this.warmup = options.warmup ?? this.warmup;
7376
this.warmupTime = options.warmupTime ?? this.warmupTime;
7477
this.warmupIterations = options.warmupIterations ?? this.warmupIterations;
7578
this.time = options.time ?? this.time;
@@ -90,6 +93,25 @@ export default class Bench extends EventTarget {
9093
}
9194
}
9295

96+
/**
97+
* warmup the benchmark tasks.
98+
*/
99+
private async warmupTasks(): Promise<void> {
100+
this.dispatchEvent(createBenchEvent('warmup'));
101+
if (this.concurrency === 'bench') {
102+
const limit = pLimit(this.threshold);
103+
const promises: Promise<void>[] = [];
104+
for (const task of this._tasks.values()) {
105+
promises.push(limit(() => task.warmup()));
106+
}
107+
await Promise.all(promises);
108+
} else {
109+
for (const task of this._tasks.values()) {
110+
await task.warmup();
111+
}
112+
}
113+
}
114+
93115
private async runTask(task: Task): Promise<Task> {
94116
if (this.signal?.aborted) {
95117
return task;
@@ -99,9 +121,11 @@ export default class Bench extends EventTarget {
99121

100122
/**
101123
* run the added tasks that were registered using the {@link add} method.
102-
* Note: This method does not do any warmup. Call {@link warmup} for that.
103124
*/
104125
async run(): Promise<Task[]> {
126+
if (this.warmup) {
127+
await this.warmupTasks();
128+
}
105129
let values: Task[] = [];
106130
this.dispatchEvent(createBenchEvent('start'));
107131
if (this.concurrency === 'bench') {
@@ -120,26 +144,6 @@ export default class Bench extends EventTarget {
120144
return values;
121145
}
122146

123-
/**
124-
* warmup the benchmark tasks.
125-
* This is not run by default by the {@link run} method.
126-
*/
127-
async warmup(): Promise<void> {
128-
this.dispatchEvent(createBenchEvent('warmup'));
129-
if (this.concurrency === 'bench') {
130-
const limit = pLimit(this.threshold);
131-
const promises: Promise<void>[] = [];
132-
for (const task of this._tasks.values()) {
133-
promises.push(limit(() => task.warmup()));
134-
}
135-
await Promise.all(promises);
136-
} else {
137-
for (const task of this._tasks.values()) {
138-
await task.warmup();
139-
}
140-
}
141-
}
142-
143147
/**
144148
* reset each task and remove its result
145149
*/
@@ -196,37 +200,24 @@ export default class Bench extends EventTarget {
196200
): (Record<string, string | number> | undefined | null)[] {
197201
return this.tasks.map((task) => {
198202
if (task.result) {
199-
if (task.result.error) {
200-
throw task.result.error;
201-
}
202-
return (
203-
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
204-
convert?.(task) || {
203+
return task.result.error
204+
? (convert?.(task) ?? {
205+
'Task name': task.name,
206+
Error: task.result.error.message,
207+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
208+
Stack: task.result.error.stack!,
209+
Samples: task.result.latency.samples.length,
210+
})
211+
: (convert?.(task) ?? {
205212
'Task name': task.name,
206-
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
207-
'Throughput average (ops/s)': task.result.error
208-
? 'NaN'
209-
: `${task.result.throughput.mean.toFixed(0)} \xb1 ${task.result.throughput.rme.toFixed(2)}%`,
210-
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
211-
'Throughput median (ops/s)': task.result.error
212-
? 'NaN'
213-
: // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
214-
`${task.result.throughput.p50!.toFixed(0)}${Number.parseInt(task.result.throughput.mad!.toFixed(0), 10) > 0 ? ` \xb1 ${task.result.throughput.mad!.toFixed(0)}` : ''}`,
215-
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
216-
'Latency average (ns)': task.result.error
217-
? 'NaN'
218-
: `${(task.result.latency.mean * 1e6).toFixed(2)} \xb1 ${task.result.latency.rme.toFixed(2)}%`,
219-
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
220-
'Latency median (ns)': task.result.error
221-
? 'NaN'
222-
: // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
223-
`${(task.result.latency.p50! * 1e6).toFixed(2)}${Number.parseFloat((task.result.latency.mad! * 1e6).toFixed(2)) > 0 ? ` \xb1 ${(task.result.latency.mad! * 1e6).toFixed(2)}` : ''}`,
224-
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
225-
Samples: task.result.error
226-
? 'NaN'
227-
: task.result.latency.samples.length,
228-
}
229-
);
213+
'Throughput average (ops/s)': `${task.result.throughput.mean.toFixed(0)} \xb1 ${task.result.throughput.rme.toFixed(2)}%`,
214+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
215+
'Throughput median (ops/s)': `${task.result.throughput.p50!.toFixed(0)}${Number.parseInt(task.result.throughput.mad!.toFixed(0), 10) > 0 ? ` \xb1 ${task.result.throughput.mad!.toFixed(0)}` : ''}`,
216+
'Latency average (ns)': `${(task.result.latency.mean * 1e6).toFixed(2)} \xb1 ${task.result.latency.rme.toFixed(2)}%`,
217+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
218+
'Latency median (ns)': `${(task.result.latency.p50! * 1e6).toFixed(2)}${Number.parseFloat((task.result.latency.mad! * 1e6).toFixed(2)) > 0 ? ` \xb1 ${(task.result.latency.mad! * 1e6).toFixed(2)}` : ''}`,
219+
Samples: task.result.latency.samples.length,
220+
});
230221
}
231222
return null;
232223
});

src/task.ts

Lines changed: 27 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -130,8 +130,35 @@ export default class Task extends EventTarget {
130130
return { samples };
131131
}
132132

133+
/**
134+
* warmup the current task
135+
* @internal
136+
*/
137+
async warmup(): Promise<void> {
138+
if (this.result?.error) {
139+
return;
140+
}
141+
this.dispatchEvent(createBenchEvent('warmup', this));
142+
await this.bench.setup(this, 'warmup');
143+
const { error } = (await this.benchmark(
144+
this.bench.warmupTime,
145+
this.bench.warmupIterations,
146+
)) as { error?: Error };
147+
await this.bench.teardown(this, 'warmup');
148+
149+
if (error) {
150+
this.setResult({ error });
151+
if (this.bench.throws) {
152+
throw error;
153+
}
154+
this.dispatchEvent(createBenchEvent('error', this));
155+
this.bench.dispatchEvent(createBenchEvent('error', this));
156+
}
157+
}
158+
133159
/**
134160
* run the current task and write the results in `Task.result` object property
161+
* @internal
135162
*/
136163
async run(): Promise<Task> {
137164
if (this.result?.error) {
@@ -205,29 +232,6 @@ export default class Task extends EventTarget {
205232
return this;
206233
}
207234

208-
/**
209-
* warmup the current task
210-
*/
211-
async warmup(): Promise<void> {
212-
if (this.result?.error) {
213-
return;
214-
}
215-
this.dispatchEvent(createBenchEvent('warmup', this));
216-
await this.bench.setup(this, 'warmup');
217-
const { error } = (await this.benchmark(
218-
this.bench.warmupTime,
219-
this.bench.warmupIterations,
220-
)) as { error?: Error };
221-
await this.bench.teardown(this, 'warmup');
222-
223-
if (error) {
224-
this.setResult({ error });
225-
if (this.bench.throws) {
226-
throw error;
227-
}
228-
}
229-
}
230-
231235
addEventListener<K extends TaskEvents>(
232236
type: K,
233237
listener: TaskEventsMap[K],

src/types.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -338,6 +338,11 @@ export interface Options {
338338
*/
339339
throws?: boolean;
340340

341+
/**
342+
* warmup benchmark @default true
343+
*/
344+
warmup?: boolean;
345+
341346
/**
342347
* warmup time (milliseconds) @default 100
343348
*/

src/utils.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { emptyFunction, tTable } from './constants';
22
import type { Fn, Statistics } from './types';
33

4-
export const nanoToMs = (nano: number) => nano / 1e6;
4+
const nanoToMs = (nano: number) => nano / 1e6;
55

66
const hrtimeBigint = process.hrtime.bigint.bind(process.hrtime);
77
export const hrtimeNow = () => nanoToMs(Number(hrtimeBigint()));

test/index.test.ts

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -89,8 +89,8 @@ test('events order', async () => {
8989
const controller = new AbortController();
9090
const bench = new Bench({
9191
signal: controller.signal,
92-
warmupIterations: 0,
9392
warmupTime: 0,
93+
warmupIterations: 0,
9494
});
9595
bench
9696
// eslint-disable-next-line @typescript-eslint/no-empty-function
@@ -165,8 +165,6 @@ test('events order', async () => {
165165
bench.add('temporary', () => {});
166166
bench.remove('temporary');
167167

168-
await bench.warmup();
169-
170168
setTimeout(() => {
171169
controller.abort();
172170
// the abort task takes 1000ms (500ms time || 10 iterations => 10 * 1000)
@@ -198,10 +196,7 @@ test('events order', async () => {
198196
}, 10000);
199197

200198
test('events order at task completion', async () => {
201-
const bench = new Bench({
202-
warmupIterations: 0,
203-
warmupTime: 0,
204-
});
199+
const bench = new Bench();
205200

206201
bench
207202
.add('foo', async () => {
@@ -231,8 +226,8 @@ test('events order at task completion', async () => {
231226
expect(tasks[1]?.name).toBe('bar');
232227
});
233228

234-
test('error event', async () => {
235-
const bench = new Bench({ time: 50 });
229+
test.each(['warmup', 'run'])('%s error event', async (mode) => {
230+
const bench = new Bench({ time: 100, warmup: mode === 'warmup' });
236231
const err = new Error();
237232

238233
bench.add('error', () => {
@@ -250,8 +245,14 @@ test('error event', async () => {
250245
expect(taskErr).toBe(err);
251246
});
252247

253-
test('throws', async () => {
254-
const bench = new Bench({ iterations: 1, throws: true });
248+
test.each(['warmup', 'run'])('%s throws', async (mode) => {
249+
const iterations = 1;
250+
const bench = new Bench({
251+
iterations,
252+
throws: true,
253+
warmup: mode === 'warmup',
254+
warmupIterations: iterations,
255+
});
255256
const err = new Error();
256257

257258
bench.add('error', () => {
@@ -406,7 +407,6 @@ test('setup and teardown', async () => {
406407
});
407408
const fooTask = bench.getTask('foo');
408409

409-
await bench.warmup();
410410
await bench.run();
411411

412412
expect(setup).toBeCalledWith(fooTask, 'warmup');
@@ -419,8 +419,8 @@ test('task beforeAll, afterAll, beforeEach, afterEach', async () => {
419419
const iterations = 100;
420420
const bench = new Bench({
421421
time: 0,
422-
warmupTime: 0,
423422
iterations,
423+
warmupTime: 0,
424424
warmupIterations: iterations,
425425
});
426426

@@ -449,7 +449,6 @@ test('task beforeAll, afterAll, beforeEach, afterEach', async () => {
449449
},
450450
);
451451

452-
await bench.warmup();
453452
await bench.run();
454453

455454
expect(beforeAll.mock.calls.length).toBe(2 /* warmup + run */);

0 commit comments

Comments
 (0)