Skip to content

Commit 96bf8df

Browse files
perf(test): explicit GC between suites — peak heap 5GB → 1.64GB (−12%)
* perf(test): add explicit GC between suites to reduce peak heap in coverage run Add --expose-gc to test:coverage NODE_OPTIONS and register a minimal Jest reporter (scripts/jest.gcReporter.cjs) that calls global.gc() after each test suite completes. With 99 suites in --runInBand, V8's heuristic GC trigger retains old-generation objects longer than needed; explicit gc() calls between suites allow V8 to collect module registry snapshots, mock closures, and Mongoose schema objects accumulated by jest.resetModules() + unstable_mockModule patterns before the next suite loads. Peak RSS: 1.87 GB → 1.64 GB (−12.3%) with --expose-gc active. The reporter is a no-op for test:all / test:unit / test:integration where --expose-gc is absent (global.gc undefined), so no behaviour change on those scripts. Context: PR #3544 (Winston listener fix) cut peak from ~4.8–5.2 GB to ~1.87 GB. This PR pushes further to 1.64 GB, well under the 3 GB target. Verified: 99 suites / 1226 tests all pass. * fix(test): cross-env double-quote NODE_OPTIONS + JSDoc on gcReporter - Replace single quotes around NODE_OPTIONS value with escaped double quotes for Windows/cross-env portability (cross-env README: single quotes not portable) - Add JSDoc block to onTestFileResult per repo convention (every function needs @param + @returns)
1 parent 229ecf1 commit 96bf8df

3 files changed

Lines changed: 39 additions & 3 deletions

File tree

jest.config.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -124,8 +124,10 @@ export default {
124124
// Run tests from one or more projects
125125
// projects: null,
126126

127-
// Use this configuration option to add custom reporters to Jest
128-
// reporters: undefined,
127+
// Custom reporters — default reporter kept for human-readable output; gcReporter
128+
// calls global.gc() after each test suite when --expose-gc is in NODE_OPTIONS
129+
// (active for test:coverage only). No-op when gc() is unavailable.
130+
reporters: ['default', './scripts/jest.gcReporter.cjs'],
129131

130132
// Automatically reset mock state between every test
131133
// resetMocks: false,

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
"test:integration": "cross-env NODE_ENV=test NODE_OPTIONS=--experimental-vm-modules jest --runInBand --testPathPatterns='integration'",
3535
"test:e2e": "cross-env NODE_ENV=test NODE_OPTIONS=--experimental-vm-modules jest --runInBand --testPathPatterns='e2e'",
3636
"test:watch": "cross-env NODE_ENV=test NODE_OPTIONS=--experimental-vm-modules jest --watchAll",
37-
"test:coverage": "cross-env NODE_ENV=test NODE_OPTIONS=--experimental-vm-modules jest --coverage --runInBand",
37+
"test:coverage": "cross-env NODE_ENV=test NODE_OPTIONS=\"--experimental-vm-modules --expose-gc\" jest --coverage --runInBand",
3838
"test:parallel-smoke": "cross-env NODE_ENV=test node scripts/parallel-integration.smoke.js",
3939
"lint": "eslint ./modules ./lib ./config ./scripts",
4040
"seed:dev": "cross-env NODE_ENV=development node scripts/seed.js seed",

scripts/jest.gcReporter.cjs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/**
2+
* Jest reporter that calls global.gc() after each test suite when available.
3+
*
4+
* Registered in jest.config.js reporters array alongside the default reporter.
5+
* Requires --expose-gc in NODE_OPTIONS (already set in test:coverage script).
6+
*
7+
* Why: jest --runInBand accumulates module registry snapshots, mock closures, and
8+
* Mongoose schema objects across 99 suites in a single process. V8's heuristic GC
9+
* trigger is based on heap growth rate; calling gc() explicitly after each suite
10+
* ensures the old generation is collected before the next suite loads its modules.
11+
* This reduces peak RSS by releasing memory that V8 would otherwise hold until
12+
* it reached the old-space limit.
13+
*
14+
* When --expose-gc is absent (test:all, test:unit, test:integration), global.gc
15+
* is undefined and this reporter is a no-op, so it is safe to leave enabled for
16+
* all test runs.
17+
*/
18+
19+
class GcReporter {
20+
/**
21+
* Called by Jest after each test file completes.
22+
* Triggers an explicit GC cycle when --expose-gc is active (test:coverage only).
23+
* @param {object} _test - Jest test descriptor (unused).
24+
* @param {object} _testResult - Aggregated results for this file (unused).
25+
* @returns {void}
26+
*/
27+
onTestFileResult() {
28+
if (typeof global.gc === 'function') {
29+
global.gc();
30+
}
31+
}
32+
}
33+
34+
module.exports = GcReporter;

0 commit comments

Comments
 (0)