perf: scale reusable pool workers adaptively#10749
Conversation
✅ Deploy Preview for vitest-dev ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
AriPerkkio
left a comment
There was a problem hiding this comment.
All this calculation seems complex. What does it do in practice? I didn't understand most of this PR based on the generated description.
| // the next worker can start while this one runs its task | ||
| void this.schedule() |
| private _startingCount = 0 | ||
| // refined with the measured duration after every worker start | ||
| private _spawnCost = 150 | ||
| // EMA of how long a single task takes; undefined until the first signal, |
There was a problem hiding this comment.
The browser side PR was partly reverted #10753. How heuristics can fail is pictured in https://artifacts.hiro18181.workers.dev/vitest-pr-10726-adaptive-sessions#failure-mode
This PR changes on node pool has slightly different code flow, but it should have effectively same characteristics where once it caps spawning new worker, it cannot grow back. This is because, while this PR changed so that one schedule can call two schedule, this "schedule ramp up chance" is given only to new worker (first we have 4 chances), so once each "chance" misses scaling opportunity due to small fileCost for earlier tests, it doesn't grow back even when later fileCost turn out to be large.
We should craft a similar adversarial examples like #10753 to verify.
Description
Starts pool workers adaptively instead of one per queued task up front. For reusable workers (
isolate: false, and vm pools' shared workers) the upfront strategy paysmaxWorkers × (spawn + runtime bring-up + environment setup)even when a couple of reused workers would drain the queue sooner. Follows the same idea as the browser pool's adaptive sessions (#10726), which @hi-ogawa pointed out is a universal scheduling optimization that should transfer to the process pools.min(maxWorkers, 4)workers start in parallel, exactly like before — any queue that reaches this point justifies them, and it keeps small machines (CI runners have 4 vcpus, somaxWorkersis 3) on the previous behavior byte for byte.maxWorkersin logarithmic time.Only tasks with
isolate: falseare gated. Isolated tasks pay a worker per task no matter what — there is no avoidable bring-up cost for the scaling to save — so they keep the exact previous behavior.Benchmarks
macOS (M4, 10 cores →
maxWorkers9), 40/160-file fixtures, jsdom environment unless noted, interleaved A/B (median of 3, same build):isolate:false, 40 filesisolate:false, 40 filesisolate:false, node env, 40 filesisolate:false, 160 files, 1022 modulesisolate:true, vmThreadsCI-sized runners (4 vcpus →
maxWorkers3, same-runner interleaved A/B, node env, median of 5):isolate:false, 40 filesisolate:true, 40 filesisolate:false, 160 filesisolate:false, 40 filesThe win scales with core count and per-worker bring-up cost (environment setup dominates it for jsdom), so the beneficiaries are many-core machines; small runners stay on the previous behavior by construction.