Skip to content

Commit fbf91ae

Browse files
committed
fix: properly handle abortion at task level and benchmark fn error
Signed-off-by: Jérôme Benoit <jerome.benoit@piment-noir.org>
1 parent fcd3b5e commit fbf91ae

File tree

4 files changed

+101
-78
lines changed

4 files changed

+101
-78
lines changed

src/task.ts

Lines changed: 93 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ export class Task extends EventTarget {
4040
runs = 0
4141

4242
/**
43-
* The task synchronous status
43+
* The task asynchronous status
4444
*/
4545
private readonly async: boolean
4646

@@ -222,62 +222,58 @@ export class Task extends EventTarget {
222222
}
223223
}
224224

225-
// TODO: factor out
226225
let totalTime = 0 // ms
227226
const samples: number[] = []
228227
const benchmarkTask = async () => {
229-
if (this.fnOpts.beforeEach != null) {
230-
await this.fnOpts.beforeEach.call(this, mode)
228+
if (this.bench.opts.signal?.aborted) {
229+
return
231230
}
232-
233-
let taskTime = 0 // ms;
234-
if (this.async) {
235-
const taskStart = this.bench.opts.now()
236-
// eslint-disable-next-line no-useless-call
237-
const fnResult = await this.fn.call(this)
238-
taskTime = this.bench.opts.now() - taskStart
239-
240-
const overriddenDuration = getOverriddenDurationFromFnResult(fnResult)
241-
if (overriddenDuration != null) {
242-
taskTime = overriddenDuration
243-
}
244-
} else {
245-
const taskStart = this.bench.opts.now()
246-
// eslint-disable-next-line no-useless-call
247-
const fnResult = this.fn.call(this)
248-
taskTime = this.bench.opts.now() - taskStart
249-
250-
const overriddenDuration = getOverriddenDurationFromFnResult(fnResult)
251-
if (overriddenDuration != null) {
252-
taskTime = overriddenDuration
231+
try {
232+
if (this.fnOpts.beforeEach != null) {
233+
await this.fnOpts.beforeEach.call(this, mode)
253234
}
254-
}
255235

256-
samples.push(taskTime)
257-
totalTime += taskTime
236+
let taskTime: number
237+
if (this.async) {
238+
({ taskTime } = await this.measureOnce())
239+
} else {
240+
({ taskTime } = this.measureOnceSync())
241+
}
258242

259-
if (this.fnOpts.afterEach != null) {
260-
await this.fnOpts.afterEach.call(this, mode)
243+
samples.push(taskTime)
244+
totalTime += taskTime
245+
} finally {
246+
if (this.fnOpts.afterEach != null) {
247+
await this.fnOpts.afterEach.call(this, mode)
248+
}
261249
}
262250
}
263251

264252
try {
265-
const limit = pLimit(this.bench.threshold) // only for task level concurrency
253+
let limit: ReturnType<typeof pLimit> | undefined // only for task level concurrency
254+
if (this.bench.concurrency === 'task') {
255+
limit = pLimit(this.bench.threshold)
256+
}
266257
const promises: Promise<void>[] = [] // only for task level concurrency
267258
while (
268259
// eslint-disable-next-line no-unmodified-loop-condition
269260
(totalTime < time ||
270-
samples.length + limit.activeCount + limit.pendingCount < iterations) &&
261+
samples.length + (limit?.activeCount ?? 0) + (limit?.pendingCount ?? 0) < iterations) &&
271262
!this.bench.opts.signal?.aborted
272263
) {
273264
if (this.bench.concurrency === 'task') {
274-
promises.push(limit(benchmarkTask))
265+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
266+
promises.push((limit!)(benchmarkTask))
275267
} else {
276268
await benchmarkTask()
277269
}
278270
}
279271
if (!this.bench.opts.signal?.aborted && promises.length > 0) {
280272
await Promise.all(promises)
273+
} else if (promises.length > 0) {
274+
// Abort path
275+
// eslint-disable-next-line no-void
276+
void Promise.allSettled(promises)
281277
}
282278
} catch (error) {
283279
return { error }
@@ -310,52 +306,42 @@ export class Task extends EventTarget {
310306
}
311307
}
312308

313-
// TODO: factor out
314-
let totalTime = 0 // ms
309+
let totalTime = 0
315310
const samples: number[] = []
316311
const benchmarkTask = () => {
317-
if (this.fnOpts.beforeEach != null) {
318-
const beforeEachResult = this.fnOpts.beforeEach.call(this, mode)
319-
invariant(
320-
!isPromiseLike(beforeEachResult),
321-
'`beforeEach` function must be sync when using `runSync()`'
322-
)
323-
}
324-
325-
let taskTime = 0 // ms;
326-
327-
const taskStart = this.bench.opts.now()
328-
// eslint-disable-next-line no-useless-call
329-
const fnResult = this.fn.call(this)
330-
taskTime = this.bench.opts.now() - taskStart
331-
332-
invariant(
333-
!isPromiseLike(fnResult),
334-
'task function must be sync when using `runSync()`'
335-
)
336-
337-
const overriddenDuration = getOverriddenDurationFromFnResult(fnResult)
338-
if (overriddenDuration != null) {
339-
taskTime = overriddenDuration
312+
if (this.bench.opts.signal?.aborted) {
313+
return
340314
}
315+
try {
316+
if (this.fnOpts.beforeEach != null) {
317+
const beforeEachResult = this.fnOpts.beforeEach.call(this, mode)
318+
invariant(
319+
!isPromiseLike(beforeEachResult),
320+
'`beforeEach` function must be sync when using `runSync()`'
321+
)
322+
}
341323

342-
samples.push(taskTime)
343-
totalTime += taskTime
344-
345-
if (this.fnOpts.afterEach != null) {
346-
const afterEachResult = this.fnOpts.afterEach.call(this, mode)
347-
invariant(
348-
!isPromiseLike(afterEachResult),
349-
'`afterEach` function must be sync when using `runSync()`'
350-
)
324+
const { taskTime } = this.measureOnceSync()
325+
326+
samples.push(taskTime)
327+
totalTime += taskTime
328+
} finally {
329+
if (this.fnOpts.afterEach != null) {
330+
const afterEachResult = this.fnOpts.afterEach.call(this, mode)
331+
invariant(
332+
!isPromiseLike(afterEachResult),
333+
'`afterEach` function must be sync when using `runSync()`'
334+
)
335+
}
351336
}
352337
}
353338

354339
try {
355340
while (
356341
// eslint-disable-next-line no-unmodified-loop-condition
357-
totalTime < time ||
358-
samples.length < iterations
342+
(totalTime < time ||
343+
samples.length < iterations) &&
344+
!this.bench.opts.signal?.aborted
359345
) {
360346
benchmarkTask()
361347
}
@@ -377,6 +363,34 @@ export class Task extends EventTarget {
377363
return { samples }
378364
}
379365

366+
private async measureOnce (): Promise<{ fnResult: ReturnType<Fn>; taskTime: number }> {
367+
const taskStart = this.bench.opts.now()
368+
// eslint-disable-next-line no-useless-call
369+
const fnResult = await this.fn.call(this)
370+
let taskTime = this.bench.opts.now() - taskStart
371+
const overriddenDuration = getOverriddenDurationFromFnResult(fnResult)
372+
if (overriddenDuration != null) {
373+
taskTime = overriddenDuration
374+
}
375+
return { fnResult, taskTime }
376+
}
377+
378+
private measureOnceSync (): { fnResult: ReturnType<Fn>; taskTime: number } {
379+
const taskStart = this.bench.opts.now()
380+
// eslint-disable-next-line no-useless-call
381+
const fnResult = this.fn.call(this)
382+
let taskTime = this.bench.opts.now() - taskStart
383+
invariant(
384+
!isPromiseLike(fnResult),
385+
'task function must be sync when using `runSync()`'
386+
)
387+
const overriddenDuration = getOverriddenDurationFromFnResult(fnResult)
388+
if (overriddenDuration != null) {
389+
taskTime = overriddenDuration
390+
}
391+
return { fnResult, taskTime }
392+
}
393+
380394
/**
381395
* merge into the result object values
382396
* @param result - the task result object to merge with the current result object values
@@ -406,7 +420,7 @@ export class Task extends EventTarget {
406420
error?: Error
407421
latencySamples?: number[]
408422
}): void {
409-
if (latencySamples) {
423+
if (latencySamples && latencySamples.length > 0) {
410424
this.runs = latencySamples.length
411425
const totalTime = latencySamples.reduce((a, b) => a + b, 0)
412426

@@ -418,16 +432,17 @@ export class Task extends EventTarget {
418432
// Throughput statistics
419433
const throughputSamples = latencySamples
420434
.map(sample =>
421-
sample !== 0 ? 1000 / sample : 1000 / latencyStatistics.mean
435+
sample !== 0
436+
? 1000 / sample
437+
: latencyStatistics.mean !== 0
438+
? 1000 / latencyStatistics.mean
439+
: 0
422440
) // Use latency average as imputed sample
423441
.sort((a, b) => a - b)
424442
const throughputStatistics = getStatisticsSorted(throughputSamples)
425443

426-
if (this.bench.opts.signal?.aborted) {
427-
return
428-
}
429-
430444
this.mergeTaskResult({
445+
aborted: this.bench.opts.signal?.aborted ?? false,
431446
critical: latencyStatistics.critical,
432447
df: latencyStatistics.df,
433448
hz: throughputStatistics.mean,
@@ -481,7 +496,9 @@ function getOverriddenDurationFromFnResult (
481496
fnResult != null &&
482497
typeof fnResult === 'object' &&
483498
'overriddenDuration' in fnResult &&
484-
typeof fnResult.overriddenDuration === 'number'
499+
typeof fnResult.overriddenDuration === 'number' &&
500+
Number.isFinite(fnResult.overriddenDuration) &&
501+
fnResult.overriddenDuration >= 0
485502
) {
486503
return fnResult.overriddenDuration
487504
}

src/types.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -321,6 +321,11 @@ export interface TaskEventsMap {
321321
* The task result object
322322
*/
323323
export interface TaskResult {
324+
/**
325+
* whether the task was aborted
326+
*/
327+
aborted: boolean
328+
324329
/**
325330
* the latency samples critical value
326331
* @deprecated use `.latency.critical` instead

src/utils.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -261,8 +261,8 @@ const average = (samples: number[]) => {
261261
* @returns the variance of the sample
262262
*/
263263
const variance = (samples: number[], avg = average(samples)) => {
264-
const result = samples.reduce((sum, n) => sum + (n - avg) ** 2, 0)
265-
return result / (samples.length - 1) || 0
264+
const sumSq = samples.reduce((sum, n) => sum + (n - avg) ** 2, 0)
265+
return samples.length <= 1 ? 0 : sumSq / (samples.length - 1)
266266
}
267267

268268
/**

test/index.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -332,6 +332,7 @@ test('events order (async)', async () => {
332332
'cycle',
333333
'error-complete',
334334
'abort',
335+
'cycle',
335336
'complete',
336337
'reset',
337338
])

0 commit comments

Comments
 (0)