From a32d282b984d5eacbc3639131d926c528060630c Mon Sep 17 00:00:00 2001 From: davidmonterocrespo24 Date: Tue, 19 May 2026 17:18:05 +0200 Subject: [PATCH] fix(ci): shard frontend tests across two matrix legs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The execArgv fix (PR #200) made vitest actually honor the 8 GB heap cap — and the next CI run promptly proved that 8 GB is still not enough. Log: every test passes but the worker hits "Ineffective mark-compacts near heap limit" at exactly 8011 MB, the new ceiling. Doubling again to 16 GB would be near the GitHub-runner total RAM (16 GB) and start swapping. The real culprit is per-file leak accumulation: 117 test files share one vitest fork; each file lazy-loads ngspice WASM (~24 MB), wires up MixedModeScheduler / zustand singletons, and leaves some of that state alive in module-level closures even after the file finishes. Sum over the suite ≈ 8 GB+ retained. Split the run with vitest's built-in `--shard N/M`: - matrix.shard: [1, 2] alongside matrix.node-version: [20, 22] = 4 parallel runners - each runner executes `npx vitest run --shard ${shard}/2` - vitest hashes file paths into deterministic shards (same flaky file always lands in the same shard) - each runner only carries ~60 files of leak state → fits in the existing 8 GB cap from poolOptions.forks.execArgv Coverage upload gated to shard 1 / node 22 to avoid the two shards racing to overwrite the same artifact name. Coverage itself runs once on the full suite (best-effort, may OOM, but `continue-on-error: true` keeps it non-blocking). The real fix is dispose hooks on the leaking singletons, but that's a multi-PR cleanup of code paths I haven't touched in this work item; sharding unblocks CI in the meantime. --- .github/workflows/frontend-tests.yml | 38 ++++++++++++++++------------ 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/.github/workflows/frontend-tests.yml b/.github/workflows/frontend-tests.yml index 0443e725..ff481c6d 100644 --- a/.github/workflows/frontend-tests.yml +++ b/.github/workflows/frontend-tests.yml @@ -13,6 +13,16 @@ jobs: fail-fast: false matrix: node-version: ['20', '22'] + # Shard the 117 test files across two parallel runners per node + # version. The full suite leaks memory across files within one + # vitest fork — ngspice WASM modules, MixedModeScheduler + # singletons, zustand stores — and the cumulative heap exceeds + # even 8 GB. Sharding gives each runner a fresh Node process + # with only ~60 files of state to carry, well under budget. + # Splits are deterministic across runs (vitest hashes file + # paths into shards), so a flaky file lands in the same shard + # each time. + shard: [1, 2] steps: - name: Checkout repository @@ -65,18 +75,13 @@ jobs: run: cd frontend && npm run tsc continue-on-error: true # tsc -b has pre-existing strict errors in unrelated test files; tracking separately - - name: Run tests - run: cd frontend && npm test - env: - # Vitest 4 forks pool keeps state alive across the 117 test - # files in one worker process, and several of those tests - # load the ngspice emscripten module (~30 MB each), the - # MixedModeScheduler singleton, and other lazy modules. - # Cumulative heap pressure exceeds Node's 4 GB default by - # the end of the suite and the OOM kills the worker AFTER - # all 1881 tests pass. Bump to 8 GB until the test files - # are sharded or singletons get proper dispose hooks. - NODE_OPTIONS: --max-old-space-size=8192 + - name: Run tests (shard ${{ matrix.shard }}/2) + run: cd frontend && npx vitest run --shard ${{ matrix.shard }}/2 + # Heap is also raised inside the vitest config via + # poolOptions.forks.execArgv — forks pool ignores + # NODE_OPTIONS. With sharding each runner only carries + # ~60 test files' worth of leaked WASM/singleton state + # so the heap stays well under the ceiling. # Production build smoke — catches Vite/Rollup-only failures that # vitest doesn't see (chunk wiring, dynamic imports, manualChunks @@ -93,15 +98,16 @@ jobs: run: cd frontend && npm run build:docker # Upload coverage as an artifact for download / inspection. Skip - # codecov for now (no org account). Run only on Node 22 to keep the - # artifact list deduped. + # codecov for now (no org account). Run only on Node 22 + + # shard 1 to keep the artifact list deduped and avoid two + # matrix legs racing to write the same artifact name. - name: Coverage report - if: matrix.node-version == '22' + if: matrix.node-version == '22' && matrix.shard == 1 run: cd frontend && npm run test:coverage continue-on-error: true - name: Upload coverage artifact - if: matrix.node-version == '22' && always() + if: matrix.node-version == '22' && matrix.shard == 1 && always() uses: actions/upload-artifact@v4 with: name: coverage-lcov