Browser Instances as Workers
How vitest-dev/vitest#11525 changes the browser pool, what the review added on top of it, and how much each change matters. The pull request resolves #9696: with 20 browser projects, CI ran out of memory because every project opened its own browser and its own set of pages at the same time.
Before and after
On main the browser pool is the only pool that works per project: each browser project gets its own BrowserPool, and all of them start at once.
The pull request replaces them with one scheduler. Every browser instance is a worker, at most maxWorkers of them are open, and the same number is a budget of pages shared by the open instances.
Life of one instance in the new pool
The top lane is the scheduler and the worker in packages/vitest/src/node/pools/browser.ts. The bottom lane is the Playwright provider.
Highlighted parts were added by the review on top of the pull request: 4 defects reproduced with probes, 4 more confirmed by subagents.
A worker is evicted only when it is idle and another task needs its slot. On shutdown the pool closes every provider without the keep-warm option, so the browser process is closed under the existing 10 s bound.
Architectural changes and how much they matter
Importance reflects what breaks or what a user pays without the change. Critical changes fix hangs or the problem the pull request exists for. High changes affect memory, correctness of results, or public behaviour. Medium and low changes improve performance, robustness, or hygiene.
| Change | Why it matters | Origin | Importance |
|---|---|---|---|
One scheduler: a queue of per-project tasks, at most maxWorkers instances open, LRU eviction of idle instances |
Resolves #9696. Browser count no longer grows with the project count: 16 browsers became 9, peak memory 18.1 GB became 5.4 GB, wall time 15.8 s became 11.3 s. | bbff01106 | critical |
Forget pages that were closed or crashed (pruneClosedSessions, opening sessions tracked as a set) |
Without it a watch rerun after a page disconnect opened no page and never settled, and vitest.close() hung on the running promise. Reproduced with a probe. |
30e2694cc | critical |
Page budget shared by the open instances (getPageAllowance, grow() when an instance finishes) |
Pages stay close to the core count instead of projects × maxWorkers: 96 renderer processes became 16. maxWorkers now means concurrent files, like in the node pools. |
bbff01106 | high |
One maxWorkers knob passed from the task group in pool.ts |
Per-project maxWorkers: 1 or fileParallelism: false now serialises instances too. On a 2-core CI runner the default is 1 worker, so 3 instances run one after another. This is a behaviour change worth a release note. |
bbff01106 | high |
Provider lifecycle: closeBrowserProvider() detaches the provider, the worker re-inits it on the next run, the RPC upgrade terminates sockets of a closed instance |
Eviction is only possible when an instance can close and reopen cleanly. Fixes the CI hang where a reopened instance read an unset provider. | 1ebbca8f7 | high |
The run settles only after every instance settled (allSettled, per-page reject, settle()) |
Before, one failing instance ended the run while the others still ran: close() killed their files, results leaked into the next watch run, and a second error was lost. |
0e666c197 | high |
Keep the browser warm only on eviction (close({ keepWarm })); shutdown closes it under the 10 s bound |
The pull request routed the final close through the warm pool, so the real browser.close() ran in an onClose listener without a timeout. A frozen browser would hang shutdown again, the problem #10791 fixed. |
30e2694cc | high |
Decide the parallel flag once per instance (maxPages > 1) |
With the budget an instance can start with 1 page and grow later. The provider launched a persistent context for the first page and then shared it with the added pages, without a warning. Reproduced: 3 pages in one persistent context. | 30e2694cc | high |
Browser recycling: an evicted instance registers its browser as warm, the next task adopts it (keepWarmBrowser, takeAnyWarmBrowser) |
Eviction reuses the browser process instead of launching one, and prewarmed browsers are adopted by whichever project is scheduled first. | bbff01106 | medium |
Prewarm capped at maxWorkers launches (_prewarmedBrowsers, reset on restart) |
Without the cap 16 browsers launched at startup and 7 of them sat idle until their project got a turn. | bbff01106 | medium |
Idle-worker-first scheduling (nextTaskIndex) |
With 3 instances and 2 slots every rerun closed and reopened all 3 instances. Now 1 reopens: rerun 312 ms became 177 ms. | 0e666c197 | medium |
| Exclusive instances (no per-session mocker or no parallelism) run alone and last | Keeps the webdriverio and preview providers correct: they share one mock registry and drive one page. | bbff01106 | medium |
ProjectBrowser.provider is typed BrowserProvider | undefined, 8 call sites guarded |
The provider is unset after an eviction until the next run. A reporter reading provider.name in onTestRunEnd failed the run with a TypeError. Public type change, needs a release note. |
0e666c197 | medium |
_throwIfClosing disposes only its own page instead of clearing the shared maps |
A page still opening during an eviction could clear the context map before close() read it, and the recycled browser kept the contexts. Confirmed in the provider: 15 of 25 runs leaked with 4 open pages. |
0e666c197 | medium |
| Session ids and exit promises of closed instances are released | Both grew without bound in watch mode: 3, 6, 9 session ids over 3 runs. | 30e2694cc | low |
headless: false and preview instances are evicted like headless ones, so their visible window or tab is closed and reopened on every rerun when there are more instances than workers, and standalone() opens all pages up front. A proposal to never evict headed instances exists and waits for a decision.
Benchmark
Measured after all applied fixes against origin/main. Every run passed all 384 tests.
| Scenario | Wall time main / branch | Peak memory main / branch | Browsers | Renderers | CPU time |
|---|---|---|---|---|---|
16 projects, default maxWorkers |
15.8 s / 11.3 s | 18.1 GB / 5.4 GB | 16 / 9 | 96 / 16 | 60.9 s / 36.2 s |
16 projects, --maxWorkers=2 |
11.7 s / 19.6 s | 9.5 GB / 1.4 GB | 16 / 2 | 32 / 2 | 41.2 s / 28.5 s |
| 1 project, 96 files | 6.7 s / 6.8 s | 2.7 GB / 2.7 GB | 1 / 1 | 9 / 9 | 24.8 s / 25.0 s |
The --maxWorkers=2 run is slower because 2 pages run instead of 32. The single-project numbers are within noise.