Skip to content

Commit 4e12731

Browse files
cpojerfacebook-github-bot-3
authored andcommitted
Fix memory leak around global.process
Summary: This memory leak was discovered in #584 and has always been present in jest. It likely affects the runtime performance of jest on low-memory machines *a lot* and we don't notice it much on devservers. I will tag and release 0.7.0 with this fix (because 0.7.0 also removes a bunch of APIs, see the changelog, it cannot be a patch release). Two samples from before and after: * P20276376 (150 mb vs 47 mb on 34 Relay tests) * P20276601 (54 mb vs 38 mb flat on the functional tests) I also added a way to pretty print the output (see the second paste). By adding `--logHeapUsage` to jest it adds these statistics after every test run. When node's global `gc` function is exposed, it calls it beforehand to make sure we are logging accurate data. I am going to think about whether it makes sense to expose this more easily (adding `--runInBand` etc. is slightly confusing) if I am confident it isn't bad for performance. On automated test runs you might want to log this and be able to log the final memory usage to whatever logging system you use to be able to track memory usage of test runs (and applications) over time. Note: It is time we rewrite jest's configs *completely*. I have plans for this and it is a discussion for a separate diff. For now I have to copy-pasta some code to make everything work in the internal and external runners and tack on the argv data to the config objects. public Reviewed By: DmitrySoshnikov Differential Revision: D2608363 fb-gh-sync-id: ab3bda143c22ef06e2e1b32a4c92d8ad0a2151cb
1 parent 615789a commit 4e12731

File tree

7 files changed

+65
-27
lines changed

7 files changed

+65
-27
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
## 0.7.0
22

3+
* Fixed a memory leak with test contexts. Jest now properly cleans up
4+
test environments after each test. Added `--logHeapUsage` to log memory
5+
usage after each test. Note: this is option is meant for debugging memory
6+
leaks and might significantly slow down your test run.
37
* Removed `mock-modules`, `node-haste` and `mocks` virtual modules. This is a
48
breaking change of undocumented public API. Usage of this API can safely be
59
automatically updated through an automated codemod:

