Skip to content

perf(core): keep the worker pool busy while collecting coverage#1348

Draft
fi3ework wants to merge 2 commits into
mainfrom
perf/coverage-pool-utilization
Draft

perf(core): keep the worker pool busy while collecting coverage#1348
fi3ework wants to merge 2 commits into
mainfrom
perf/coverage-pool-utilization

Conversation

@fi3ework

Copy link
Copy Markdown
Member

Background

When coverage is enabled (the default forks pool), the worker pool doesn't stay fully busy. On CI, CPU usage plateaus well below the available cores even with many test files still waiting to run — the same suite without coverage uses all cores. Reported in #1326.

Why it happens

Each test file runs in its own worker and sends its coverage back to the main process when it finishes. The main process runs on a single event loop, and that one loop does two jobs at the same time: decoding and merging incoming coverage, and handing the next test file to each free worker.

As coverage piles up, decoding it crowds out the dispatch work — so workers that just finished sit idle waiting for their next file. The more files there are (and the larger the coverage), the worse the stall. That's why it shows up specifically when coverage is on, and only at scale.

The fix

Take coverage off the main process's critical path:

  • Each worker writes its coverage to a temporary file and sends back only the path. During the run the main process no longer stops to decode coverage, so it can keep every worker fed.
  • Once the run finishes, those files are read and merged in parallel across a small pool of background threads, then combined into the final report.

The coverage result is exactly the same as before — only when and where it gets computed changes. On a 500-file project this brought CPU utilization back from ~56% to ~80% and cut wall-clock time by ~30%.

Diagnostics

Running with DEBUG=rstest now prints the run's environment (cores, load, pool and coverage settings) plus event-loop and coverage-ingest timings, so a utilization problem can be pinned down from a single run.

Closes #1326

When coverage is enabled, the host process decoded and merged every test file's coverage on its single event loop during the run — the same loop that dispatches the next file to each free worker. Under load that decode work starved dispatch, leaving workers idle and CPU under-utilized (worse with more files / larger coverage).

Workers now write coverage to a temp file and hand back only the path, so the host never decodes coverage mid-run. After the run, the files are read and merged in a worker_threads pool in parallel, then combined. Coverage output is byte-identical; on a 500-file project utilization rose ~56%→~80% and wall time dropped ~30%.

Each provider (@rstest/coverage-istanbul, @rstest/coverage-v8) ships a self-contained merge worker that uses its own istanbul merge; the core pool orchestrates the threads and falls back to a host-side parse if needed. DEBUG=rstest prints pool env + event-loop/ingest diagnostics.

Closes #1326
@github-actions

Copy link
Copy Markdown

pkg.pr.new preview b3a5a97 · workflow run

  • pnpm add https://pkg.pr.new/@rstest/adapter-rsbuild@b3a5a97
  • pnpm add https://pkg.pr.new/@rstest/adapter-rslib@b3a5a97
  • pnpm add https://pkg.pr.new/@rstest/adapter-rspack@b3a5a97
  • pnpm add https://pkg.pr.new/@rstest/browser@b3a5a97
  • pnpm add https://pkg.pr.new/@rstest/browser-react@b3a5a97
  • pnpm add https://pkg.pr.new/@rstest/core@b3a5a97
  • pnpm add https://pkg.pr.new/@rstest/coverage-istanbul@b3a5a97
  • pnpm add https://pkg.pr.new/@rstest/coverage-v8@b3a5a97

@github-actions

Copy link
Copy Markdown

Rsdoctor Bundle Diff Analysis

Found 13 projects in monorepo, 3 projects with changes.

📊 Quick Summary
Project Total Size Change
adapter-rsbuild 3.7 KB 0
adapter-rslib 24.7 KB 0
adapter-rspack 7.8 KB 0
browser 2.0 MB 0
browser-react 3.7 KB 0
browser-ui 810.0 KB 0
coverage-istanbul 14.7 KB 📈 +190.0 B (+1.3%)
coverage-v8 16.6 KB 📈 +220.0 B (+1.3%)
core/browser 977.5 KB 0
core/loaders 869.0 B 0
core/main 1.7 MB +4.6 KB (0.3%)
vscode/extension 27.0 MB 0
vscode/worker 14.5 KB 0
📋 Detailed Reports (Click to expand)

