Skip to content

Commit 0867dfa

Browse files
committed
fix: workerInitializationDelay is not hardcoded
1 parent 8b91815 commit 0867dfa

4 files changed

Lines changed: 27 additions & 20 deletions

File tree

docs/configuration.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ export const config = {
4444

4545
- `timeout` — default per-test timeout in seconds; a test is killed if it stops responding.
4646
- `mocha`[Mocha options](https://mochajs.org/#configuring-mocha-nodejs), including extra reporters. See [Reporters](/reports).
47+
- `workerInitializationDelay` — delay in milliseconds between spinning up parallel workers to prevent CPU spikes and stagger browser startup. Defaults to `200`. Set to `0` to disable.
4748

4849
**BDD**
4950

docs/parallel.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ npx codeceptjs run-workers 4
2222

2323
Steps are not streamed to the console in this mode — output from separate threads can't be interleaved cleanly. While workers run, CodeceptJS sets `process.env.RUNS_WITH_WORKERS=true`, so plugins and helpers can branch on it. All `run` options work here too: `--grep "@smoke"`, `-c codecept.conf.js`, `--debug`, and the rest.
2424

25+
By default, workers are created with a staggered delay of 200ms to prevent CPU spikes and stagger browser initializations. You can adjust this via `workerInitializationDelay` in your configuration.
26+
2527
### Distribution strategies
2628

2729
`--by` controls how tests spread across workers:

lib/command/workers/runTests.js

Lines changed: 14 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -129,13 +129,7 @@ let config
129129
// Load test and run
130130
initPromise = (async function () {
131131
try {
132-
// Add staggered delay at the very start to prevent resource conflicts
133-
// Longer delay for browser initialization conflicts
134-
const delay = (workerIndex - 1) * 2000 // 0ms, 2s, 4s, etc.
135-
if (delay > 0) {
136-
await new Promise(resolve => setTimeout(resolve, delay))
137-
}
138-
132+
139133
// Import modules dynamically to avoid ES Module loader race conditions in Node 22.x
140134
const eventModule = await import('../../event.js')
141135
const containerModule = await import('../../container.js')
@@ -153,9 +147,9 @@ initPromise = (async function () {
153147
Codecept = CodeceptModule.default
154148
fixErrorStack = typescriptModule.fixErrorStack
155149
loadTests = loadTestsModule.default
156-
150+
157151
const overrideConfigs = tryOrDefault(() => JSON.parse(options.override), {})
158-
152+
159153
let baseConfig
160154
try {
161155
// IMPORTANT: await is required here since getConfig is async
@@ -172,14 +166,14 @@ initPromise = (async function () {
172166
await new Promise(resolve => setTimeout(resolve, 100))
173167
process.exit(1)
174168
}
175-
169+
176170
// important deep merge so dynamic things e.g. functions on config are not overridden
177171
config = deepMerge(baseConfig, overrideConfigs)
178-
172+
179173
// Pass workerIndex as child option for output.process() to display worker prefix
180174
const optsWithChild = { ...options, child: workerIndex }
181175
codecept = new Codecept(config, optsWithChild)
182-
176+
183177
try {
184178
await codecept.init(testRoot)
185179
} catch (initErr) {
@@ -193,7 +187,7 @@ initPromise = (async function () {
193187
process.stderr.write(`${initErr.stack}\n`)
194188
process.exit(1)
195189
}
196-
190+
197191
codecept.loadTests()
198192
mocha = container.mocha()
199193

@@ -279,7 +273,7 @@ async function runPoolTests() {
279273
const messageHandler = async eventData => {
280274
// Remove handler immediately to prevent duplicate processing
281275
parentPort?.off('message', messageHandler)
282-
276+
283277
if (eventData.type === 'TEST_ASSIGNED') {
284278
// In pool mode with ESM, we receive test FILE paths instead of UIDs
285279
// because UIDs are not stable across different mocha instances
@@ -289,7 +283,7 @@ async function runPoolTests() {
289283
// Create a fresh Mocha instance for each test file
290284
container.createMocha()
291285
const mocha = container.mocha()
292-
286+
293287
// Load only the assigned test file
294288
mocha.files = [testIdentifier]
295289
await loadTests(mocha)
@@ -348,7 +342,7 @@ async function runPoolTests() {
348342

349343
// Set up handler BEFORE sending request to avoid race condition
350344
parentPort?.on('message', messageHandler)
351-
345+
352346
// Now send the request
353347
sendToParentThread({ type: 'REQUEST_TEST', workerIndex })
354348
})
@@ -391,13 +385,13 @@ async function runPoolTests() {
391385
function filterTestById(testUid) {
392386
// In pool mode with ESM, test files are already loaded once at initialization
393387
// We just need to filter the existing mocha suite to only include the target test
394-
388+
395389
// Get the existing mocha instance
396390
const mocha = container.mocha()
397391

398392
// Save reference to all suites before clearing
399393
const allSuites = [...mocha.suite.suites]
400-
394+
401395
// Clear suites and tests but preserve other mocha settings
402396
mocha.suite.suites = []
403397
mocha.suite.tests = []
@@ -406,10 +400,10 @@ function filterTestById(testUid) {
406400
let foundTest = false
407401
for (const suite of allSuites) {
408402
const originalTests = [...suite.tests]
409-
403+
410404
// Check if this suite has our target test
411405
const targetTest = originalTests.find(test => test.uid === testUid)
412-
406+
413407
if (targetTest) {
414408
// Create a filtered suite with only the target test
415409
suite.tests = [targetTest]

lib/workers.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -508,10 +508,20 @@ class Workers extends EventEmitter {
508508
// Create workers and set up message handlers immediately (not in recorder queue)
509509
// This prevents a race condition where workers start sending messages before handlers are attached
510510
const workerThreads = []
511+
const staggerDelay = this.codecept.config.workerInitializationDelay !== undefined
512+
? this.codecept.config.workerInitializationDelay
513+
: 200
514+
511515
for (const worker of this.workers) {
512516
const workerThread = createWorker(worker, this.isPoolMode)
513517
this._listenWorkerEvents(workerThread)
514518
workerThreads.push(workerThread)
519+
520+
// Stagger worker creation to prevent CPU spikes
521+
// from massive V8 isolate creation and naturally stagger browser init
522+
if (this.workers.length > 1 && staggerDelay > 0) {
523+
await new Promise(resolve => setTimeout(resolve, staggerDelay))
524+
}
515525
}
516526

517527
recorder.add('workers started', () => {

0 commit comments

Comments
 (0)