Skip to content

Commit 584aa71

Browse files
authored
perf: pass testfiles at once when --no-isolate --maxWorkers=1 (#8835)
1 parent 7ee283c commit 584aa71

File tree

7 files changed

+108
-8
lines changed

7 files changed

+108
-8
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ dist
1616
.vite-node
1717
ltex*
1818
.DS_Store
19+
.zed
1920
bench/test/*/*/
2021
**/bench.json
2122
**/browser/browser.json
@@ -35,4 +36,4 @@ test/browser/html/
3536
test/core/html/
3637
.vitest-attachments
3738
explainFiles.txt
38-
.vitest-dump
39+
.vitest-dump

packages/vitest/src/node/pool.ts

Lines changed: 51 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { Awaitable } from '@vitest/utils'
2+
import type { ContextTestEnvironment } from '../types/worker'
23
import type { Vitest } from './core'
34
import type { PoolTask } from './pools/types'
45
import type { TestProject } from './project'
@@ -87,7 +88,7 @@ export function createPool(ctx: Vitest): ProcessPool {
8788

8889
const sorted = await sequencer.sort(specs)
8990
const environments = await getSpecificationsEnvironments(specs)
90-
const groups = groupSpecs(sorted)
91+
const groups = groupSpecs(sorted, environments)
9192

9293
const projectEnvs = new WeakMap<TestProject, Partial<NodeJS.ProcessEnv>>()
9394
const projectExecArgvs = new WeakMap<TestProject, string[]>()
@@ -330,9 +331,8 @@ function getMemoryLimit(config: ResolvedConfig, pool: string) {
330331
return null
331332
}
332333

333-
function groupSpecs(specs: TestSpecification[]) {
334-
// Test files are passed to test runner one at a time, except Typechecker.
335-
// TODO: Should non-isolated test files be passed to test runner all at once?
334+
function groupSpecs(specs: TestSpecification[], environments: Awaited<ReturnType<typeof getSpecificationsEnvironments>>) {
335+
// Test files are passed to test runner one at a time, except for Typechecker or when "--maxWorker=1 --no-isolate"
336336
type SpecsForRunner = TestSpecification[]
337337

338338
// Tests in a single group are executed with `maxWorkers` parallelism.
@@ -346,6 +346,43 @@ function groupSpecs(specs: TestSpecification[]) {
346346
// Type tests are run in a single group, per project
347347
const typechecks: Record<string, TestSpecification[]> = {}
348348

349+
const serializedEnvironmentOptions = new Map<ContextTestEnvironment, string>()
350+
351+
function getSerializedOptions(env: ContextTestEnvironment) {
352+
const options = serializedEnvironmentOptions.get(env)
353+
354+
if (options) {
355+
return options
356+
}
357+
358+
const serialized = JSON.stringify(env.options)
359+
serializedEnvironmentOptions.set(env, serialized)
360+
return serialized
361+
}
362+
363+
function isEqualEnvironments(a: TestSpecification, b: TestSpecification) {
364+
const aEnv = environments.get(a)
365+
const bEnv = environments.get(b)
366+
367+
if (!aEnv && !bEnv) {
368+
return true
369+
}
370+
371+
if (!aEnv || !bEnv || aEnv.name !== bEnv.name) {
372+
return false
373+
}
374+
375+
if (!aEnv.options && !bEnv.options) {
376+
return true
377+
}
378+
379+
if (!aEnv.options || !bEnv.options) {
380+
return false
381+
}
382+
383+
return getSerializedOptions(aEnv) === getSerializedOptions(bEnv)
384+
}
385+
349386
specs.forEach((spec) => {
350387
if (spec.pool === 'typescript') {
351388
typechecks[spec.project.name] ||= []
@@ -361,6 +398,7 @@ function groupSpecs(specs: TestSpecification[]) {
361398
}
362399

363400
const maxWorkers = resolveMaxWorkers(spec.project)
401+
const isolate = spec.project.config.isolate
364402
groups[order] ||= { specs: [], maxWorkers }
365403

366404
// Multiple projects with different maxWorkers but same groupId
@@ -370,6 +408,15 @@ function groupSpecs(specs: TestSpecification[]) {
370408
throw new Error(`Projects "${last}" and "${spec.project.name}" have different 'maxWorkers' but same 'sequence.groupId'.\nProvide unique 'sequence.groupId' for them.`)
371409
}
372410

411+
// Non-isolated single worker can receive all files at once
412+
if (isolate === false && maxWorkers === 1) {
413+
const previous = groups[order].specs[0]?.[0]
414+
415+
if (previous && previous.project.name === spec.project.name && isEqualEnvironments(spec, previous)) {
416+
return groups[order].specs[0].push(spec)
417+
}
418+
}
419+
373420
groups[order].specs.push([spec])
374421
})
375422

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { test } from "vitest"
2+
3+
test("a", () => { })
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { test } from "vitest"
2+
3+
test("b", () => { })
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { test } from "vitest"
2+
3+
test("c", () => { })
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { test } from 'vitest'
2+
3+
test('print config', () => {
4+
// @ts-expect-error -- internal
5+
console.log(JSON.stringify(globalThis.__vitest_worker__.ctx.files.map(file => file.filepath)))
6+
})

test/config/test/pool.test.ts

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import type { SerializedConfig } from 'vitest'
22
import type { TestUserConfig } from 'vitest/node'
3+
import { normalize } from 'pathe'
34
import { assert, describe, expect, test } from 'vitest'
4-
import { runVitest } from '../../test-utils'
5+
import { runVitest, StableTestFileOrderSorter } from '../../test-utils'
56

67
describe.each(['forks', 'threads', 'vmThreads', 'vmForks'])('%s', async (pool) => {
78
test('resolves top-level pool', async () => {
@@ -51,8 +52,39 @@ test('project level pool options overwrites top-level', async () => {
5152
expect(config.fileParallelism).toBe(false)
5253
})
5354

54-
async function getConfig(options: Partial<TestUserConfig>, cliOptions: Partial<TestUserConfig> = {}) {
55-
let config: SerializedConfig | undefined
55+
test('isolated single worker pool receives single testfile at once', async () => {
56+
const files = await getConfig<string[]>({
57+
maxWorkers: 1,
58+
isolate: true,
59+
sequence: { sequencer: StableTestFileOrderSorter },
60+
}, { include: ['print-testfiles.test.ts', 'a.test.ts', 'b.test.ts', 'c.test.ts'] })
61+
62+
expect(files.map(normalizeFilename)).toMatchInlineSnapshot(`
63+
[
64+
"<process-cwd>/fixtures/pool/print-testfiles.test.ts",
65+
]
66+
`)
67+
})
68+
69+
test('non-isolated single worker pool receives all testfiles at once', async () => {
70+
const files = await getConfig<string[]>({
71+
maxWorkers: 1,
72+
isolate: false,
73+
sequence: { sequencer: StableTestFileOrderSorter },
74+
}, { include: ['print-testfiles.test.ts', 'a.test.ts', 'b.test.ts', 'c.test.ts'] })
75+
76+
expect(files.map(normalizeFilename)).toMatchInlineSnapshot(`
77+
[
78+
"<process-cwd>/fixtures/pool/a.test.ts",
79+
"<process-cwd>/fixtures/pool/b.test.ts",
80+
"<process-cwd>/fixtures/pool/c.test.ts",
81+
"<process-cwd>/fixtures/pool/print-testfiles.test.ts",
82+
]
83+
`)
84+
})
85+
86+
async function getConfig<T = SerializedConfig>(options: Partial<TestUserConfig>, cliOptions: Partial<TestUserConfig> = {}): Promise<T> {
87+
let config: T | undefined
5688

5789
await runVitest({
5890
root: './fixtures/pool',
@@ -66,3 +98,8 @@ async function getConfig(options: Partial<TestUserConfig>, cliOptions: Partial<T
6698
assert(config)
6799
return config
68100
}
101+
102+
function normalizeFilename(filename: string) {
103+
return normalize(filename)
104+
.replace(normalize(process.cwd()), '<process-cwd>')
105+
}

0 commit comments

Comments
 (0)