Skip to content

Commit 67b46e7

Browse files
authored
feat: allow overridding task function durations (#329)
1 parent 53dbfe6 commit 67b46e7

File tree

5 files changed

+144
-51
lines changed

5 files changed

+144
-51
lines changed

src/bench.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import type {
88
Fn,
99
FnOptions,
1010
RemoveEventListenerOptionsArgument,
11+
ResolvedBenchOptions,
1112
TaskResult,
1213
} from './types'
1314

@@ -51,7 +52,7 @@ export class Bench extends EventTarget {
5152
/**
5253
* The options.
5354
*/
54-
readonly opts: Readonly<BenchOptions>
55+
readonly opts: Readonly<ResolvedBenchOptions>
5556

5657
/**
5758
* The JavaScript runtime environment.

src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@ export type {
99
Fn,
1010
FnHook,
1111
FnOptions,
12+
FnReturnedObject,
1213
Hook,
14+
ResolvedBenchOptions,
1315
Statistics,
1416
TaskEvents,
1517
TaskEventsMap,

src/task.ts

Lines changed: 60 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -105,15 +105,15 @@ export class Task extends EventTarget {
105105
return this
106106
}
107107
this.dispatchEvent(createBenchEvent('start', this))
108-
await this.bench.opts.setup?.(this, 'run')
108+
await this.bench.opts.setup(this, 'run')
109109
const { error, samples: latencySamples } = (await this.benchmark(
110110
'run',
111-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
112-
this.bench.opts.time!,
113-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
114-
this.bench.opts.iterations!
111+
112+
this.bench.opts.time,
113+
114+
this.bench.opts.iterations
115115
)) as { error?: Error; samples?: number[] }
116-
await this.bench.opts.teardown?.(this, 'run')
116+
await this.bench.opts.teardown(this, 'run')
117117

118118
this.processRunResult({ error, latencySamples })
119119

@@ -136,21 +136,21 @@ export class Task extends EventTarget {
136136
)
137137
this.dispatchEvent(createBenchEvent('start', this))
138138

139-
const setupResult = this.bench.opts.setup?.(this, 'run')
139+
const setupResult = this.bench.opts.setup(this, 'run')
140140
invariant(
141141
!isPromiseLike(setupResult),
142142
'`setup` function must be sync when using `runSync()`'
143143
)
144144

145145
const { error, samples: latencySamples } = this.benchmarkSync(
146146
'run',
147-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
148-
this.bench.opts.time!,
149-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
150-
this.bench.opts.iterations!
147+
148+
this.bench.opts.time,
149+
150+
this.bench.opts.iterations
151151
) as { error?: Error; samples?: number[] }
152152

153-
const teardownResult = this.bench.opts.teardown?.(this, 'run')
153+
const teardownResult = this.bench.opts.teardown(this, 'run')
154154
invariant(
155155
!isPromiseLike(teardownResult),
156156
'`teardown` function must be sync when using `runSync()`'
@@ -170,15 +170,15 @@ export class Task extends EventTarget {
170170
return
171171
}
172172
this.dispatchEvent(createBenchEvent('warmup', this))
173-
await this.bench.opts.setup?.(this, 'warmup')
173+
await this.bench.opts.setup(this, 'warmup')
174174
const { error } = (await this.benchmark(
175175
'warmup',
176-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
177-
this.bench.opts.warmupTime!,
178-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
179-
this.bench.opts.warmupIterations!
176+
177+
this.bench.opts.warmupTime,
178+
179+
this.bench.opts.warmupIterations
180180
)) as { error?: Error }
181-
await this.bench.opts.teardown?.(this, 'warmup')
181+
await this.bench.opts.teardown(this, 'warmup')
182182

183183
this.postWarmup(error)
184184
}
@@ -194,21 +194,21 @@ export class Task extends EventTarget {
194194

195195
this.dispatchEvent(createBenchEvent('warmup', this))
196196

197-
const setupResult = this.bench.opts.setup?.(this, 'warmup')
197+
const setupResult = this.bench.opts.setup(this, 'warmup')
198198
invariant(
199199
!isPromiseLike(setupResult),
200200
'`setup` function must be sync when using `runSync()`'
201201
)
202202

203203
const { error } = this.benchmarkSync(
204204
'warmup',
205-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
206-
this.bench.opts.warmupTime!,
207-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
208-
this.bench.opts.warmupIterations!
205+
206+
this.bench.opts.warmupTime,
207+
208+
this.bench.opts.warmupIterations
209209
) as { error?: Error }
210210

211-
const teardownResult = this.bench.opts.teardown?.(this, 'warmup')
211+
const teardownResult = this.bench.opts.teardown(this, 'warmup')
212212
invariant(
213213
!isPromiseLike(teardownResult),
214214
'`teardown` function must be sync when using `runSync()`'
@@ -240,19 +240,25 @@ export class Task extends EventTarget {
240240

241241
let taskTime = 0 // ms;
242242
if (this.async) {
243-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
244-
const taskStart = this.bench.opts.now!()
243+
const taskStart = this.bench.opts.now()
245244
// eslint-disable-next-line no-useless-call
246-
await this.fn.call(this)
247-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
248-
taskTime = this.bench.opts.now!() - taskStart
245+
const fnResult = await this.fn.call(this)
246+
taskTime = this.bench.opts.now() - taskStart
247+
248+
const overriddenDuration = getOverriddenDurationFromFnResult(fnResult)
249+
if (overriddenDuration != null) {
250+
taskTime = overriddenDuration
251+
}
249252
} else {
250-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
251-
const taskStart = this.bench.opts.now!()
253+
const taskStart = this.bench.opts.now()
252254
// eslint-disable-next-line no-useless-call
253-
this.fn.call(this)
254-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
255-
taskTime = this.bench.opts.now!() - taskStart
255+
const fnResult = this.fn.call(this)
256+
taskTime = this.bench.opts.now() - taskStart
257+
258+
const overriddenDuration = getOverriddenDurationFromFnResult(fnResult)
259+
if (overriddenDuration != null) {
260+
taskTime = overriddenDuration
261+
}
256262
}
257263

258264
samples.push(taskTime)
@@ -326,18 +332,21 @@ export class Task extends EventTarget {
326332

327333
let taskTime = 0 // ms;
328334

329-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
330-
const taskStart = this.bench.opts.now!()
335+
const taskStart = this.bench.opts.now()
331336
// eslint-disable-next-line no-useless-call
332-
const result = this.fn.call(this)
333-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
334-
taskTime = this.bench.opts.now!() - taskStart
337+
const fnResult = this.fn.call(this)
338+
taskTime = this.bench.opts.now() - taskStart
335339

336340
invariant(
337-
!isPromiseLike(result),
341+
!isPromiseLike(fnResult),
338342
'task function must be sync when using `runSync()`'
339343
)
340344

345+
const overriddenDuration = getOverriddenDurationFromFnResult(fnResult)
346+
if (overriddenDuration != null) {
347+
taskTime = overriddenDuration
348+
}
349+
341350
samples.push(taskTime)
342351
totalTime += taskTime
343352

@@ -467,3 +476,14 @@ export class Task extends EventTarget {
467476
this.dispatchEvent(createBenchEvent('complete', this))
468477
}
469478
}
479+
480+
/**
481+
*
482+
* @param fnResult - the result of the task function.
483+
* @returns the overridden duration if defined by the function.
484+
*/
485+
function getOverriddenDurationFromFnResult (fnResult: ReturnType<Fn>): number | undefined {
486+
if (fnResult != null && typeof fnResult === 'object' && 'overriddenDuration' in fnResult && typeof fnResult.overriddenDuration === 'number') {
487+
return fnResult.overriddenDuration
488+
}
489+
}

src/types.ts

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -108,10 +108,15 @@ export interface BenchOptions {
108108
export type EventListener = (evt: BenchEvent) => void
109109

110110
/**
111-
* the task function
111+
* The task function.
112+
*
113+
* If you need to provide a custom duration for the task (e.g.: because
114+
* you want to measure a specific part of its execution), you can return an
115+
* object with a `overriddenDuration` field. You should still use
116+
* `bench.opts.now()` to measure that duration.
112117
*/
113118
// eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents
114-
export type Fn = () => Promise<unknown> | unknown
119+
export type Fn = () => FnReturnedObject | Promise<FnReturnedObject | unknown> | unknown
115120

116121
/**
117122
* The task hook function signature.
@@ -148,6 +153,21 @@ export interface FnOptions {
148153
beforeEach?: FnHook
149154
}
150155

156+
/**
157+
* A possible object returned by task functions to override default behaviors,
158+
* like the duration of the function itself.
159+
*/
160+
export interface FnReturnedObject {
161+
/**
162+
* An overridden duration for the task function, to be used instead of the
163+
* duration measured by tinybench when running the benchmark.
164+
*
165+
* This can be useful to measure parts of the execution of a function that are
166+
* hard to execute independently.
167+
*/
168+
overriddenDuration?: number;
169+
}
170+
151171
/**
152172
* The hook function signature.
153173
* If warmup is enabled, the hook will be called twice, once for the warmup and once for the run.
@@ -164,6 +184,18 @@ export type RemoveEventListenerOptionsArgument = Parameters<
164184
typeof EventTarget.prototype.removeEventListener
165185
>[2]
166186

187+
export interface ResolvedBenchOptions extends BenchOptions {
188+
iterations: NonNullable<BenchOptions['iterations']>,
189+
now: NonNullable<BenchOptions['now']>,
190+
setup: NonNullable<BenchOptions['setup']>,
191+
teardown: NonNullable<BenchOptions['teardown']>,
192+
throws: NonNullable<BenchOptions['throws']>,
193+
time: NonNullable<BenchOptions['time']>,
194+
warmup: NonNullable<BenchOptions['warmup']>,
195+
warmupIterations: NonNullable<BenchOptions['warmupIterations']>,
196+
warmupTime: NonNullable<BenchOptions['warmupTime']>,
197+
}
198+
167199
/**
168200
* the statistics object
169201
*/

test/index.test.ts

Lines changed: 46 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -205,10 +205,9 @@ test('bench task runs and time consistency (async)', async () => {
205205

206206
const fooTask = bench.getTask('foo')
207207

208-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
209-
expect(fooTask?.runs).toBeGreaterThanOrEqual(bench.opts.iterations!)
210-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
211-
expect(fooTask?.result?.totalTime).toBeGreaterThanOrEqual(bench.opts.time!)
208+
expect(fooTask?.runs).toBeGreaterThanOrEqual(bench.opts.iterations)
209+
210+
expect(fooTask?.result?.totalTime).toBeGreaterThanOrEqual(bench.opts.time)
212211
})
213212

214213
test('bench task runs and time consistency (sync)', () => {
@@ -221,10 +220,9 @@ test('bench task runs and time consistency (sync)', () => {
221220

222221
const fooTask = bench.getTask('foo')
223222

224-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
225-
expect(fooTask?.runs).toBeGreaterThanOrEqual(bench.opts.iterations!)
226-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
227-
expect(fooTask?.result?.totalTime).toBeGreaterThanOrEqual(bench.opts.time!)
223+
expect(fooTask?.runs).toBeGreaterThanOrEqual(bench.opts.iterations)
224+
225+
expect(fooTask?.result?.totalTime).toBeGreaterThanOrEqual(bench.opts.time)
228226
})
229227

230228
test('events order (async)', async () => {
@@ -1309,3 +1307,43 @@ test('using concurrency should throw (sync)', () => {
13091307
bench.runSync()
13101308
}).toThrowError('Cannot use `concurrency` option when using `runSync`')
13111309
})
1310+
1311+
test('uses overridden task durations (async)', async () => {
1312+
const bench = new Bench({
1313+
iterations: 16,
1314+
now: () => 100,
1315+
throws: true,
1316+
})
1317+
1318+
bench.add('foo', () => {
1319+
return {
1320+
overriddenDuration: bench.opts.now() + 50,
1321+
}
1322+
})
1323+
1324+
await bench.run()
1325+
1326+
expect(bench.getTask('foo')?.result?.latency.mean).toBe(150)
1327+
expect(bench.getTask('foo')?.result?.latency.min).toBe(150)
1328+
expect(bench.getTask('foo')?.result?.latency.max).toBe(150)
1329+
})
1330+
1331+
test('uses overridden task durations (sync)', () => {
1332+
const bench = new Bench({
1333+
iterations: 16,
1334+
now: () => 100,
1335+
throws: true,
1336+
})
1337+
1338+
bench.add('foo', () => {
1339+
return {
1340+
overriddenDuration: bench.opts.now() + 50,
1341+
}
1342+
})
1343+
1344+
bench.runSync()
1345+
1346+
expect(bench.getTask('foo')?.result?.latency.mean).toBe(150)
1347+
expect(bench.getTask('foo')?.result?.latency.min).toBe(150)
1348+
expect(bench.getTask('foo')?.result?.latency.max).toBe(150)
1349+
})

0 commit comments

Comments
 (0)