Skip to content

Commit 86c700e

Browse files
feat(benchmark): add pluggable benchmark provider API (#10799)
Co-authored-by: Vladimir Sheremet <[email protected]>
1 parent b19e5ec commit 86c700e

16 files changed

Lines changed: 493 additions & 128 deletions

File tree

‎docs/.vitepress/config.ts‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1084,6 +1084,10 @@ export default ({ mode }: { mode: string }) => {
10841084
text: 'Custom Pool',
10851085
link: '/guide/advanced/pool',
10861086
},
1087+
{
1088+
text: 'Benchmark Provider',
1089+
link: '/guide/advanced/benchmark-provider',
1090+
},
10871091
],
10881092
},
10891093
// Migration — one-time transitional content: cross-version

‎docs/config/benchmark.md‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,11 +46,18 @@ When defined, Vitest will run all matched files with `import.meta.vitest` inside
4646

4747
Include the `samples` array of per-iteration timings on every benchmark result. Disabled by default to reduce memory usage; enable when a custom reporter or API consumer needs the raw samples.
4848

49+
## benchmark.provider
50+
51+
- **Type:** `string`
52+
- **Default:** `undefined` (uses the built-in provider)
53+
54+
The benchmark provider that executes registered benchmarks and returns their results. Set this to a module path whose default export implements `BenchmarkProvider`. Relative paths are resolved from the project root.
55+
56+
See the [Custom Benchmark Provider](/guide/advanced/benchmark-provider) guide for setup instructions and the provider API.
4957

5058
## benchmark.suppressExportGetterWarnings
5159

5260
- **Type:** `boolean`
5361
- **Default:** `false`
5462

