feat(pool): add pool.memoryLimit to recycle workers by RSS#1291
Draft
pkasarda wants to merge 1 commit into
Draft
feat(pool): add pool.memoryLimit to recycle workers by RSS#1291pkasarda wants to merge 1 commit into
pkasarda wants to merge 1 commit into
Conversation
Reused workers (`isolate: false`) accumulate heap monotonically across
test files — vendor module caches, JSDOM nodes, React fiber trees,
accumulated closures. On long suites this drifts into multi-GB
GC-thrash territory and individual tests start running 2-3× slower
than they would in a fresh worker.
This adds `pool.memoryLimit` (mirrors Vitest's `VmOptions.memoryLimit`)
so users can bound the pressure without giving up worker reuse:
```ts
defineConfig({
isolate: false,
pool: { memoryLimit: '1GB' },
});
```
Mechanism (zero protocol changes — the worker already reports
`memoryUsage().rss` after each task for the existing `MemoryGate` spawn
gate):
- `PoolRunner` captures `response.memory.rss` from each `runFinished`
/ `collectFinished` message.
- `isUsable()` returns `false` when the last sample exceeds the parsed
byte cap. The pool's existing release path then disposes the worker
instead of returning it to the idle pool, and the next dispatch
spawns a fresh one.
- `isolate: true` opts out of the check (workers are single-use, so
the cap is dead code there).
API:
- `pool.memoryLimit?: number | string` — matches Vitest's accepted
forms so the contract is familiar:
- `number >= 1` → bytes
- `number in (0, 1]` → fraction of total system memory
- `string` with a unit suffix → `"512MB"`, `"1.5GB"`, `"20%"`,
`"1GiB"`, … (case-insensitive). Unknown units, unparseable
strings, or non-positive numbers disable the cap rather than
erroring out — keeps CI green when a config option is misspelled.
Files:
- `packages/core/src/pool/parseMemoryLimit.ts` — new, parses the
user-facing union to bytes
- `packages/core/src/pool/poolRunner.ts` — `lastReportedRssBytes`,
`memoryLimitBytes` opt, `isUsable()` cap check
- `packages/core/src/pool/pool.ts` — wires the bytes through to
`PoolRunner` only when `isolate: false`
- `packages/core/src/pool/index.ts` — calls `parseMemoryLimit` once
at pool construction
- `packages/core/src/pool/types.ts` — `PoolOptions.memoryLimitBytes`
- `packages/core/src/types/config.ts` — `RstestPoolOptions.memoryLimit`
- `packages/core/tests/pool/parseMemoryLimit.test.ts` — 31 cases
covering byte/fraction/unit/percent/whitespace/invalid inputs
- `e2e/memory-limit/` — fixture with `memoryLimit: 2` (2 bytes) +
`maxWorkers: 1` + 4 files; driver asserts unique `process.pid` per
file, proving the cap recycles end-to-end
This was referenced May 20, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Draft — opening for visibility, no CC, no reviewers.
What
Adds
pool.memoryLimitto recycle a reused worker once its RSS exceeds a configurable threshold. Mirrors Vitest'sVmOptions.memoryLimitso the API shape is familiar.Accepted values:
number >= 1— bytesnumberin(0, 1]— fraction of total system memorystringwith a unit suffix —"512MB","1.5GB","20%","1GiB", … (case-insensitive)Unknown units / unparseable strings / non-positive numbers disable the cap rather than erroring out, so a misspelled config option doesn't tank CI.
Why
isolate: falsereuses worker processes across files for speed, but vendor module caches, JSDOM nodes, React fiber trees, and accumulated closures grow the heap monotonically. On a long jsdom-heavy suite we measured a single worker climbing from 71 MB at file 1 to 4 GB at file 65, with per-test wall time degrading 2-3× under GC pressure. Without an upper bound, users either give upisolate: falseentirely or hit OOM.Mechanism (zero protocol changes)
The worker already reports
process.memoryUsage().rssafter each task — the existingMemoryGatespawn gate uses it to defer new spawns under memory pressure. This PR consumes the same signal:PoolRunnercapturesresponse.memory.rssfrom eachrunFinished/collectFinishedmessage.isUsable()returnsfalsewhen the last sample exceeds the parsed byte cap.isolate: trueopts out — workers are single-use, so the cap is dead code there.What doesn't change
isolate: truesemantics — unchanged.isolate: falsewithoutmemoryLimit— unchanged (unbounded reuse).Files
packages/core/src/pool/parseMemoryLimit.ts— new, parses the user-facing union to bytespackages/core/src/pool/poolRunner.ts—lastReportedRssBytes,memoryLimitBytesopt,isUsable()cap checkpackages/core/src/pool/pool.ts— wires bytes through toPoolRunneronly whenisolate: falsepackages/core/src/pool/index.ts— callsparseMemoryLimitonce at pool constructionpackages/core/src/pool/types.ts—PoolOptions.memoryLimitBytespackages/core/src/types/config.ts—RstestPoolOptions.memoryLimitpackages/core/tests/pool/parseMemoryLimit.test.ts— 31 cases covering byte / fraction / unit / percent / whitespace / invalid inputse2e/memory-limit/— fixture withmemoryLimit: 2(2 bytes) +maxWorkers: 1+ 4 files. Driver asserts uniqueprocess.pidper file, proving the cap recycles end-to-end.Testing
pnpm --filter @rstest/core test— 273/273 (was 242 — +31 new parseMemoryLimit cases)e2e/memory-limit— 1/1e2e— 628/630 (2 pre-existing browser-mode flakes unrelated)Status
Draft. Opening for visibility — not requesting review yet. No CC. Will mark ready once we've validated on a workspace running under
isolate: falseat scale.