From 0867dfab31183e72af2f8cc82e971f6bfdf30900 Mon Sep 17 00:00:00 2001 From: kobenguyent Date: Tue, 4 Aug 2026 11:50:09 +0200 Subject: [PATCH] fix: workerInitializationDelay is not hardcoded --- docs/configuration.md | 1 + docs/parallel.md | 2 ++ lib/command/workers/runTests.js | 34 ++++++++++++++------------------- lib/workers.js | 10 ++++++++++ 4 files changed, 27 insertions(+), 20 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index 6bc7733fb..ebef6d864 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -44,6 +44,7 @@ export const config = { - `timeout` — default per-test timeout in seconds; a test is killed if it stops responding. - `mocha` — [Mocha options](https://mochajs.org/#configuring-mocha-nodejs), including extra reporters. See [Reporters](/reports). +- `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. **BDD** diff --git a/docs/parallel.md b/docs/parallel.md index e75429764..ea78a8f14 100644 --- a/docs/parallel.md +++ b/docs/parallel.md @@ -22,6 +22,8 @@ npx codeceptjs run-workers 4 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. +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. + ### Distribution strategies `--by` controls how tests spread across workers: diff --git a/lib/command/workers/runTests.js b/lib/command/workers/runTests.js index a564e26c3..9d6519117 100644 --- a/lib/command/workers/runTests.js +++ b/lib/command/workers/runTests.js @@ -129,13 +129,7 @@ let config // Load test and run initPromise = (async function () { try { - // Add staggered delay at the very start to prevent resource conflicts - // Longer delay for browser initialization conflicts - const delay = (workerIndex - 1) * 2000 // 0ms, 2s, 4s, etc. - if (delay > 0) { - await new Promise(resolve => setTimeout(resolve, delay)) - } - + // Import modules dynamically to avoid ES Module loader race conditions in Node 22.x const eventModule = await import('../../event.js') const containerModule = await import('../../container.js') @@ -153,9 +147,9 @@ initPromise = (async function () { Codecept = CodeceptModule.default fixErrorStack = typescriptModule.fixErrorStack loadTests = loadTestsModule.default - + const overrideConfigs = tryOrDefault(() => JSON.parse(options.override), {}) - + let baseConfig try { // IMPORTANT: await is required here since getConfig is async @@ -172,14 +166,14 @@ initPromise = (async function () { await new Promise(resolve => setTimeout(resolve, 100)) process.exit(1) } - + // important deep merge so dynamic things e.g. functions on config are not overridden config = deepMerge(baseConfig, overrideConfigs) - + // Pass workerIndex as child option for output.process() to display worker prefix const optsWithChild = { ...options, child: workerIndex } codecept = new Codecept(config, optsWithChild) - + try { await codecept.init(testRoot) } catch (initErr) { @@ -193,7 +187,7 @@ initPromise = (async function () { process.stderr.write(`${initErr.stack}\n`) process.exit(1) } - + codecept.loadTests() mocha = container.mocha() @@ -279,7 +273,7 @@ async function runPoolTests() { const messageHandler = async eventData => { // Remove handler immediately to prevent duplicate processing parentPort?.off('message', messageHandler) - + if (eventData.type === 'TEST_ASSIGNED') { // In pool mode with ESM, we receive test FILE paths instead of UIDs // because UIDs are not stable across different mocha instances @@ -289,7 +283,7 @@ async function runPoolTests() { // Create a fresh Mocha instance for each test file container.createMocha() const mocha = container.mocha() - + // Load only the assigned test file mocha.files = [testIdentifier] await loadTests(mocha) @@ -348,7 +342,7 @@ async function runPoolTests() { // Set up handler BEFORE sending request to avoid race condition parentPort?.on('message', messageHandler) - + // Now send the request sendToParentThread({ type: 'REQUEST_TEST', workerIndex }) }) @@ -391,13 +385,13 @@ async function runPoolTests() { function filterTestById(testUid) { // In pool mode with ESM, test files are already loaded once at initialization // We just need to filter the existing mocha suite to only include the target test - + // Get the existing mocha instance const mocha = container.mocha() // Save reference to all suites before clearing const allSuites = [...mocha.suite.suites] - + // Clear suites and tests but preserve other mocha settings mocha.suite.suites = [] mocha.suite.tests = [] @@ -406,10 +400,10 @@ function filterTestById(testUid) { let foundTest = false for (const suite of allSuites) { const originalTests = [...suite.tests] - + // Check if this suite has our target test const targetTest = originalTests.find(test => test.uid === testUid) - + if (targetTest) { // Create a filtered suite with only the target test suite.tests = [targetTest] diff --git a/lib/workers.js b/lib/workers.js index aba291665..141af6d6b 100644 --- a/lib/workers.js +++ b/lib/workers.js @@ -508,10 +508,20 @@ class Workers extends EventEmitter { // Create workers and set up message handlers immediately (not in recorder queue) // This prevents a race condition where workers start sending messages before handlers are attached const workerThreads = [] + const staggerDelay = this.codecept.config.workerInitializationDelay !== undefined + ? this.codecept.config.workerInitializationDelay + : 200 + for (const worker of this.workers) { const workerThread = createWorker(worker, this.isPoolMode) this._listenWorkerEvents(workerThread) workerThreads.push(workerThread) + + // Stagger worker creation to prevent CPU spikes + // from massive V8 isolate creation and naturally stagger browser init + if (this.workers.length > 1 && staggerDelay > 0) { + await new Promise(resolve => setTimeout(resolve, staggerDelay)) + } } recorder.add('workers started', () => {