5563
Suppress the warning printed when a benchmark accesses module export getters too many times. Vitest tracks getter access during benchmark runs because Vite's module runner wraps every export in a getter, and excessive access can dominate the measurement (see [Module Runner Overhead](/guide/benchmarking#module-runner-overhead)). Enable this when you've intentionally accepted the overhead, or when the warning is noisy for benchmarks where the getter cost is negligible.
56-
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
# Custom Benchmark Provider <Version type="experimental">5.0.0</Version> <Badge type="danger">advanced</Badge> {#custom-benchmark-provider}
2+
3+
::: warning
4+
This is an advanced, experimental API. If you only need to run benchmarks with Vitest's built-in provider, read the [Benchmarking](/guide/benchmarking) guide instead.
5+
:::
6+
7+
Vitest uses a benchmark provider to execute the functions registered with `bench` and convert their measurements into results that Vitest can report. The built-in provider uses [Tinybench](https://github.com/tinylibs/tinybench), but you can replace it to use another benchmarking engine or execution strategy.
8+
9+
## Setup
10+
11+
Set [`benchmark.provider`](/config/benchmark#benchmark-provider) to the path of your provider module. Relative paths are resolved from the project root.
12+
13+
```ts [vitest.config.ts]
14+
import { defineConfig } from 'vitest/config'
15+
16+
export default defineConfig({
17+
test: {
18+
benchmark: {
19+
provider: './benchmark-provider.ts',
20+
},
21+
},
22+
})
23+
```
24+
25+
The module must has a default export with an object that implements `BenchmarkProvider`. This example wraps Tinybench to demonstrate how registrations and results flow through a provider. If you use Tinybench in your provider, add it as a direct dependency of your project.
26+
27+
```ts [benchmark-provider.ts]
28+
import type { BenchmarkProvider } from 'vitest'
29+
import { Bench } from 'tinybench'
30+
31+
const provider = {
32+
async run({ test, config, registrations, options }) {
33+
const bench = new Bench({
34+
signal: test.context.signal,
35+
retainSamples: config.retainSamples,
36+
...options,
37+
})
38+
39+
for (const { name, fn, fnOpts } of registrations) {
40+
bench.add(name, fn, fnOpts)
41+
}
42+
43+
await bench.run()
44+
45+
return bench.tasks.map((task) => {
46+
const result = task.result
47+
48+
if (result.state === 'errored') {
49+
throw result.error
50+
}
51+
if (result.state !== 'completed') {
52+
throw new Error(`Benchmark "${task.name}" ended in the "${result.state}" state`)
53+
}
54+
55+
return {
56+
...result,
57+
name: task.name,
58+
}
59+
})
60+
},
61+
} satisfies BenchmarkProvider
62+
63+
export default provider
64+
```
65+
66+
## Provider API
67+
68+
Vitest calls `provider.run(group)` when a registration's `.run()` method is called, or once for all runnable registrations passed to `bench.compare()`. The `group` contains:
69+
70+
- `test`: the test that registered the benchmarks. `test.context.signal` is aborted when the run is cancelled.
71+
- `config`: the resolved benchmark configuration for the current project.
72+
- `registrations`: runnable benchmarks in registration order. Every registration contains `name`, `fn`, and optional `fnOpts` for lifecycle hooks, cancellation, async behavior, and sample retention.
73+
- `options`: benchmark run options passed to `.run()` or `bench.compare()`, if any.
74+
75+
The provider is responsible for honoring the run and registration options and for running every benchmark function and its `beforeAll`, `beforeEach`, `afterEach`, and `afterAll` hooks according to the benchmarking engine's lifecycle. If execution fails, throw the error to fail the test.
76+
77+
`run` must resolve to one `BenchResult` for every runnable registration. Results are matched to registrations by `name` and are the source for `.run()` return values, comparison tables, reporters, and saved benchmark results. A custom engine must convert its measurements into the Tinybench-compatible `BenchResult` shape exported by `vitest`.
78+
79+
Registrations created by `bench.from()` are loaded by Vitest and are not passed to the provider.
80+
81+
## Provider Lifetime
82+
83+
Vitest imports the provider module on first use and caches its default export for the lifetime of the worker. The API does not have separate setup or teardown hooks; keep worker-scoped state on the provider object when needed.

‎docs/guide/benchmarking.md‎

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ title: Benchmarking | Guide
44

55
# Benchmarking
66

7-
Vitest lets you write benchmarks alongside your tests using the `bench` fixture from the [test context](/guide/test-context). Benchmarks are powered by [Tinybench](https://github.com/tinylibs/tinybench) and are defined inside regular `test()` calls, giving you access to the full power of Vitest's test runner: retries, lifecycle hooks, filtering, and assertions.
7+
Vitest lets you write benchmarks alongside your tests using the `bench` fixture from the [test context](/guide/test-context). The built-in benchmark provider is powered by [Tinybench](https://github.com/tinylibs/tinybench), and benchmarks are defined inside regular `test()` calls, giving you access to the full power of Vitest's test runner: retries, lifecycle hooks, filtering, and assertions.
88

99
## Defining a Benchmark
1010

@@ -23,18 +23,18 @@ test('parsing performance', async ({ bench }) => {
2323
The `bench()` function registers a benchmark without executing it. Calling `.run()` runs the benchmark and returns the result. After the test completes, Vitest prints a single-row version of the [comparison table](#comparing-benchmarks) (ops/sec, mean time, percentiles, etc.), so you get the same output for a one-off benchmark as you do for `bench.compare()`.
2424

2525
::: warning
26-
The `bench` fixture is only available in files matched by [`benchmark.include`](/config/#benchmark-include) (default: `**/*.{bench,benchmark}.?(c|m)[jt]s?(x)`). Using `{ bench }` inside a regular test file will throw an error.
26+
The `bench` fixture is only available in files matched by [`benchmark.include`](/config/benchmark#benchmark-include) (default: `**/*.{bench,benchmark}.?(c|m)[jt]s?(x)`). Using `{ bench }` inside a regular test file will throw an error.
2727

2828
Whether a file participates in the benchmark run is decided by the filename, not by whether the test uses the `bench` fixture. Renaming `parser.test.ts` to `parser.bench.ts` (or adjusting `benchmark.include`) is what moves it into the benchmark project.
2929
:::
3030

3131
## Running Benchmarks
3232

33-
Benchmark files are matched by [`benchmark.include`](/config/#benchmark-include) (default: `**/*.{bench,benchmark}.?(c|m)[jt]s?(x)`) and run in their own project, separate from your regular tests. There are three ways to run them, depending on whether you want to skip them, run them alongside tests, or run them on their own.
33+
Benchmark files are matched by [`benchmark.include`](/config/benchmark#benchmark-include) (default: `**/*.{bench,benchmark}.?(c|m)[jt]s?(x)`) and run in their own project, separate from your regular tests. There are three ways to run them, depending on whether you want to skip them, run them alongside tests, or run them on their own.
3434

3535
### `vitest` (default)
3636

37-
Without [`benchmark.enabled`](/config/#benchmark-enabled), the `vitest` command only runs regular tests. Benchmark files are ignored entirely. This is the default and the right choice for day-to-day development, since benchmarks are slow and noisy and shouldn't run on every save.
37+
Without [`benchmark.enabled`](/config/benchmark#benchmark-enabled), the `vitest` command only runs regular tests. Benchmark files are ignored entirely. This is the default and the right choice for day-to-day development, since benchmarks are slow and noisy and shouldn't run on every save.
3838

3939
### `vitest` with `benchmark.enabled`
4040

@@ -72,6 +72,8 @@ vitest bench parser
7272
vitest bench -t JSON
7373
```
7474

75+
To execute benchmarks with another benchmarking engine or execution strategy, see the [Custom Benchmark Provider](/guide/advanced/benchmark-provider) guide.
76+
7577
## Comparing Benchmarks
7678

7779
Use `bench.compare()` to compare multiple benchmarks against each other:
@@ -330,7 +332,7 @@ Use the same template in `bench.from()` so each project reads its own artifact.
330332

331333
Benchmarks are inherently flaky: CPU load, thermal throttling, GC pressure, and background processes all affect results. Vitest takes several steps to minimize this noise:
332334

333-
- **Separate project**: Benchmark files are grouped into their own project based on the [`benchmark.include`](/config/#benchmark-include) pattern. The `bench` fixture is only exposed in files matched by that pattern. Using it inside a regular test file will throw an error.
335+
- **Separate project**: Benchmark files are grouped into their own project based on the [`benchmark.include`](/config/benchmark#benchmark-include) pattern. The `bench` fixture is only exposed in files matched by that pattern. Using it inside a regular test file will throw an error.
334336
- **No concurrency**: Tests within a benchmark file always run sequentially. Benchmark files themselves also run one at a time, never in parallel. This prevents benchmarks from interfering with each other.
335337

336338
To further improve stability:

‎packages/ui/client/composables/client/static.ts‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -70,10 +70,10 @@ export function createStaticClient(): VitestClient {
7070
rpc: undefined!,
7171
reconnect: () => registerMetadata(),
7272
waitForConnection: async () => {},
73-
})
73+
}) as VitestClient
7474

75-
ctx.state.filesMap = reactive(ctx.state.filesMap)
76-
ctx.state.idMap = reactive(ctx.state.idMap)
75+
ctx.state.filesMap = reactive(ctx.state.filesMap) as StateManager['filesMap']
76+
ctx.state.idMap = reactive(ctx.state.idMap) as StateManager['idMap']
7777

7878
async function registerMetadata() {
7979
const content = await window.HTML_REPORT_METADATA!

‎packages/vitest/src/defaults.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type {
2-
BenchmarkUserOptions,
32
CoverageOptions,
3+
ResolvedBenchmarkOptions,
44
UserConfig,
55
} from './node/types/config'
66
import type { FieldsWithDefaultValues } from './node/types/coverage'
@@ -14,7 +14,7 @@ export const defaultExclude: string[] = [
1414
'**/node_modules/**',
1515
'**/.git/**',
1616
]
17-
export const benchmarkConfigDefaults: Required<BenchmarkUserOptions> = {
17+
export const benchmarkConfigDefaults: ResolvedBenchmarkOptions = {
1818
enabled: false,
1919
include: ['**/*.{bench,benchmark}.?(c|m)[jt]s?(x)'],
2020
exclude: defaultExclude,

‎packages/vitest/src/node/config/resolveConfig.ts‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,12 @@ export function resolveTestConfig(
294294
...benchmarkConfigDefaults,
295295
...resolved.benchmark,
296296
}
297+
if (resolved.benchmark.provider) {
298+
resolved.benchmark.provider = resolvePath(
299+
resolved.benchmark.provider,
300+
resolved.root,
301+
)
302+
}
297303

298304
const inspector = resolved.inspect || resolved.inspectBrk
299305

‎packages/vitest/src/node/config/serializeConfig.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@ export function serializeConfig(project: TestProject): SerializedConfig {
134134
benchmark: {
135135
enabled: config.benchmark.enabled,
136136
retainSamples: config.benchmark.retainSamples,
137+
provider: config.benchmark.provider,
137138
suppressExportGetterWarnings: config.benchmark.suppressExportGetterWarnings,
138139
projectName: config.benchmark.projectName,
139140
},

‎packages/vitest/src/node/types/benchmark.ts‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,16 @@ export interface BenchmarkUserOptions {
2828
*/
2929
retainSamples?: boolean
3030

31+
/**
32+
* The benchmark provider that executes registered benchmarks and produces
33+
* their results. Provide a path to a module whose default export implements
34+
* `BenchmarkProvider`. The path is resolved relative to the project
35+
* root. If not specified, the built-in provider is used.
36+
*
37+
* @experimental
38+
*/
39+
provider?: string
40+
3141
/**
3242
* Disable warnings when a benchmark accesses module export getters too many times.
3343
* @default false
@@ -43,3 +53,7 @@ export interface BenchmarkUserOptions {
4353
*/
4454
projectName?: string
4555
}
56+
57+
export type ResolvedBenchmarkOptions = Omit<Required<BenchmarkUserOptions>, 'provider'> & {
58+
provider?: string | undefined
59+
}

‎packages/vitest/src/node/types/config.ts‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,13 @@ import type { TestCase, TestModule, TestSuite } from '../reporters/reported-task
2020
import type { TestSequencerConstructor } from '../sequencers/types'
2121
import type { VCSProvider } from '../vcs/vcs'
2222
import type { WatcherTriggerPattern } from '../watcher'
23-
import type { BenchmarkUserOptions } from './benchmark'
23+
import type { BenchmarkUserOptions, ResolvedBenchmarkOptions } from './benchmark'
2424
import type { BrowserConfigOptions, BrowserServerContribution, ResolvedBrowserOptions } from './browser'
2525
import type { CoverageOptions, ResolvedCoverageOptions } from './coverage'
2626
import type { Reporter } from './reporter'
2727

2828
export type { CoverageOptions, ResolvedCoverageOptions }
29-
export type { BenchmarkUserOptions }
29+
export type { BenchmarkUserOptions, ResolvedBenchmarkOptions }
3030
export type { RuntimeConfig, SerializedConfig } from '../../runtime/config'
3131
export type { SequenceHooks, SequenceSetupFiles } from '../../runtime/runner/types'
3232
export type { BrowserConfigOptions, BrowserInstanceOption, BrowserScript } from './browser'
@@ -1227,7 +1227,7 @@ export interface ResolvedConfig
12271227
cliExclude?: string[]
12281228

12291229
project: string[]
1230-
benchmark: Required<BenchmarkUserOptions>
1230+
benchmark: ResolvedBenchmarkOptions
12311231
shard?: {
12321232
index: number
12331233
count: number

0 commit comments

Comments
 (0)