-
-
Notifications
You must be signed in to change notification settings - Fork 6.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Mitigate memory leaks in jest-environment-node #15215
base: main
Are you sure you want to change the base?
Conversation
✅ Deploy Preview for jestjs ready!Built without sensitive environment variables
To edit notification comments on pull requests, go to your Netlify site configuration. |
2c95e2d
to
7423314
Compare
…rties in teardown
…rce-map-support
Thank you so much for working on this! My main feedback would be to split up the changes here (already in atomic commits, which is nice!) into separate PRs so they can be merged independently of each other. Some quick feedback on each of these
On the const log4jsPath = require.resolve('log4js');
require(log4jsPath);
delete require.cache[log4jsPath];
require(log4jsPath);
delete require.cache[log4jsPath];
require(log4jsPath);
// etc. |
@SimenB Thanks for the reply!
Agreed. I wanted 1 PR mostly for the tests and the "dev journey". I'll keep the first method (global cleanup) here and move the others to separate PRs.
Waiting on your thoughts :) This is the bulk of the PR and the thing that reduces memory leak the most (by far).
Yep. I missed that. At first I tried to clean listeners directly on This code illustrates how the listeners are indeed shared: import vm from 'node:vm';
import cluster from 'node:cluster';
console.log(cluster.listenerCount('foo')); // 0
cluster.on('foo', () => {});
console.log(cluster.listenerCount('foo')); // 1
const context = vm.createContext();
vm.runInContext(`
const cluster = require('node:cluster');
cluster.removeAllListeners();
`, Object.assign(context, { require, console }));
console.log(cluster.listenerCount('foo')); // 0 I could potentially use the same technique I did with
Sure, I'll do it in a separate PR.
It only cleans custom handlers, but it doesn't clean |
7423314
to
a124e2e
Compare
…rce-map-support
…rce-map-support
…rties in teardown
a124e2e
to
05592c7
Compare
Mind opening a PR upstream with that? It was my PR some years ago that added the cleanup, seems I missed some 😀 |
…rce-map-support
Tried my best (without tests): |
any progress on this? does this also improves CPU ? |
Summary
This PR attempts to drastically mitigate memory leaks in
jest-environment-node
, raised by multiple issues:The leaks are likely caused by the Jest/NodeJS sandbox (vm context) not being fully cleaned up between test suites.
This PR introduces 3 orthogonal mitigations, all made during env teardown.
The mitigations do not eliminate the leak entirely, but they drastically reduce it (at least in my testing).
The mitigations (each in its own commit):
1. Global cleanup
In my testing, the biggest memory leak was caused by custom objects on
global
that keep references to source-code strings.This change deletes all properties of all the objects in the environment's
global
; i.e:The exceptions are:
jest-utils
(setNotShreddable
)jest-circus
's state is leaking, so it should be deleted; however, some of the properties are still used after teardown -- for instance,unhandledErrors
andunhandledRejectionErrorByPromise
.performance
property (hack)react-native
comes with a preset that overrides this property on env-setup and never reverts it. Then, react-test-renderer attempts to accessperformance
(via scheduler) after the env has already been torn down. A fix needs to be made toreact-native
to either (a) revert theperformance
property at teardown or (b) protect the customperformance
object with the new shredder API.The PR requires an update to the official docs, so users would know about this behavior, and how to avoid it if the need arises.
2. Event listeners cleanup
Another memory leak came from event listeners on the env
process
andnode:cluster
module, so these are removed at env teardown.3. source-map-support cleanup
Although some cleanup of
source-map-support
exists in Jest, it's not enough. This module overridesError.prepareStackTrace
with an implementation that holds a cache that contains (memory-heavy) source code.The mitigation here is to override
Error.prepareStackTrace
back to a no-op method at env teardown.Stacked of nested (
cause
) errors were harmed by this change, so a better assurance of stack generation was added tojest-runner
.Test plan
I have been using one of our private repositories that are suffering from heavy memory leaks.
All of them use some version of NodeJS (18.x), Jest (27/29) and Typescript (4.x). They also make use of
sequelize
,lodash
,moment
,log4js
, AWS SDK (v3) and many more packages.Method
runTest.ts
injest-runner
to run with the local jest environment (/path/to/jest/packages/jest-environment-node/build/index.js
) instead of the test repo..nvmrc
with20.14.0
.jest-cli
:(we use
--no-experimental-fetch
due to our use ofnock
for mocking HTTP)Attach to the process via Chrome devtools, and let it run.
Capture a full heap snapshot soon after letting the process run.
Capture a second full heap snapshot after letting the process run a while and the heap grow.
Examined the objects that were allocated between the snapshots, targeting objects with highest shallow size.
Steps
1. Original code
With Jest's original code, we can see the following:
A huge chunk of memory is
lodash
's source code replicated by references to it from a custom matcher:However, the vast majority of memory is probably held by
jest-circus
state (symbolJEST_STATE_SYMBOL
):In this snapshot we can see that just one state (of one test file) is leaking ~25MB in memory.
2. With global cleanup
This is after applying the first mitigation:
The leak has been reduced dramatically.
What we see now is (again) references to
lodash
's source code, but from a customlog4js
we use:Turns out that
log4js
adds the layout function tonode:cluster
module as an event listener.3. With global cleanup + event listeners cleanup
Next, after adding the event-listeners cleanup as well, we see this:
This time, it seems that
sequelize
's source (map) code is being duplicated in memory.In our version of
seqeulize
, it creates anew Error()
on each query (this was replaced in sequelize/sequelize#15559). This error is somehow captured and persisted in memory, keeping a customprepareStackTrace
(fromsource-map-support
) that keeps the source code.4. With all mitigations
After all mitigations in this PR applied:
We see that there's still some leaking source code, but that's for another PR :)