Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(benchmark infra) Add beforeEachBatchAsync callback #23391

Draft
wants to merge 9 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 16 additions & 6 deletions tools/benchmark/api-report/benchmark.alpha.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,14 @@ import { Test } from 'mocha';
export function benchmark(args: BenchmarkArguments): Test;

// @public
export type BenchmarkArguments = Titled & (BenchmarkSyncArguments | BenchmarkAsyncArguments | CustomBenchmarkArguments);
export type BenchmarkArguments = Titled & BenchmarkRunningOptions;

// @public
export interface BenchmarkAsyncArguments extends BenchmarkAsyncFunction, BenchmarkOptions {
export interface BenchmarkAsyncArguments extends BenchmarkAsyncFunction, BenchmarkOptions, OnBatchAsync {
}

// @public
export interface BenchmarkAsyncFunction extends BenchmarkOptions {
export interface BenchmarkAsyncFunction {
benchmarkFnAsync: () => Promise<unknown>;
}

Expand Down Expand Up @@ -47,7 +47,7 @@ export interface BenchmarkError {
export function benchmarkMemory(testObject: IMemoryTestObject): Test;

// @public
export interface BenchmarkOptions extends MochaExclusiveOptions, HookArguments, BenchmarkTimingOptions, OnBatch, BenchmarkDescription {
export interface BenchmarkOptions extends MochaExclusiveOptions, HookArguments, BenchmarkTimingOptions, BenchmarkDescription {
}

// @public
Expand All @@ -65,11 +65,11 @@ export type BenchmarkResult = BenchmarkError | BenchmarkData;
export type BenchmarkRunningOptions = BenchmarkSyncArguments | BenchmarkAsyncArguments | CustomBenchmarkArguments;

// @public
export interface BenchmarkSyncArguments extends BenchmarkSyncFunction, BenchmarkOptions {
export interface BenchmarkSyncArguments extends BenchmarkSyncFunction, BenchmarkOptions, OnBatch {
}

// @public
export interface BenchmarkSyncFunction extends BenchmarkOptions {
export interface BenchmarkSyncFunction {
benchmarkFn: () => void;
}

Expand Down Expand Up @@ -172,6 +172,14 @@ export interface MochaExclusiveOptions {
// @public
export interface OnBatch {
beforeEachBatch?: () => void;
beforeEachBatchAsync?: never;
}

// @public
export interface OnBatchAsync {
// @deprecated
beforeEachBatch?: () => void;
beforeEachBatchAsync?: () => Promise<void>;
Comment on lines +175 to +182
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change seems unnecessary complex. While we have to make the actual function being timed clearly sync or async to avoid the over head of checking which each iteration, I don't see an issue with making beforeEachBatch?: () => void; just typed as beforeEachBatch?: () => void | promise<unknown>; then just awaiting what ever it returns unconditionally.

That should keep the API identical, except that async beforeEachBatch will work. This is similar to how mocha deals with hooks: same API for sync and async.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's how I started. I was bothered by the lack of compiler error when passing args with synchronous benchmarkFn by async beforeEachBatch

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking at the implementation, I guess that would be an issue with how a few things are currently factored, like runBenchmarkSync, so some of this refactoring might be needed to support async beforeEachBatch.

}

// @public (undocumented)
Expand Down Expand Up @@ -221,9 +229,11 @@ export interface Titled {
export function validateBenchmarkArguments(args: BenchmarkSyncArguments | BenchmarkAsyncArguments): {
isAsync: true;
benchmarkFn: () => Promise<unknown>;
beforeEachBatch?: () => Promise<void>;
} | {
isAsync: false;
benchmarkFn: () => void;
beforeEachBatch?: () => void;
};

```
22 changes: 16 additions & 6 deletions tools/benchmark/api-report/benchmark.beta.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,14 @@ import { Test } from 'mocha';
export function benchmark(args: BenchmarkArguments): Test;

// @public
export type BenchmarkArguments = Titled & (BenchmarkSyncArguments | BenchmarkAsyncArguments | CustomBenchmarkArguments);
export type BenchmarkArguments = Titled & BenchmarkRunningOptions;

// @public
export interface BenchmarkAsyncArguments extends BenchmarkAsyncFunction, BenchmarkOptions {
export interface BenchmarkAsyncArguments extends BenchmarkAsyncFunction, BenchmarkOptions, OnBatchAsync {
}

// @public
export interface BenchmarkAsyncFunction extends BenchmarkOptions {
export interface BenchmarkAsyncFunction {
benchmarkFnAsync: () => Promise<unknown>;
}

Expand Down Expand Up @@ -47,7 +47,7 @@ export interface BenchmarkError {
export function benchmarkMemory(testObject: IMemoryTestObject): Test;

// @public
export interface BenchmarkOptions extends MochaExclusiveOptions, HookArguments, BenchmarkTimingOptions, OnBatch, BenchmarkDescription {
export interface BenchmarkOptions extends MochaExclusiveOptions, HookArguments, BenchmarkTimingOptions, BenchmarkDescription {
}

// @public
Expand All @@ -65,11 +65,11 @@ export type BenchmarkResult = BenchmarkError | BenchmarkData;
export type BenchmarkRunningOptions = BenchmarkSyncArguments | BenchmarkAsyncArguments | CustomBenchmarkArguments;

// @public
export interface BenchmarkSyncArguments extends BenchmarkSyncFunction, BenchmarkOptions {
export interface BenchmarkSyncArguments extends BenchmarkSyncFunction, BenchmarkOptions, OnBatch {
}

// @public
export interface BenchmarkSyncFunction extends BenchmarkOptions {
export interface BenchmarkSyncFunction {
benchmarkFn: () => void;
}

Expand Down Expand Up @@ -172,6 +172,14 @@ export interface MochaExclusiveOptions {
// @public
export interface OnBatch {
beforeEachBatch?: () => void;
beforeEachBatchAsync?: never;
}

// @public
export interface OnBatchAsync {
// @deprecated
beforeEachBatch?: () => void;
beforeEachBatchAsync?: () => Promise<void>;
}

// @public (undocumented)
Expand Down Expand Up @@ -221,9 +229,11 @@ export interface Titled {
export function validateBenchmarkArguments(args: BenchmarkSyncArguments | BenchmarkAsyncArguments): {
isAsync: true;
benchmarkFn: () => Promise<unknown>;
beforeEachBatch?: () => Promise<void>;
} | {
isAsync: false;
benchmarkFn: () => void;
beforeEachBatch?: () => void;
};

```
22 changes: 16 additions & 6 deletions tools/benchmark/api-report/benchmark.public.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,14 @@ import { Test } from 'mocha';
export function benchmark(args: BenchmarkArguments): Test;

// @public
export type BenchmarkArguments = Titled & (BenchmarkSyncArguments | BenchmarkAsyncArguments | CustomBenchmarkArguments);
export type BenchmarkArguments = Titled & BenchmarkRunningOptions;

// @public
export interface BenchmarkAsyncArguments extends BenchmarkAsyncFunction, BenchmarkOptions {
export interface BenchmarkAsyncArguments extends BenchmarkAsyncFunction, BenchmarkOptions, OnBatchAsync {
}

// @public
export interface BenchmarkAsyncFunction extends BenchmarkOptions {
export interface BenchmarkAsyncFunction {
benchmarkFnAsync: () => Promise<unknown>;
}

Expand Down Expand Up @@ -47,7 +47,7 @@ export interface BenchmarkError {
export function benchmarkMemory(testObject: IMemoryTestObject): Test;

// @public
export interface BenchmarkOptions extends MochaExclusiveOptions, HookArguments, BenchmarkTimingOptions, OnBatch, BenchmarkDescription {
export interface BenchmarkOptions extends MochaExclusiveOptions, HookArguments, BenchmarkTimingOptions, BenchmarkDescription {
}

// @public
Expand All @@ -65,11 +65,11 @@ export type BenchmarkResult = BenchmarkError | BenchmarkData;
export type BenchmarkRunningOptions = BenchmarkSyncArguments | BenchmarkAsyncArguments | CustomBenchmarkArguments;

// @public
export interface BenchmarkSyncArguments extends BenchmarkSyncFunction, BenchmarkOptions {
export interface BenchmarkSyncArguments extends BenchmarkSyncFunction, BenchmarkOptions, OnBatch {
}

// @public
export interface BenchmarkSyncFunction extends BenchmarkOptions {
export interface BenchmarkSyncFunction {
benchmarkFn: () => void;
}

Expand Down Expand Up @@ -172,6 +172,14 @@ export interface MochaExclusiveOptions {
// @public
export interface OnBatch {
beforeEachBatch?: () => void;
beforeEachBatchAsync?: never;
}

// @public
export interface OnBatchAsync {
// @deprecated
beforeEachBatch?: () => void;
beforeEachBatchAsync?: () => Promise<void>;
}

// @public (undocumented)
Expand Down Expand Up @@ -221,9 +229,11 @@ export interface Titled {
export function validateBenchmarkArguments(args: BenchmarkSyncArguments | BenchmarkAsyncArguments): {
isAsync: true;
benchmarkFn: () => Promise<unknown>;
beforeEachBatch?: () => Promise<void>;
} | {
isAsync: false;
benchmarkFn: () => void;
beforeEachBatch?: () => void;
};

```
99 changes: 75 additions & 24 deletions tools/benchmark/src/Configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,8 +95,7 @@ for (const type of Object.values(TestType)) {
* Arguments to `benchmark`
* @public
*/
export type BenchmarkArguments = Titled &
(BenchmarkSyncArguments | BenchmarkAsyncArguments | CustomBenchmarkArguments);
export type BenchmarkArguments = Titled & BenchmarkRunningOptions;

/**
* @public
Expand All @@ -113,11 +112,9 @@ export type BenchmarkRunningOptions =
| BenchmarkAsyncArguments
| CustomBenchmarkArguments;

export type BenchmarkRunningOptionsSync = BenchmarkSyncArguments & BenchmarkTimingOptions & OnBatch;
export type BenchmarkRunningOptionsSync = BenchmarkSyncArguments;

export type BenchmarkRunningOptionsAsync = BenchmarkAsyncArguments &
BenchmarkTimingOptions &
OnBatch;
export type BenchmarkRunningOptionsAsync = BenchmarkAsyncArguments;
Comment on lines +115 to +117
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: these two feel redundant now, maybe we should remove them?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fine with me. @CraigMacomber ?


/**
* Object with a "title".
Expand All @@ -134,13 +131,13 @@ export interface Titled {
* Arguments to benchmark a synchronous function
* @public
*/
export interface BenchmarkSyncArguments extends BenchmarkSyncFunction, BenchmarkOptions {}
export interface BenchmarkSyncArguments extends BenchmarkSyncFunction, BenchmarkOptions, OnBatch {}

/**
* Arguments to benchmark a synchronous function
* @public
*/
export interface BenchmarkSyncFunction extends BenchmarkOptions {
export interface BenchmarkSyncFunction {
/**
* The (synchronous) function to benchmark.
*/
Expand All @@ -151,13 +148,16 @@ export interface BenchmarkSyncFunction extends BenchmarkOptions {
* Configuration for benchmarking an asynchronous function.
* @public
*/
export interface BenchmarkAsyncArguments extends BenchmarkAsyncFunction, BenchmarkOptions {}
export interface BenchmarkAsyncArguments
extends BenchmarkAsyncFunction,
BenchmarkOptions,
OnBatchAsync {}

/**
* An asynchronous function to benchmark.
* @public
*/
export interface BenchmarkAsyncFunction extends BenchmarkOptions {
export interface BenchmarkAsyncFunction {
/**
* The asynchronous function to benchmark. The time measured includes all time spent until the returned promise is
* resolved. This includes the event loop or processing other events. For example, a test which calls `setTimeout`
Expand Down Expand Up @@ -209,20 +209,57 @@ export interface BenchmarkTimingOptions {
}

/**
* Set of options that can be provided to a benchmark. These options generally align with the BenchmarkJS options type;
* you can see more documentation {@link https://benchmarkjs.com/docs#options | here}.
* Synchronous operations that can be performed on a per-batch basis.
*
* @remarks
* If you need to perform asynchronous operations, use {@link BenchmarkAsyncFunction} and {@link OnBatchAsync}.
*
* @public
*/
export interface OnBatch {
/**
* Executes before the start of each batch. This has the same semantics as benchmarkjs's `onCycle`:
* Executes synchronously before the start of each batch.
* This has the same semantics as benchmarkjs's `onCycle`:
* https://benchmarkjs.com/docs/#options_onCycle
*
* @remarks
* Beware that batches run `benchmarkFn` more than once: a typical micro-benchmark might involve 10k
* iterations per batch.
*/
beforeEachBatch?: () => void;

/**
* Use {@link BenchmarkAsyncFunction} and {@link OnBatchAsync} if you need an async hook.
*/
beforeEachBatchAsync?: never;
}

/**
* Operations (synchronous or asynchronous) that can be performed on a per-batch basis.
*
* @remarks
* Not compatible with synchronous {@link BenchmarkSyncFunction}.
*
* @public
*/
export interface OnBatchAsync {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OnBatch and OnBatchAsync leave me a bit confused. Is it strictly necessary to have beforeEachBatchAsync?: never; in the former? And should beforeEachBatch?: () => void; really be deprecated on the latter? Seems like we allow both to be defined (since we call both in the "synthetic" beforeAfterBatch we craft inside validateBenchmarkArguments.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The key feature is to block synchronous benchmark with asynchronous beforeEachBatch. I tried a number of strategies, this was the best I came up with.

If you can see a simpler way to keep the tests passing, I'm all for it. Or maybe that requirement I held to isn't worth it? Seems like it to me but open to discussion.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I deprecated the one in the Async type because it's unnecessary, as you pointed out. Later someone could migrate usages to the new Async function and remove that one.

/**
* Executes synchronously before the start of each batch.
*
* @deprecated use {@link OnBatchAsync.beforeEachBatchAsync} instead
*/
beforeEachBatch?: () => void;

/**
* Executes before the start of each batch.
* This has similar semantics to benchmarkjs's `onCycle`, but is asynchronous:
* https://benchmarkjs.com/docs/#options_onCycle
*
* @remarks
* Beware that batches run `benchmarkFn` more than once: a typical micro-benchmark might involve 10k
* iterations per batch.
*/
beforeEachBatchAsync?: () => Promise<void>;
}

/**
Expand All @@ -234,7 +271,6 @@ export interface BenchmarkOptions
extends MochaExclusiveOptions,
HookArguments,
BenchmarkTimingOptions,
OnBatch,
BenchmarkDescription {}

/**
Expand Down Expand Up @@ -386,30 +422,45 @@ export interface HookArguments {
}

/**
* Validates arguments to `benchmark`.
* @public
* Type guard to distinguish between synchronous and asynchronous benchmark arguments.
* @param args - Either {@link BenchmarkSyncArguments} or {@link BenchmarkAsyncArguments}
* @returns true if the arguments are for an asynchronous benchmark, false if they are for a synchronous benchmark.
*/
export function validateBenchmarkArguments(
function isAsync(
args: BenchmarkSyncArguments | BenchmarkAsyncArguments,
):
| { isAsync: true; benchmarkFn: () => Promise<unknown> }
| { isAsync: false; benchmarkFn: () => void } {
): args is BenchmarkAsyncArguments {
const intersection = args as BenchmarkSyncArguments & BenchmarkAsyncArguments;
const isSync = intersection.benchmarkFn !== undefined;
const isAsync = intersection.benchmarkFnAsync !== undefined;
assert(
isSync !== isAsync,
"Exactly one of `benchmarkFn` and `benchmarkFnAsync` should be defined.",
);
if (isSync) {
return { isAsync: false, benchmarkFn: intersection.benchmarkFn };
return isAsync;
}

/**
* Validates arguments to `benchmark`.
* @public
*/
export function validateBenchmarkArguments(
args: BenchmarkSyncArguments | BenchmarkAsyncArguments,
):
| { isAsync: true; benchmarkFn: () => Promise<unknown>; beforeEachBatch?: () => Promise<void> }
| { isAsync: false; benchmarkFn: () => void; beforeEachBatch?: () => void } {
if (isAsync(args)) {
const beforeEachBatch = async (): Promise<void> => {
args.beforeEachBatch?.();
await args.beforeEachBatchAsync?.();
};
return { isAsync: true, benchmarkFn: args.benchmarkFnAsync, beforeEachBatch };
}

return { isAsync: true, benchmarkFn: intersection.benchmarkFnAsync };
return { isAsync: false, benchmarkFn: args.benchmarkFn, beforeEachBatch: args.beforeEachBatch };
}

/**
* Validates arguments to `benchmark`.
* type guard for {@link BenchmarkRunningOptions} to determine if the provided arguments are for a custom benchmark.
* @public
*/
export function benchmarkArgumentsIsCustom(
Expand Down
1 change: 1 addition & 0 deletions tools/benchmark/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export {
BenchmarkSyncFunction,
BenchmarkAsyncFunction,
OnBatch,
OnBatchAsync,
BenchmarkDescription,
CustomBenchmark,
BenchmarkTimer,
Expand Down
Loading
Loading