bin/jest.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,13 @@ const argv = optimist
165165
),
166166
type: 'boolean',
167167
},
168+
logHeapUsage: {
169+
description: _wrapDesc(
170+
'Logs the heap usage after every test. Useful to debug memory ' +
171+
'leaks. Use together with `--runInBand` and `--expose-gc` in node.'
172+
),
173+
type: 'boolean',
174+
},
168175
})
169176
.check(function(argv) {
170177
if (argv.runInBand && argv.hasOwnProperty('maxWorkers')) {

src/DefaultTestReporter.js

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -49,16 +49,25 @@ class DefaultTestReporter {
4949
? (testResult.perfStats.end - testResult.perfStats.start) / 1000
5050
: null;
5151

52-
let testRunTimeString;
52+
let testDetail = [];
5353
if (testRunTime !== null) {
54-
testRunTimeString = '(' + testRunTime + 's)';
55-
if (testRunTime > 2.5) {
56-
testRunTimeString = this._formatMsg(testRunTimeString, FAIL_COLOR);
57-
}
54+
testDetail.push(
55+
testRunTime > 2.5
56+
? this._formatMsg(testRunTime + 's', FAIL_COLOR)
57+
: testRunTime + 's'
58+
);
59+
}
60+
61+
if (testResult.memoryUsage) {
62+
const toMB = bytes => Math.floor(bytes / 1024 / 1024);
63+
testDetail.push(
64+
`${toMB(testResult.memoryUsage)} MB current`,
65+
`${toMB(testResult.maxMemoryUsage)} MB max`
66+
);
5867
}
5968

6069
const resultHeader = this._getResultHeader(allTestsPassed, pathStr, [
61-
testRunTimeString,
70+
(testDetail.length ? '(' + testDetail.join(', ') + ')' : null),
6271
]);
6372

6473
/*

src/JSDomEnvironment.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,9 @@ class JSDomEnvironment {
2626

2727
// Forward some APIs
2828
this.global.Buffer = Buffer;
29-
this.global.process = process;
29+
// `this.global.process` is mutated in FakeTimers. Make a copy of the
30+
// object for the jsdom environment to prevent memory leaks.
31+
this.global.process = Object.assign({}, process);
3032
this.global.setImmediate = setImmediate;
3133
this.global.clearImmediate = clearImmediate;
3234

src/TestRunner.js

Lines changed: 31 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -70,13 +70,14 @@ function optionPathToRegex(p) {
7070
* @param options See DEFAULT_OPTIONS for descriptions on the various options
7171
* and their defaults.
7272
*/
73-
7473
class TestRunner {
7574

7675
constructor(config, options) {
7776
this._config = config;
7877
this._configDeps = null;
7978
this._moduleLoaderResourceMap = null;
79+
// Maximum memory usage if `logHeapUsage` is enabled.
80+
this._maxMemoryUsage = 0;
8081
this._testPathDirsRegExp = new RegExp(
8182
config.testPathDirs
8283
.map(dir => optionPathToRegex(dir))
@@ -91,6 +92,7 @@ class TestRunner {
9192
.join('|') +
9293
')$'
9394
);
95+
9496
// Map from testFilePath -> time it takes to run the test. Used to
9597
// optimally schedule bigger test runs.
9698
this._testPerformanceCache = null;
@@ -375,22 +377,27 @@ class TestRunner {
375377

376378
const testExecStats = {start: Date.now()};
377379
return testRunner(config, env, moduleLoader, testFilePath)
378-
.then(results => {
380+
.then(result => {
379381
testExecStats.end = Date.now();
380382

381-
results.perfStats = testExecStats;
382-
results.testFilePath = testFilePath;
383-
results.coverage =
383+
result.perfStats = testExecStats;
384+
result.testFilePath = testFilePath;
385+
result.coverage =
384386
config.collectCoverage
385387
? moduleLoader.getAllCoverageInfo()
386388
: {};
387389

388-
return results;
390+
return result;
389391
});
390392
}).then(
391-
results => Promise.resolve().then(() => {
393+
result => Promise.resolve().then(() => {
392394
env.dispose();
393-
return results;
395+
396+
if (config.logHeapUsage) {
397+
this._addMemoryUsage(result);
398+
}
399+
400+
return result;
394401
}),
395402
err => Promise.resolve().then(() => {
396403
env.dispose();
@@ -483,12 +490,10 @@ class TestRunner {
483490
const TestReporter = require(config.testReporter);
484491
if (config.useStderr) {
485492
/* eslint-disable fb-www/object-create-only-one-param */
486-
reporter = new TestReporter(
487-
Object.create(
488-
process,
489-
{ stdout: { value: process.stderr } }
490-
)
491-
);
493+
reporter = new TestReporter(Object.create(
494+
process,
495+
{stdout: {value: process.stderr}}
496+
));
492497
/* eslint-enable fb-www/object-create-only-one-param */
493498
} else {
494499
reporter = new TestReporter();
@@ -563,19 +568,15 @@ class TestRunner {
563568
.then(this._cacheTestResults.bind(this));
564569
}
565570

566-
_createTestRun(
567-
testPaths, onTestResult, onRunFailure
568-
) {
571+
_createTestRun(testPaths, onTestResult, onRunFailure) {
569572
if (this._opts.runInBand || testPaths.length <= 1) {
570573
return this._createInBandTestRun(testPaths, onTestResult, onRunFailure);
571574
} else {
572575
return this._createParallelTestRun(testPaths, onTestResult, onRunFailure);
573576
}
574577
}
575578

576-
_createInBandTestRun(
577-
testPaths, onTestResult, onRunFailure
578-
) {
579+
_createInBandTestRun(testPaths, onTestResult, onRunFailure) {
579580
let testSequence = Promise.resolve();
580581
testPaths.forEach(testPath =>
581582
testSequence = testSequence
@@ -617,6 +618,16 @@ class TestRunner {
617618
})
618619
))).then(() => workerFarm.end(farm));
619620
}
621+
622+
_addMemoryUsage(result) {
623+
if (global.gc) {
624+
global.gc();
625+
}
626+
const memoryUsage = process.memoryUsage().heapUsed;
627+
this._maxMemoryUsage = Math.max(this._maxMemoryUsage, memoryUsage);
628+
result.maxMemoryUsage = this._maxMemoryUsage;
629+
result.memoryUsage = memoryUsage;
630+
}
620631
}
621632

622633
function _pathStreamToPromise(stream) {

src/jest.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,10 @@ function _promiseConfig(argv, packageRoot) {
109109
config.useStderr = true;
110110
}
111111

112+
if (argv.logHeapUsage) {
113+
config.logHeapUsage = argv.logHeapUsage;
114+
}
115+
112116
config.noStackTrace = argv.noStackTrace;
113117

114118
return config;

src/lib/utils.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,7 @@ function normalizeConfig(config) {
261261
case 'moduleFileExtensions':
262262
case 'noHighlight':
263263
case 'noStackTrace':
264+
case 'logHeapUsage':
264265
case 'cache':
265266
case 'verbose':
266267
value = config[key];

0 commit comments

Comments
 (0)