📁 coverage-istanbul

Path: packages/coverage-istanbul/.rsdoctor/rsdoctor-data.json

📌 Baseline Commit: f8fa2d7b65 | PR: #1343

Metric Current Baseline Change
📊 Total Size 14.7 KB 14.5 KB +190.0 B (+1.3%)
📄 JavaScript 14.7 KB 14.5 KB +190.0 B (+1.3%)
🎨 CSS 0 B 0 B 0
🌐 HTML 0 B 0 B 0
📁 Other Assets 0 B 0 B 0

📦 Download Diff Report: coverage-istanbul Bundle Diff

📁 coverage-v8

Path: packages/coverage-v8/.rsdoctor/rsdoctor-data.json

📌 Baseline Commit: f8fa2d7b65 | PR: #1343

Metric Current Baseline Change
📊 Total Size 16.6 KB 16.3 KB +220.0 B (+1.3%)
📄 JavaScript 16.6 KB 16.3 KB +220.0 B (+1.3%)
🎨 CSS 0 B 0 B 0
🌐 HTML 0 B 0 B 0
📁 Other Assets 0 B 0 B 0

📦 Download Diff Report: coverage-v8 Bundle Diff

📁 core/main

Path: packages/core/.rsdoctor/main/rsdoctor-data.json

📌 Baseline Commit: f8fa2d7b65 | PR: #1343

Metric Current Baseline Change
📊 Total Size 1.7 MB 1.7 MB +4.6 KB (0.3%)
📄 JavaScript 1.6 MB 1.6 MB +4.6 KB (0.3%)
🎨 CSS 0 B 0 B 0
🌐 HTML 0 B 0 B 0
📁 Other Assets 81.6 KB 81.6 KB 0

📦 Download Diff Report: core/main Bundle Diff

Generated by Rsdoctor GitHub Action

@fi3ework fi3ework marked this pull request as draft May 29, 2026 16:34
The end-of-run fan-out (b3a5a97) materializes the full un-deduped coverage
corpus on tmpdir for the whole run, then amplifies it ~Nx across merge
threads at the very end. On a RAM-backed /tmp that peak competes with the
V8 heap and swap-thrashes into a hang; at large per-file coverage it
OOM-crashes (SIGABRT) even on a healthy box.

Replace the batch fan-out with a single long-lived merge worker_thread that
consumes each per-file coverage path as it arrives, merges it incrementally,
and unlinks the temp file immediately — so the on-disk corpus stays bounded
and only one deduped map is ever resident (no Nx amplification). On istanbul
this also keeps the host event loop free (eld p99 roughly halved) and removes
the end-of-run merge tail.

Gated on a provider capability flag (`coverageMergeWorkerStreaming`) so the
batch-only v8 worker is never mis-driven into the streaming protocol (which
would silently drop coverage); `RSTEST_COV_INGEST=batch` forces the
end-of-run fan-out, `=stream` is explicit opt-in.

Refs #1326
@github-actions

github-actions Bot commented Jun 1, 2026

Copy link
Copy Markdown

pkg.pr.new preview c33e508 · workflow run

  • pnpm add https://pkg.pr.new/@rstest/adapter-rsbuild@c33e508
  • pnpm add https://pkg.pr.new/@rstest/adapter-rslib@c33e508
  • pnpm add https://pkg.pr.new/@rstest/adapter-rspack@c33e508
  • pnpm add https://pkg.pr.new/@rstest/browser@c33e508
  • pnpm add https://pkg.pr.new/@rstest/browser-react@c33e508
  • pnpm add https://pkg.pr.new/@rstest/core@c33e508
  • pnpm add https://pkg.pr.new/@rstest/coverage-istanbul@c33e508
  • pnpm add https://pkg.pr.new/@rstest/coverage-v8@c33e508

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.

[Bug]: maybe workers pool's underutilized?

1 participant