Skip to content

Commit

Permalink
fix: properly handle promises in benchmark
Browse files Browse the repository at this point in the history
Signed-off-by: Jérôme Benoit <[email protected]>
  • Loading branch information
jerome-benoit committed Oct 16, 2024
1 parent 79aa988 commit f7ee29e
Show file tree
Hide file tree
Showing 5 changed files with 74 additions and 17 deletions.
14 changes: 6 additions & 8 deletions src/benchmark.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import {
} from './reporter/terminal/index.js'
import { runtime } from './runtime.js'
import { now } from './time.js'
import { isAsyncFunction, isFunction, isObject } from './utils.js'
import { isAsyncFnResource, isFunction, isObject } from './utils.js'

let groupName = null
const groups = new Map()
Expand Down Expand Up @@ -127,7 +127,7 @@ export function group(name, cb = undefined) {
before: name.before ?? emptyFunction,
after: name.after ?? emptyFunction,
})
if (isAsyncFunction(cb)) {
if (isAsyncFnResource(cb)) {
cb().then(() => {
groupName = null
})
Expand Down Expand Up @@ -172,7 +172,7 @@ export function bench(name, fn = undefined, opts = {}) {
samples: opts.samples ?? defaultSamples,
warmup: opts.warmup ?? true,
baseline: false,
async: isAsyncFunction(fn),
async: isAsyncFnResource(fn),
})
}

Expand Down Expand Up @@ -212,7 +212,7 @@ export function baseline(name, fn = undefined, opts = {}) {
samples: opts.samples ?? defaultSamples,
warmup: opts.warmup ?? true,
baseline: true,
async: isAsyncFunction(fn),
async: isAsyncFnResource(fn),
})
}

Expand Down Expand Up @@ -362,8 +362,7 @@ export async function run(opts = {}) {
if (once) log('')
log(groupHeader(groupName, opts))
}

isAsyncFunction(groupOpts.before)
isAsyncFnResource(groupOpts.before)
? await groupOpts.before()
: groupOpts.before()

Expand All @@ -373,8 +372,7 @@ export async function run(opts = {}) {
opts,
groupOpts
)

isAsyncFunction(groupOpts.after)
isAsyncFnResource(groupOpts.after)
? await groupOpts.after()
: groupOpts.after()
}
Expand Down
12 changes: 6 additions & 6 deletions src/lib.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import { now } from './time.js'
import {
AsyncFunction,
checkDividend,
isAsyncFunction,
isAsyncFnResource,
isFunction,
isObject,
} from './utils.js'
Expand Down Expand Up @@ -185,7 +185,7 @@ export const overrideBenchmarkOptions = (benchmark, opts) => {
/**
* Measure a function runtime.
*
* @param {Function} [fn] function to measure
* @param {Function} fn function to measure
* @param {Object} [opts={}] options object
* @param {Boolean} [opts.async=undefined] function is async
* @param {Number} [opts.samples=128] minimum number of benchmark samples
Expand All @@ -194,7 +194,7 @@ export const overrideBenchmarkOptions = (benchmark, opts) => {
* @param {Function} [opts.now=undefined] nanoseconds timestamp function
* @param {Function} [opts.before=()=>{}] before function hook
* @param {Function} [opts.after=()=>{}] after function hook
* @returns {Object} benchmark stats
* @returns {Promise<Object>} benchmark stats
*/
export async function measure(fn, opts = {}) {
checkBenchmarkArgs(fn, opts)
Expand All @@ -203,7 +203,7 @@ export async function measure(fn, opts = {}) {
`expected boolean as 'async' option, got ${opts.async.constructor.name}`
)

opts.async = opts.async ?? isAsyncFunction(fn)
opts.async = opts.async ?? isAsyncFnResource(fn)
opts.time = opts.time ?? defaultTime
opts.samples = opts.samples ?? defaultSamples
opts.warmup =
Expand All @@ -216,8 +216,8 @@ export async function measure(fn, opts = {}) {
opts.before = opts.before ?? emptyFunction
opts.after = opts.after ?? emptyFunction

const asyncBefore = isAsyncFunction(opts.before)
const asyncAfter = isAsyncFunction(opts.after)
const asyncBefore = isAsyncFnResource(opts.before)
const asyncAfter = isAsyncFnResource(opts.after)

const asyncFunction = opts.async || asyncBefore || asyncAfter

Expand Down
57 changes: 56 additions & 1 deletion src/utils.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,68 @@
/**
* Checks if a value is a promise-like object.
*
* @param {unknown} maybePromiseLike the value to check
* @returns {Boolean} true if the value is a promise-like object
*/
const isPromiseLike = maybePromiseLike =>
maybePromiseLike !== null &&
typeof maybePromiseLike === 'object' &&
typeof maybePromiseLike.then === 'function'

/**
* Checks if a value is a function.
*
* @param {unknown} fn the value to check
* @returns {Boolean} true if the value is a function
*/
export const isFunction = fn => {
return typeof fn === 'function'
}

export const AsyncFunction = (async () => {}).constructor

export const isAsyncFunction = fn => {
/**
* An async function check helper only considering runtime support async syntax.
*
* @param {Function} fn the function to check
* @returns {Boolean} true if the function is an async function
*/
const isAsyncFunction = fn => {
return AsyncFunction === fn?.constructor
}

/**
* An async function check helper considering runtime support async syntax and promise return.
*
* @param {Function} fn the function to check
* @returns {Boolean} true if the function is an async function or returns a promise
*/
export const isAsyncFnResource = fn => {
if (fn == null) {
return false
}
if (isAsyncFunction(fn)) {
return true
}
try {
const fnCall = fn()
const promiseLike = isPromiseLike(fnCall)
if (promiseLike) {
// silence promise rejection
fnCall.catch(() => {})
}
return promiseLike
} catch {
return false
}
}

/**
* Checks if a value is an object.
*
* @param {unknown} value the value to check
* @returns {Boolean} true if the value is an object
*/
export const isObject = value => {
return Object.prototype.toString.call(value).slice(8, -1) === 'Object'
}
Expand Down
4 changes: 3 additions & 1 deletion tests/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,19 @@ bench('noop', () => {}, {
})
bench('async noop', async () => {})
baseline('baseline noop', () => {})
bench('async noop2', async () => Promise.resolve())
bench('async promise noop', () => Promise.resolve())
bench('error', () => {
throw new Error('error')
})
bench('async promise reject', () => Promise.reject(new Error('reject')))

group(() => {
bench('a', () => {})
bench('b', () => {})
bench('e', () => {
throw new Error("error 'e'")
})
bench('r', () => Promise.reject(new Error("reject 'r'")))
})

group('group', () => {
Expand Down
4 changes: 3 additions & 1 deletion tests/test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,19 @@ bench('noop', () => {}, {
})
bench('async noop', async () => {})
baseline('baseline noop', () => {})
bench('async noop2', async () => Promise.resolve())
bench('async promise noop', () => Promise.resolve())
bench('error', () => {
throw new Error('error')
})
bench('async promise reject', () => Promise.reject(new Error('reject')))

group(() => {
bench('a', () => {})
bench('b', () => {})
bench('e', () => {
throw new Error("error 'e'")
})
bench('r', () => Promise.reject(new Error("reject 'r'")))
})

group('group', () => {
Expand Down

0 comments on commit f7ee29e

Please sign in to comment.