Skip to content

test(deps-installer): isolate the heavy deepRecursive test in its own process#12388

Merged
zkochan merged 1 commit into
mainfrom
raise-deps-installer-test-heap
Jun 13, 2026
Merged

test(deps-installer): isolate the heavy deepRecursive test in its own process#12388
zkochan merged 1 commit into
mainfrom
raise-deps-installer-test-heap

Conversation

@zkochan

@zkochan zkochan commented Jun 13, 2026

Copy link
Copy Markdown
Member

Problem

@pnpm/installing.deps-installer OOMs in CI (FATAL ERROR: Reached heap limit Allocation failed) in the shard that runs test/install/deepRecursive.ts, which resolves @teambit/[email protected] — an enormous circular + peer-dependency graph — from the live registry.

Root cause (reproduced in a CI-faithful container)

I couldn't reproduce this on macOS/Node-24 (it uses less heap there, and my default heap is actually smaller than CI's). Reproduced it in a linux/amd64 + node:22.13.0 container (CI's exact platform, default heap 4144 MB, matching CI):

Environment deepRecursive peak RSS Default heap
macOS / Node 24 / arm64 ~3.19 GB 3192 MB
Linux / Node 22 / amd64 (= CI) ~3.6 GB 4144 MB

So deepRecursive needs ~3.6 GB on CI — it fits the default 4 GB heap on its own (passes), but not with the memory the other ~14 test files in its shard leave behind in the same jest process (jest's --experimental-vm-modules registry isn't reclaimed between files). That overflow is the OOM. There is no recent code regression — peak memory is unchanged across the last 30 commits; the live graph simply grew until deepRecursive + accumulation crossed 4 GB.

Fix

Run deepRecursive in a dedicated jest process (.test:heavy) so it gets the whole default heap to itself, and run the rest (.test:rest) in a separate process with it excluded (negative-lookahead path pattern). The two runs cover every test file exactly once (verified: 1 + 75 = 76).

Crucially, no --max-old-space-size bump — every run stays within Node's default ~4 GB, matching the budget pnpm has in production. deepRecursive alone passing at the default heap was confirmed in the container.

Because deepRecursive was the sole culprit, the two earlier OOM workarounds are reverted as unnecessary:

  • the 5-way sharding of the deps-installer suite, and
  • workerIdleMemoryLimit in the with-registry jest preset.

Caveat

deepRecursive's ~3.6 GB leaves ~0.5 GB of margin under the 4 GB default. Since it resolves a live, growing graph, if it later exceeds 4 GB on its own that's a real pnpm problem (it would OOM in production too), to be fixed by reducing resolver memory — not by raising the limit.


Written by an agent (Claude Code, claude-opus-4-8).

Summary by CodeRabbit

  • Chores
    • Enhanced test execution infrastructure with optimized test partitioning strategy for improved performance and resource efficiency
    • Streamlined Jest configuration settings for improved test execution stability

@coderabbitai

coderabbitai Bot commented Jun 13, 2026

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: b452e19f-c759-4f82-901c-b6a4de7ac1f0

📥 Commits

Reviewing files that changed from the base of the PR and between 94c13cc and c365e6a.

📒 Files selected for processing (3)
  • .meta-updater/src/index.ts
  • __utils__/jest-config/with-registry/jest-preset.js
  • installing/deps-installer/package.json

📝 Walkthrough

Walkthrough

This PR refactors the test execution strategy for @pnpm/installing.deps-installer by replacing shard-based partitioning with a dedicated heavy/rest split. The .test:heavy script runs memory-intensive deepRecursive tests, while .test:rest runs remaining tests. Unused sharding constants and Jest memory limits are removed.

Changes

Test Execution Strategy Refactoring

Layer / File(s) Summary
Test Script Generation Strategy
.meta-updater/src/index.ts
Removes depsInstallerTestShardCount constant and updates code generation to replace shard-based execution with separate .test:heavy (deepRecursive only) and .test:rest (excluding deepRecursive) Jest invocations that compose into a single .test sequence.
Jest Configuration Cleanup
__utils__/jest-config/with-registry/jest-preset.js
Removes workerIdleMemoryLimit: '1500MB' setting and explanatory comment, simplifying Jest worker configuration.
Generated Package.json Test Scripts
installing/deps-installer/package.json
Replaces .test:shard shard-based script with split .test:heavy and .test:rest Jest subsets, both running against the same registry-mock port and Node options.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Possibly related PRs

  • pnpm/pnpm#12384: Directly modifies workerIdleMemoryLimit setting in the same Jest preset file (PR adds it back, this PR removes it).
  • pnpm/pnpm#12376: Introduces the shard-based test execution that this PR replaces with the heavy/rest split strategy, updating the same meta-updater and package.json test scripts.

Suggested reviewers

  • KSXGitHub

Poem

🐰 Shards no more, the tests now split,
Heavy loads and light bits fit,
DeepRecursive gets its due,
Memory limits—adieu, adieu!
Swift and tidy, PR so clean.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch raise-deps-installer-test-heap

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint install timed out. The project may have too many dependencies for the sandbox.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@zkochan
zkochan force-pushed the raise-deps-installer-test-heap branch from 6d159d4 to e0bfe30 Compare June 13, 2026 18:16
@zkochan zkochan changed the title test(deps-installer): raise the test heap to 8 GB to stop the OOM test(deps-installer): isolate the heavy deepRecursive test in its own process Jun 13, 2026
… process

test/install/deepRecursive.ts resolves @teambit/bit's enormous circular and
peer-dependency graph. Measured in a CI-faithful container (Linux, Node
22.13.0, amd64, default ~4 GB heap) it peaks at ~3.6 GB — it fits the default
heap on its own, but not with the memory the other deps-installer test files
leave behind in the same jest process (the --experimental-vm-modules module
registry is not reclaimed between files). That overflow is the
"FATAL ERROR: Reached heap limit" the CI suite hit.

Run deepRecursive in a dedicated jest process (.test:heavy) so it gets the
whole default heap to itself, and run the rest (.test:rest) in a separate
process with it excluded via a negative-lookahead path pattern. The two runs
cover every test file exactly once.

This makes the earlier OOM workarounds unnecessary, so they are reverted:
- the 5-way sharding of the suite (deepRecursive was the sole culprit; the
  remaining files are a subset of what historically ran in one process), and
- the workerIdleMemoryLimit in the with-registry jest preset.

No global heap bump: every run stays within Node's default ~4 GB, matching
the budget pnpm has in production.
@zkochan
zkochan force-pushed the raise-deps-installer-test-heap branch from e0bfe30 to c365e6a Compare June 13, 2026 19:31
@zkochan
zkochan marked this pull request as ready for review June 13, 2026 20:21
@zkochan
zkochan merged commit 5b402ea into main Jun 13, 2026
14 of 15 checks passed
@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Jun 13, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (2) 📘 Rule violations (0)

Grey Divider


Action required

1. Worker recycling removed 🐞 Bug ☼ Reliability
Description
@pnpm/jest-config/with-registry no longer configures workerIdleMemoryLimit, so with `maxWorkers:
1 and --experimental-vm-modules` the same long-lived Jest process retains memory across many test
files and can hit Node’s heap limit. This affects every package using the with-registry preset,
not just @pnpm/installing.deps-installer.
Code

utils/jest-config/with-registry/jest-preset.js[R7-12]

  // Unfortunately, this means that if two such tests will run at the same time,
  // they may break each other.
  maxWorkers: 1,
-  // Recycle the test worker once its heap crosses this limit. Under
-  // `--experimental-vm-modules` Jest's VM module registry is never released
-  // between test files, so a long-running process climbs to Node's ~4 GB
-  // old-space ceiling and dies with an out-of-memory FATAL ERROR. Setting a
-  // limit is also what keeps `maxWorkers: 1` from collapsing to in-band
-  // execution (see `shouldRunInBand`): a recyclable worker is spawned instead,
-  // still serial, so the leaked memory is reclaimed between files without
-  // reintroducing the dist-tag races `maxWorkers: 1` prevents.
-  workerIdleMemoryLimit: '1500MB',
  // Force Jest to exit after globalTeardown completes.  The Verdaccio server
  // and lifecycle child-processes spawned during tests may leave ref'd handles
  // that prevent the process from exiting on its own.
Evidence
The preset still forces single-worker execution but no longer has any memory-based recycling;
meanwhile the repo continues to run Jest with --experimental-vm-modules and even documents that
the VM module registry isn’t reclaimed between files, which means memory can accumulate across a
suite. Multiple packages use the with-registry preset and run Jest with these NODE_OPTIONS, so the
risk is repo-wide.

utils/jest-config/with-registry/jest-preset.js[4-16]
.meta-updater/src/index.ts[368-383]
installing/deps-installer/package.json[56-58]
installing/deps-installer/package.json[171-174]
installing/commands/package.json[26-33]
installing/commands/package.json[139-141]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`__utils__/jest-config/with-registry/jest-preset.js` removed `workerIdleMemoryLimit`, which was the only built-in mechanism to recycle the Jest worker process and reclaim heap between test files when running under `--experimental-vm-modules`.

### Issue Context
- The repo’s test scripts commonly run Jest with `--experimental-vm-modules`.
- The PR itself documents that under this mode, Jest’s VM module registry is not reclaimed between files.
- The `with-registry` preset is used across many packages, so this is a cross-suite reliability regression.

### Fix Focus Areas
- __utils__/jest-config/with-registry/jest-preset.js[4-16]
- .meta-updater/src/index.ts[368-386]

### Implementation notes
- Re-add `workerIdleMemoryLimit` to the `with-registry` preset (potentially keeping the previous value) so long suites don’t accumulate unreclaimed VM-module memory across files.
- If the intent is to avoid recycling for specific suites, consider making it configurable via env var (default on), rather than removing it globally.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Informational

2. Overbroad rest-test regex 🐞 Bug ⚙ Maintainability
Description
The new .test:rest uses a negative-lookahead regex that excludes any test file whose path contains
the substring deepRecursive, not specifically test/install/deepRecursive.ts. This can silently
skip unrelated tests in the future if their path happens to include that substring.
Code

installing/deps-installer/package.json[R56-58]

+    ".test": "pn .test:heavy && pn .test:rest",
+    ".test:heavy": "cross-env PNPM_REGISTRY_MOCK_PORT=7769 NODE_OPTIONS=\"$NODE_OPTIONS --experimental-vm-modules --disable-warning=ExperimentalWarning --disable-warning=DEP0169\" jest test/install/deepRecursive.ts",
+    ".test:rest": "cross-env PNPM_REGISTRY_MOCK_PORT=7769 NODE_OPTIONS=\"$NODE_OPTIONS --experimental-vm-modules --disable-warning=ExperimentalWarning --disable-warning=DEP0169\" jest \"^(?!.*deepRecursive)\""
Evidence
The scripts explicitly run deepRecursive.ts in .test:heavy and exclude via a substring-based
negative lookahead in .test:rest, which is broader than the single target file.

installing/deps-installer/package.json[56-58]
.meta-updater/src/index.ts[379-383]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`.test:rest` excludes tests via `jest "^(?!.*deepRecursive)"`, which is broader than needed and will exclude any future test file containing `deepRecursive` in its path.

### Issue Context
`.test:heavy` already targets the exact file (`test/install/deepRecursive.ts`), so `.test:rest` can exclude that exact path rather than any substring match.

### Fix Focus Areas
- installing/deps-installer/package.json[56-58]
- .meta-updater/src/index.ts[379-383]

### Implementation notes
- Prefer a more specific exclusion, e.g. using a regex that matches all tests except the exact path, or using Jest’s ignore options to ignore only `test/install/deepRecursive.ts` for the rest run.
- Keep the behavior “(heavy) + (rest) covers each test exactly once.”

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

@zkochan
zkochan deleted the raise-deps-installer-test-heap branch June 13, 2026 20:21
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

PR Summary by Qodo

Isolate deps-installer deepRecursive test into its own Jest process to avoid CI OOM
🧪 Tests ⚙️ Configuration changes 🕐 10-20 Minutes

Grey Divider

Walkthroughs

Description
• Split deps-installer tests into heavy vs rest Jest runs to avoid heap accumulation OOM.
• Remove prior OOM workarounds (test sharding and workerIdleMemoryLimit) as unnecessary.
• Keep Node default heap budget by isolating the high-memory deepRecursive registry resolution test.
Diagram
graph TD
  A["CI: deps-installer suite"] --> B["pn .test"] --> H["Test env (NODE_OPTIONS + registry port)"] --> C[".test:heavy (jest)"] --> D["deepRecursive.ts"]
  H --> E[".test:rest (jest)"] --> F["all other tests"]
  C --> G["with-registry preset"]
  E --> G
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Increase Node heap size globally
  • ➕ Simplest change (single NODE_OPTIONS tweak)
  • ➕ Avoids modifying how tests are selected/excluded
  • ➖ Masks real memory ceilings and diverges from production heap budgets
  • ➖ Still risks further growth and future OOM at a higher threshold
2. Keep worker recycling via workerIdleMemoryLimit
  • ➕ Addresses Jest memory retention without changing test selection
  • ➕ Keeps single logical test command
  • ➖ Adds non-obvious Jest behavior and tuning surface area
  • ➖ Doesn't guarantee deepRecursive gets a clean heap if recycling timing changes
3. Continue sharding the suite (e.g., 5-way)
  • ➕ Reduces per-process memory accumulation by spreading tests out
  • ➕ Can reduce wall-clock time if shards run in parallel
  • ➖ More CI complexity and orchestration cost
  • ➖ Treats symptoms broadly when only one test is the main driver

Recommendation: Prefer the PR’s approach: run the known high-memory deepRecursive test in a dedicated Jest process and run the rest separately. It directly targets the root cause (heap accumulation across files under --experimental-vm-modules) while preserving Node’s default heap budget and removing prior, broader workarounds (sharding and worker recycling) that add CI/config complexity.

Grey Divider

File Changes

Tests (1)
package.json Split deps-installer .test into .test:heavy and .test:rest +3/-2

Split deps-installer .test into .test:heavy and .test:rest

• Replaces the 5-way sharded .test command with two sequential Jest invocations: a heavy run for deepRecursive.ts and a rest run excluding deepRecursive via a regex. Keeps the same NODE_OPTIONS and registry-mock environment setup.

installing/deps-installer/package.json


Other (2)
index.ts Generate deps-installer heavy/rest Jest scripts instead of sharded runs +13/-3

Generate deps-installer heavy/rest Jest scripts instead of sharded runs

• Replaces the generated deps-installer test script sharding with two scripts: one dedicated run for test/install/deepRecursive.ts and one run for all remaining tests using a negative-lookahead path pattern. Adds in-code rationale documenting the CI heap behavior under --experimental-vm-modules.

.meta-updater/src/index.ts


jest-preset.js Remove workerIdleMemoryLimit from with-registry Jest preset +0/-9

Remove workerIdleMemoryLimit from with-registry Jest preset

• Deletes the workerIdleMemoryLimit setting (and its explanatory comment) that previously forced recycling workers to reclaim leaked VM module registry memory. With deepRecursive isolated into its own process, this workaround is no longer needed for the remaining tests.

utils/jest-config/with-registry/jest-preset.js


Grey Divider

Qodo Logo

Comment on lines 7 to 12
// Unfortunately, this means that if two such tests will run at the same time,
// they may break each other.
maxWorkers: 1,
// Recycle the test worker once its heap crosses this limit. Under
// `--experimental-vm-modules` Jest's VM module registry is never released
// between test files, so a long-running process climbs to Node's ~4 GB
// old-space ceiling and dies with an out-of-memory FATAL ERROR. Setting a
// limit is also what keeps `maxWorkers: 1` from collapsing to in-band
// execution (see `shouldRunInBand`): a recyclable worker is spawned instead,
// still serial, so the leaked memory is reclaimed between files without
// reintroducing the dist-tag races `maxWorkers: 1` prevents.
workerIdleMemoryLimit: '1500MB',
// Force Jest to exit after globalTeardown completes. The Verdaccio server
// and lifecycle child-processes spawned during tests may leave ref'd handles
// that prevent the process from exiting on its own.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

1. Worker recycling removed 🐞 Bug ☼ Reliability

@pnpm/jest-config/with-registry no longer configures workerIdleMemoryLimit, so with `maxWorkers:
1 and --experimental-vm-modules` the same long-lived Jest process retains memory across many test
files and can hit Node’s heap limit. This affects every package using the with-registry preset,
not just @pnpm/installing.deps-installer.
Agent Prompt
### Issue description
`__utils__/jest-config/with-registry/jest-preset.js` removed `workerIdleMemoryLimit`, which was the only built-in mechanism to recycle the Jest worker process and reclaim heap between test files when running under `--experimental-vm-modules`.

### Issue Context
- The repo’s test scripts commonly run Jest with `--experimental-vm-modules`.
- The PR itself documents that under this mode, Jest’s VM module registry is not reclaimed between files.
- The `with-registry` preset is used across many packages, so this is a cross-suite reliability regression.

### Fix Focus Areas
- __utils__/jest-config/with-registry/jest-preset.js[4-16]
- .meta-updater/src/index.ts[368-386]

### Implementation notes
- Re-add `workerIdleMemoryLimit` to the `with-registry` preset (potentially keeping the previous value) so long suites don’t accumulate unreclaimed VM-module memory across files.
- If the intent is to avoid recycling for specific suites, consider making it configurable via env var (default on), rather than removing it globally.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant