diff --git a/docs/config/index.md b/docs/config/index.md index fb5e08a45fdb..7015323dcf7f 100644 --- a/docs/config/index.md +++ b/docs/config/index.md @@ -658,17 +658,17 @@ export default defineConfig({ ##### poolOptions.threads.maxThreads -- **Type:** `number` +- **Type:** `number | string` - **Default:** _available CPUs_ -Maximum number of threads. You can also use `VITEST_MAX_THREADS` environment variable. +Maximum number or percentage of threads. You can also use `VITEST_MAX_THREADS` environment variable. ##### poolOptions.threads.minThreads -- **Type:** `number` +- **Type:** `number | string` - **Default:** _available CPUs_ -Minimum number of threads. You can also use `VITEST_MIN_THREADS` environment variable. +Minimum number or percentage of threads. You can also use `VITEST_MIN_THREADS` environment variable. ##### poolOptions.threads.singleThread @@ -730,17 +730,17 @@ export default defineConfig({ ##### poolOptions.forks.maxForks -- **Type:** `number` +- **Type:** `number | string` - **Default:** _available CPUs_ -Maximum number of forks. +Maximum number or percentage of forks. ##### poolOptions.forks.minForks -- **Type:** `number` +- **Type:** `number | string` - **Default:** _available CPUs_ -Minimum number of forks. +Minimum number or percentage of forks. ##### poolOptions.forks.isolate @@ -793,17 +793,17 @@ export default defineConfig({ ##### poolOptions.vmThreads.maxThreads -- **Type:** `number` +- **Type:** `number | string` - **Default:** _available CPUs_ -Maximum number of threads. You can also use `VITEST_MAX_THREADS` environment variable. +Maximum number or percentage of threads. You can also use `VITEST_MAX_THREADS` environment variable. ##### poolOptions.vmThreads.minThreads -- **Type:** `number` +- **Type:** `number | string` - **Default:** _available CPUs_ -Minimum number of threads. You can also use `VITEST_MIN_THREADS` environment variable. +Minimum number or percentage of threads. You can also use `VITEST_MIN_THREADS` environment variable. ##### poolOptions.vmThreads.memoryLimit @@ -874,17 +874,17 @@ export default defineConfig({ ##### poolOptions.vmForks.maxForks -- **Type:** `number` +- **Type:** `number | string` - **Default:** _available CPUs_ -Maximum number of threads. You can also use `VITEST_MAX_FORKS` environment variable. +Maximum number or percentage of threads. You can also use `VITEST_MAX_FORKS` environment variable. ##### poolOptions.vmForks.minForks -- **Type:** `number` +- **Type:** `number | string` - **Default:** _available CPUs_ -Minimum number of threads. You can also use `VITEST_MIN_FORKS` environment variable. +Minimum number or percentage of threads. You can also use `VITEST_MIN_FORKS` environment variable. ##### poolOptions.vmForks.memoryLimit @@ -918,15 +918,15 @@ This option doesn't affect tests running in the same file. If you want to run th ### maxWorkers {#maxworkers} -- **Type:** `number` +- **Type:** `number | string` -Maximum number of workers to run tests in. `poolOptions.{threads,vmThreads}.maxThreads`/`poolOptions.forks.maxForks` has higher priority. +Maximum number or percentage of workers to run tests in. `poolOptions.{threads,vmThreads}.maxThreads`/`poolOptions.forks.maxForks` has higher priority. ### minWorkers {#minworkers} -- **Type:** `number` +- **Type:** `number | string` -Minimum number of workers to run tests in. `poolOptions.{threads,vmThreads}.minThreads`/`poolOptions.forks.minForks` has higher priority. +Minimum number or percentage of workers to run tests in. `poolOptions.{threads,vmThreads}.minThreads`/`poolOptions.forks.minForks` has higher priority. ### testTimeout diff --git a/docs/guide/cli-table.md b/docs/guide/cli-table.md index e36b70452893..c343c3bb6250 100644 --- a/docs/guide/cli-table.md +++ b/docs/guide/cli-table.md @@ -61,27 +61,27 @@ | `--pool ` | Specify pool, if not running in the browser (default: `threads`) | | `--poolOptions.threads.isolate` | Isolate tests in threads pool (default: `true`) | | `--poolOptions.threads.singleThread` | Run tests inside a single thread (default: `false`) | -| `--poolOptions.threads.maxThreads ` | Maximum number of threads to run tests in | -| `--poolOptions.threads.minThreads ` | Minimum number of threads to run tests in | +| `--poolOptions.threads.maxThreads ` | Maximum number or percentage of threads to run tests in | +| `--poolOptions.threads.minThreads ` | Minimum number or percentage of threads to run tests in | | `--poolOptions.threads.useAtomics` | Use Atomics to synchronize threads. This can improve performance in some cases, but might cause segfault in older Node versions (default: `false`) | | `--poolOptions.vmThreads.isolate` | Isolate tests in threads pool (default: `true`) | | `--poolOptions.vmThreads.singleThread` | Run tests inside a single thread (default: `false`) | -| `--poolOptions.vmThreads.maxThreads ` | Maximum number of threads to run tests in | -| `--poolOptions.vmThreads.minThreads ` | Minimum number of threads to run tests in | +| `--poolOptions.vmThreads.maxThreads ` | Maximum number or percentage of threads to run tests in | +| `--poolOptions.vmThreads.minThreads ` | Minimum number or percentage of threads to run tests in | | `--poolOptions.vmThreads.useAtomics` | Use Atomics to synchronize threads. This can improve performance in some cases, but might cause segfault in older Node versions (default: `false`) | | `--poolOptions.vmThreads.memoryLimit ` | Memory limit for VM threads pool. If you see memory leaks, try to tinker this value. | | `--poolOptions.forks.isolate` | Isolate tests in forks pool (default: `true`) | | `--poolOptions.forks.singleFork` | Run tests inside a single child_process (default: `false`) | -| `--poolOptions.forks.maxForks ` | Maximum number of processes to run tests in | -| `--poolOptions.forks.minForks ` | Minimum number of processes to run tests in | +| `--poolOptions.forks.maxForks ` | Maximum number or percentage of processes to run tests in | +| `--poolOptions.forks.minForks ` | Minimum number or percentage of processes to run tests in | | `--poolOptions.vmForks.isolate` | Isolate tests in forks pool (default: `true`) | | `--poolOptions.vmForks.singleFork` | Run tests inside a single child_process (default: `false`) | -| `--poolOptions.vmForks.maxForks ` | Maximum number of processes to run tests in | -| `--poolOptions.vmForks.minForks ` | Minimum number of processes to run tests in | +| `--poolOptions.vmForks.maxForks ` | Maximum number or percentage of processes to run tests in | +| `--poolOptions.vmForks.minForks ` | Minimum number or percentage of processes to run tests in | | `--poolOptions.vmForks.memoryLimit ` | Memory limit for VM forks pool. If you see memory leaks, try to tinker this value. | | `--fileParallelism` | Should all test files run in parallel. Use `--no-file-parallelism` to disable (default: `true`) | -| `--maxWorkers ` | Maximum number of workers to run tests in | -| `--minWorkers ` | Minimum number of workers to run tests in | +| `--maxWorkers ` | Maximum number or percentage of workers to run tests in | +| `--minWorkers ` | Minimum number or percentage of workers to run tests in | | `--environment ` | Specify runner environment, if not running in the browser (default: `node`) | | `--passWithNoTests` | Pass when no tests are found | | `--logHeapUsage` | Show the size of heap for each test when running in node | diff --git a/packages/vitest/rollup.config.js b/packages/vitest/rollup.config.js index 9b0693000521..133ec7fdab5e 100644 --- a/packages/vitest/rollup.config.js +++ b/packages/vitest/rollup.config.js @@ -67,6 +67,7 @@ const external = [ 'worker_threads', 'node:worker_threads', 'node:fs', + 'node:os', 'node:stream', 'node:vm', 'inspector', diff --git a/packages/vitest/src/node/cli/cli-config.ts b/packages/vitest/src/node/cli/cli-config.ts index 50293f2e9277..f8fa04c5dbd9 100644 --- a/packages/vitest/src/node/cli/cli-config.ts +++ b/packages/vitest/src/node/cli/cli-config.ts @@ -62,11 +62,11 @@ const poolThreadsCommands: CLIOptions = { description: 'Run tests inside a single thread (default: `false`)', }, maxThreads: { - description: 'Maximum number of threads to run tests in', + description: 'Maximum number or percentage of threads to run tests in', argument: '', }, minThreads: { - description: 'Minimum number of threads to run tests in', + description: 'Minimum number or percentage of threads to run tests in', argument: '', }, useAtomics: { @@ -84,11 +84,11 @@ const poolForksCommands: CLIOptions = { description: 'Run tests inside a single child_process (default: `false`)', }, maxForks: { - description: 'Maximum number of processes to run tests in', + description: 'Maximum number or percentage of processes to run tests in', argument: '', }, minForks: { - description: 'Minimum number of processes to run tests in', + description: 'Minimum number or percentage of processes to run tests in', argument: '', }, execArgv: null, diff --git a/packages/vitest/src/node/config.ts b/packages/vitest/src/node/config.ts index f07163183bb7..85359cc02fc5 100644 --- a/packages/vitest/src/node/config.ts +++ b/packages/vitest/src/node/config.ts @@ -16,7 +16,8 @@ import { } from '../constants' import { benchmarkConfigDefaults, configDefaults } from '../defaults' import { isCI, stdProvider, toArray } from '../utils' -import type { BuiltinPool } from '../types/pool-options' +import type { BuiltinPool, ForksOptions, PoolOptions, ThreadsOptions } from '../types/pool-options' +import { getWorkersCountByPercentage } from '../utils/workers' import { VitestCache } from './cache' import { BaseSequencer } from './sequencers/BaseSequencer' import { RandomSequencer } from './sequencers/RandomSequencer' @@ -97,6 +98,15 @@ export function resolveApiServerConfig( return api } +function resolveInlineWorkerOption(value: string | number): number { + if (typeof value === 'string' && value.trim().endsWith('%')) { + return getWorkersCountByPercentage(value) + } + else { + return Number(value) + } +} + export function resolveConfig( mode: VitestRunMode, options: UserConfig, @@ -176,11 +186,11 @@ export function resolveConfig( } if (resolved.maxWorkers) { - resolved.maxWorkers = Number(resolved.maxWorkers) + resolved.maxWorkers = resolveInlineWorkerOption(resolved.maxWorkers) } if (resolved.minWorkers) { - resolved.minWorkers = Number(resolved.minWorkers) + resolved.minWorkers = resolveInlineWorkerOption(resolved.minWorkers) } resolved.browser ??= {} as any @@ -436,6 +446,32 @@ export function resolveConfig( } } + const poolThreadsOptions = [ + ['threads', 'minThreads'], + ['threads', 'maxThreads'], + ['vmThreads', 'minThreads'], + ['vmThreads', 'maxThreads'], + ] as const satisfies [keyof PoolOptions, keyof ThreadsOptions][] + + for (const [poolOptionKey, workerOptionKey] of poolThreadsOptions) { + if (resolved.poolOptions?.[poolOptionKey]?.[workerOptionKey]) { + resolved.poolOptions[poolOptionKey]![workerOptionKey] = resolveInlineWorkerOption(resolved.poolOptions[poolOptionKey]![workerOptionKey]!) + } + } + + const poolForksOptions = [ + ['forks', 'minForks'], + ['forks', 'maxForks'], + ['vmForks', 'minForks'], + ['vmForks', 'maxForks'], + ] as const satisfies [keyof PoolOptions, keyof ForksOptions][] + + for (const [poolOptionKey, workerOptionKey] of poolForksOptions) { + if (resolved.poolOptions?.[poolOptionKey]?.[workerOptionKey]) { + resolved.poolOptions[poolOptionKey]![workerOptionKey] = resolveInlineWorkerOption(resolved.poolOptions[poolOptionKey]![workerOptionKey]!) + } + } + if (resolved.workspace) { // if passed down from the CLI and it's relative, resolve relative to CWD resolved.workspace diff --git a/packages/vitest/src/types/config.ts b/packages/vitest/src/types/config.ts index b77748a87fb2..6b230a80c18e 100644 --- a/packages/vitest/src/types/config.ts +++ b/packages/vitest/src/types/config.ts @@ -17,7 +17,7 @@ import type { SnapshotStateOptions } from './snapshot' import type { Arrayable, ParsedStack } from './general' import type { BenchmarkUserOptions } from './benchmark' import type { BrowserConfigOptions, ResolvedBrowserOptions } from './browser' -import type { Pool, PoolOptions } from './pool-options' +import type { Pool, PoolOptions, ResolvedPoolOptions } from './pool-options' export type { BrowserScript, BrowserConfigOptions } from './browser' export type { SequenceHooks, SequenceSetupFiles } from '@vitest/runner' @@ -343,13 +343,13 @@ export interface InlineConfig { poolOptions?: PoolOptions /** - * Maximum number of workers to run tests in. `poolOptions.{threads,vmThreads}.maxThreads`/`poolOptions.forks.maxForks` has higher priority. + * Maximum number or percentage of workers to run tests in. `poolOptions.{threads,vmThreads}.maxThreads`/`poolOptions.forks.maxForks` has higher priority. */ - maxWorkers?: number + maxWorkers?: number | string /** - * Minimum number of workers to run tests in. `poolOptions.{threads,vmThreads}.minThreads`/`poolOptions.forks.minForks` has higher priority. + * Minimum number or percentage of workers to run tests in. `poolOptions.{threads,vmThreads}.minThreads`/`poolOptions.forks.minForks` has higher priority. */ - minWorkers?: number + minWorkers?: number | string /** * Should all test files run in parallel. Doesn't affect tests running in the same file. @@ -969,7 +969,7 @@ export interface ResolvedConfig browser: ResolvedBrowserOptions pool: Pool - poolOptions?: PoolOptions + poolOptions?: ResolvedPoolOptions reporters: (InlineReporter | ReporterWithOptions)[] @@ -1009,6 +1009,9 @@ export interface ResolvedConfig enabled: boolean } runner?: string + + maxWorkers: number + minWorkers: number } export type ProjectConfig = Omit< diff --git a/packages/vitest/src/types/pool-options.ts b/packages/vitest/src/types/pool-options.ts index 25e26c1d8cea..a3fbf8a20d0a 100644 --- a/packages/vitest/src/types/pool-options.ts +++ b/packages/vitest/src/types/pool-options.ts @@ -42,12 +42,19 @@ export interface PoolOptions extends Record { vmForks?: ForksOptions & VmOptions } +export interface ResolvedPoolOptions extends PoolOptions { + threads?: ResolvedThreadsOptions & WorkerContextOptions + forks?: ResolvedForksOptions & WorkerContextOptions + vmThreads?: ResolvedThreadsOptions & VmOptions + vmForks?: ResolvedForksOptions & VmOptions +} + export interface ThreadsOptions { /** Minimum amount of threads to use */ - minThreads?: number + minThreads?: number | string /** Maximum amount of threads to use */ - maxThreads?: number + maxThreads?: number | string /** * Run tests inside a single thread. @@ -66,12 +73,17 @@ export interface ThreadsOptions { useAtomics?: boolean } +export interface ResolvedThreadsOptions extends ThreadsOptions { + minThreads?: number + maxThreads?: number +} + export interface ForksOptions { /** Minimum amount of child processes to use */ - minForks?: number + minForks?: number | string /** Maximum amount of child processes to use */ - maxForks?: number + maxForks?: number | string /** * Run tests inside a single fork. @@ -81,6 +93,11 @@ export interface ForksOptions { singleFork?: boolean } +export interface ResolvedForksOptions extends ForksOptions { + minForks?: number + maxForks?: number +} + export interface WorkerContextOptions { /** * Isolate test environment by recycling `worker_threads` or `child_process` after each test diff --git a/packages/vitest/src/utils/workers.ts b/packages/vitest/src/utils/workers.ts new file mode 100644 index 000000000000..f702219d7578 --- /dev/null +++ b/packages/vitest/src/utils/workers.ts @@ -0,0 +1,8 @@ +import os from 'node:os' + +export function getWorkersCountByPercentage(percent: string) { + const maxWorkersCount = os.availableParallelism?.() ?? os.cpus().length + const workersCountByPercentage = Math.round((Number.parseInt(percent) / 100) * maxWorkersCount) + + return Math.max(1, Math.min(maxWorkersCount, workersCountByPercentage)) +} diff --git a/test/config/fixtures/workers-option/example.test.ts b/test/config/fixtures/workers-option/example.test.ts new file mode 100644 index 000000000000..5a275a3fb95f --- /dev/null +++ b/test/config/fixtures/workers-option/example.test.ts @@ -0,0 +1,5 @@ +import { expect, test } from 'vitest' + +test('it works', () => { + expect(true).toBe(true) +}) diff --git a/test/config/fixtures/workers-option/vitest.config.js b/test/config/fixtures/workers-option/vitest.config.js new file mode 100644 index 000000000000..56004c9f9e06 --- /dev/null +++ b/test/config/fixtures/workers-option/vitest.config.js @@ -0,0 +1 @@ +export default {} \ No newline at end of file diff --git a/test/config/test/workers-option.test.ts b/test/config/test/workers-option.test.ts new file mode 100644 index 000000000000..a31fa95f46bf --- /dev/null +++ b/test/config/test/workers-option.test.ts @@ -0,0 +1,56 @@ +import { type UserConfig, describe, expect, test, vi } from 'vitest' + +import { getWorkersCountByPercentage } from 'vitest/src/utils/workers.js' +import * as testUtils from '../../test-utils' + +vi.mock(import('node:os'), async importOriginal => ({ + default: { + ...(await importOriginal()).default, + availableParallelism: () => 10, + }, +})) + +describe('workers util', () => { + test('percent=50% should return 5', () => { + expect(getWorkersCountByPercentage('50%')).toBe(5) + }) + + test('percent=-10% should return 1', () => { + expect(getWorkersCountByPercentage('-10%')).toBe(1) + }) + + test('percent=110% should return 10', () => { + expect(getWorkersCountByPercentage('110%')).toBe(10) + }) +}) + +function runVitest(config: UserConfig) { + return testUtils.runVitest({ ...config, root: './fixtures/workers-option' }) +} + +test('workers percent argument should not throw error', async () => { + const { stderr } = await runVitest({ maxWorkers: '100%', minWorkers: '10%' }) + + expect(stderr).toBe('') +}) + +test.each([ + { poolOption: 'threads' }, + { poolOption: 'vmThreads' }, + { poolOption: 'forks' }, + { poolOption: 'vmForks' }, +] as const)('workers percent argument in $poolOption should not throw error', async ({ poolOption }) => { + let workerOptions = {} + + if (poolOption.toLowerCase().includes('threads')) { + workerOptions = { maxThreads: '100%', minThreads: '10%' } + } + + if (poolOption.toLowerCase().includes('forks')) { + workerOptions = { maxForks: '100%', minForks: '10%' } + } + + const { stderr } = await runVitest({ poolOptions: { [poolOption]: workerOptions } }) + + expect(stderr).toBe('') +})