perf: pass testfiles at once when --no-isolate --maxWorkers=1 (#8835)

This commit is contained in:
Ari Perkkiö 2025-10-28 13:57:22 +02:00 committed by GitHub
parent 7ee283c965
commit 584aa7148d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 108 additions and 8 deletions

3
.gitignore vendored
View File

@ -16,6 +16,7 @@ dist
.vite-node
ltex*
.DS_Store
.zed
bench/test/*/*/
**/bench.json
**/browser/browser.json
@ -35,4 +36,4 @@ test/browser/html/
test/core/html/
.vitest-attachments
explainFiles.txt
.vitest-dump
.vitest-dump

View File

@ -1,4 +1,5 @@
import type { Awaitable } from '@vitest/utils'
import type { ContextTestEnvironment } from '../types/worker'
import type { Vitest } from './core'
import type { PoolTask } from './pools/types'
import type { TestProject } from './project'
@ -87,7 +88,7 @@ export function createPool(ctx: Vitest): ProcessPool {
const sorted = await sequencer.sort(specs)
const environments = await getSpecificationsEnvironments(specs)
const groups = groupSpecs(sorted)
const groups = groupSpecs(sorted, environments)
const projectEnvs = new WeakMap<TestProject, Partial<NodeJS.ProcessEnv>>()
const projectExecArgvs = new WeakMap<TestProject, string[]>()
@ -330,9 +331,8 @@ function getMemoryLimit(config: ResolvedConfig, pool: string) {
return null
}
function groupSpecs(specs: TestSpecification[]) {
// Test files are passed to test runner one at a time, except Typechecker.
// TODO: Should non-isolated test files be passed to test runner all at once?
function groupSpecs(specs: TestSpecification[], environments: Awaited<ReturnType<typeof getSpecificationsEnvironments>>) {
// Test files are passed to test runner one at a time, except for Typechecker or when "--maxWorker=1 --no-isolate"
type SpecsForRunner = TestSpecification[]
// Tests in a single group are executed with `maxWorkers` parallelism.
@ -346,6 +346,43 @@ function groupSpecs(specs: TestSpecification[]) {
// Type tests are run in a single group, per project
const typechecks: Record<string, TestSpecification[]> = {}
const serializedEnvironmentOptions = new Map<ContextTestEnvironment, string>()
function getSerializedOptions(env: ContextTestEnvironment) {
const options = serializedEnvironmentOptions.get(env)
if (options) {
return options
}
const serialized = JSON.stringify(env.options)
serializedEnvironmentOptions.set(env, serialized)
return serialized
}
function isEqualEnvironments(a: TestSpecification, b: TestSpecification) {
const aEnv = environments.get(a)
const bEnv = environments.get(b)
if (!aEnv && !bEnv) {
return true
}
if (!aEnv || !bEnv || aEnv.name !== bEnv.name) {
return false
}
if (!aEnv.options && !bEnv.options) {
return true
}
if (!aEnv.options || !bEnv.options) {
return false
}
return getSerializedOptions(aEnv) === getSerializedOptions(bEnv)
}
specs.forEach((spec) => {
if (spec.pool === 'typescript') {
typechecks[spec.project.name] ||= []
@ -361,6 +398,7 @@ function groupSpecs(specs: TestSpecification[]) {
}
const maxWorkers = resolveMaxWorkers(spec.project)
const isolate = spec.project.config.isolate
groups[order] ||= { specs: [], maxWorkers }
// Multiple projects with different maxWorkers but same groupId
@ -370,6 +408,15 @@ function groupSpecs(specs: TestSpecification[]) {
throw new Error(`Projects "${last}" and "${spec.project.name}" have different 'maxWorkers' but same 'sequence.groupId'.\nProvide unique 'sequence.groupId' for them.`)
}
// Non-isolated single worker can receive all files at once
if (isolate === false && maxWorkers === 1) {
const previous = groups[order].specs[0]?.[0]
if (previous && previous.project.name === spec.project.name && isEqualEnvironments(spec, previous)) {
return groups[order].specs[0].push(spec)
}
}
groups[order].specs.push([spec])
})

View File

@ -0,0 +1,3 @@
import { test } from "vitest"
test("a", () => { })

View File

@ -0,0 +1,3 @@
import { test } from "vitest"
test("b", () => { })

View File

@ -0,0 +1,3 @@
import { test } from "vitest"
test("c", () => { })

View File

@ -0,0 +1,6 @@
import { test } from 'vitest'
test('print config', () => {
// @ts-expect-error -- internal
console.log(JSON.stringify(globalThis.__vitest_worker__.ctx.files.map(file => file.filepath)))
})

View File

@ -1,7 +1,8 @@
import type { SerializedConfig } from 'vitest'
import type { TestUserConfig } from 'vitest/node'
import { normalize } from 'pathe'
import { assert, describe, expect, test } from 'vitest'
import { runVitest } from '../../test-utils'
import { runVitest, StableTestFileOrderSorter } from '../../test-utils'
describe.each(['forks', 'threads', 'vmThreads', 'vmForks'])('%s', async (pool) => {
test('resolves top-level pool', async () => {
@ -51,8 +52,39 @@ test('project level pool options overwrites top-level', async () => {
expect(config.fileParallelism).toBe(false)
})
async function getConfig(options: Partial<TestUserConfig>, cliOptions: Partial<TestUserConfig> = {}) {
let config: SerializedConfig | undefined
test('isolated single worker pool receives single testfile at once', async () => {
const files = await getConfig<string[]>({
maxWorkers: 1,
isolate: true,
sequence: { sequencer: StableTestFileOrderSorter },
}, { include: ['print-testfiles.test.ts', 'a.test.ts', 'b.test.ts', 'c.test.ts'] })
expect(files.map(normalizeFilename)).toMatchInlineSnapshot(`
[
"<process-cwd>/fixtures/pool/print-testfiles.test.ts",
]
`)
})
test('non-isolated single worker pool receives all testfiles at once', async () => {
const files = await getConfig<string[]>({
maxWorkers: 1,
isolate: false,
sequence: { sequencer: StableTestFileOrderSorter },
}, { include: ['print-testfiles.test.ts', 'a.test.ts', 'b.test.ts', 'c.test.ts'] })
expect(files.map(normalizeFilename)).toMatchInlineSnapshot(`
[
"<process-cwd>/fixtures/pool/a.test.ts",
"<process-cwd>/fixtures/pool/b.test.ts",
"<process-cwd>/fixtures/pool/c.test.ts",
"<process-cwd>/fixtures/pool/print-testfiles.test.ts",
]
`)
})
async function getConfig<T = SerializedConfig>(options: Partial<TestUserConfig>, cliOptions: Partial<TestUserConfig> = {}): Promise<T> {
let config: T | undefined
await runVitest({
root: './fixtures/pool',
@ -66,3 +98,8 @@ async function getConfig(options: Partial<TestUserConfig>, cliOptions: Partial<T
assert(config)
return config
}
function normalizeFilename(filename: string) {
return normalize(filename)
.replace(normalize(process.cwd()), '<process-cwd>')
}