jest: jsdom or babel-polyfill cause memory leak (?)

Hello.

First of all thanks for this project, it’s been great so far. I’m running into a issue where when Jest can resolve babel-polyfill the node memory heap keeps on growing resulting in memory allocation errors in node 5 or extremely slow test in node 6 when node starts to reach the memory limit.

This very simple repository reproduce the issue https://github.com/quentin-/jest-test

describe('test', function() {
  beforeAll(function() {
    global.gc();
  });

  it('work', function() {
    expect(1).toBe(1);
  });
});

# heap size is trending upward.
# test become unbearably slow after a while
npm run test -- --logHeapUsage

node: 6.7.0

About this issue

  • Original URL
  • State: closed
  • Created 8 years ago
  • Reactions: 10
  • Comments: 52 (8 by maintainers)

Most upvoted comments

This was fixed accidentally with this commit https://github.com/facebook/jest/commit/8256904751d85e16909152ddaf8395d7c2b60eba and I have no idea why this works, seriously. Anybody have any ideas? Maybe it’s some kind of weird edge bug in Node.

It will land in Jest 19. We plan to release it in a week.

Some more specific data on the heap growth in the example repo, from running it on my machine.

npm install
node --expose-gc ./node_modules/.bin/jest --runInBand --logHeapUsage

The heap size after the first test is 45MB and it grows linearly with a slope of 0.48.

rm -rf node_modules/babel-polyfill
node --expose-gc ./node_modules/.bin/jest --runInBand --logHeapUsage

The heap size after the first test is 48MB and it grows linearly with a slope of 1.03.

Is that to be expected? This is 1MB growth on the heap for this test:

describe('test', function() {
  it('works', function() {
    expect(1).toBe(1);
  });
});

@thymikee I was facing the same memory issue and then very slow tests. I made a fresh install of node_modules and then jest@next - my tests went from ~750s to ~125s and memory was stable around 800mb per worker (previously hitting the limit of 1.45gb per worker)

I apologize. I forgot to merge https://github.com/facebook/jest/pull/2755 into the release (honestly I thought I did), it will be part of the next release.

We upgraded to Jest 19 in the hopes that this memory leak would be fixed, but unfortunately that’s not the case. We’ve been using the --runInBand flag to run tests on individual folders to dodge this memory leak up until this point, but now (with Jest 19) the memory leak has resurfaced. You can see how the test times are snowballing here:

image

As a result, Circle CI gives us this failure:

image

Is anyone else still plagued by this memory leak in Jest 19?

@thymikee any idea when this will be released? Also hitting issues with memory leaks on circleci and the setImmediate change appears to do the trick. Thanks for tracking it down!

Okay. We have this issue fixed in our project with a small workaround. As already said earlier, both issues (jsdom and babel-polyfill) have to be tackled in order to have a constant memory usage. These steps are necessary:

  1. Remove babel-polyfill from your project. This is essential. Otherwise jsdom windows will still leak.
  2. Create a new setup file in your project that overrides the describe method and automatically creates/destroys the JSDom environment before/after each test. See the example below.
  3. Run jest with node –harmony (for babel-polyfill replacement), –env node (instead of the default jsdom environment) and –setupTestFrameworkScriptFile path/to/your/setup.js

Following example of a setup file:

import jsdom from 'jsdom'
 
const FAKE_DOM_HTML = `
    <html>
    <body>
    </body>
    </html>
`
 
/*
 * First, we override Jasmine's describe method so that we can automatically add beforeAll and afterAll.
 * In the beforeAll and afterAll we asynchronously setup and tear down the jsdom instance.
 * This solves the synchronous window.close() issue with jsdom described here:
 *
 * https://github.com/tmpvar/jsdom/issues/1682#issuecomment-270310752
 *
 * Be aware, that if your project has "babel-polyfill" in its node_modules, you will have
 * problems with leaking windows. If you need polyfills for the building process, use the cloned version
 * that is renamed to something else than babel-polyfill.
 */
const desc = global.describe
global.describe = (title, test) => {
    /* We need to keep reference to the window to destroy the correct one afterwards.
     * Otherwise, some tests might fail. This is probably related to this fact here:
     * https://github.com/airbnb/enzyme/blob/master/docs/guides/jsdom.md#describewithdom-api-and-clearing-the-document-after-every-test
     */
    let testWindow = null
    desc(title, () => {
        beforeAll((done) => {
            // We wait until JSDom has finished building the window, before continuing with the tests.
            jsdom.env(FAKE_DOM_HTML, (err, window) => { // eslint-disable-line
                testWindow = window
                global.window = window
                done()
            })
        })
        afterAll((done) => {
            // After the test suite completes, we ASYNCHRONOUSLY delete the apropriate window, making sure it is released correctly.
            setImmediate(() => {
                if (testWindow) {
                    testWindow.close()
                }
                done()
            })
        })
        const testRunner = test.bind({})
        testRunner()
    })
}
global.describe.skip = desc.skip

Now you can run jest with:

node --harmony node_modules/.bin/jest --env node --setupTestFrameworkScriptFile path/to/your/setup.js

Profit!

In the jest project, following could be done to make the life easier:

  1. Make polyfills optional. For, example, if babel-jest is provided, but --no-polyfills flag is set, run the code with --harmony flag.
  2. Rewrite jest-environment-jsdom to create/destroy the window asynchronously as shown above.

I’m wondering: do we even need babel-polyfill in recent versions of Node? Shall we just not load it (except for regenerator, which we may still need).

I did research further. It seems that JSDOM is just a drop in an ocean. If I implement a fix for JSDOM locally, I still get huge memory leaks. However, as soon as I remove babel-polyfill, the leaks drop by 95%. IT seems that certain shims in corejs (the Object getters/setters, for example) are causing the leaks when combined with jest.

A solution would be disabling babel-polyfill and starting jest with

node --harmony jest --no-polyfill...
  1. I would change the title of this ticket back to babel-polyfill, since it seems to be far bigger memory issue than JSDOM.
  2. I would add a config option to disable automatic babel-polyfill import.

See my comment in tmpvar/jsdom#1682 : the memory leak in jsdom only happens when window.close() is called synchronously after jsdom.jsdom() I see two options here:

  1. To try fixing jsdom. There is obviously something asynchronous happening in jsdom.jsdom. This leads to a reference of the window being held beyond the window.close() call.
  2. Try fixing jest-envirnment-jsdom. If it is rewritten to return a promise, instead setting directly a window, we could use jsdom.env, which solves the problem by being asynchronous.

@thymikee I’ll give it a try.

Update: @next (19.1.0-alpha.eed82034), with babel-polyfill re-installed in node_modules (dependency of another package).

node ./node_modules/.bin/jest - Heap size growing, running slow node --expose-gc ./node_modules/.bin/jest --logHeapUsage [--runInBand] - Heap size stable, running slow

Note: “running slow” here means:

Test Suites: ...52 of 550 total
Snapshots:   0 total
Time:        50s, estimated 74s

After removing babel-polyfill from node_modules:

node ./node_modules/.bin/jest (heap size growing)

Test Suites: ...61 of 550 total
Snapshots:   0 total
Time:        50s, estimated 74s

@mlanter @jedmao I encourage you to debug your tests with --inspect flag for Chrome Debugger and try to figure out where the leak may come from (it’s likely something in your tests). To simulate slow CI environment, you can throttle the CPU a bit:

screen shot 2017-05-16 at 08 42 18

Still getting the memory leak on Jest 20.0.1, but it only happens in our Travis-CI environment. Any ideas on why that would be?

image

The same tests run in just under a minute on my local machine:

image

Hi there, let me add some observations from our project. I am trying to migrate our Mocha stack to Jest and I encountered similar problems.

I’ve tried removing babel-polyfill and adding the afterAll hacks as @romansemko suggested, however, I have got the same results for all cases — around 7 extra MB in heap memory per describe. It makes the heap grow over 1G after 1500 tests and crashes node for JavaScript heap out of memory after a while. We have over 3k tests for now and the main problem with the leakage is that the tests take 25m to finish. With mocha it is under 3 minutes for comparison.

Our test environment is quite complicated, we mock canvas rendering context in jsdom, we still have half of our codebase in coffee-scriptand we have large number of dependencies, any of them might have another leaks on their own.

I realized that besides transforming coffee-script via transform config option I had require('coffee-script/register') in one of our setupFiles as well. So I removed it and tada 🎉 the memory usage dropped. So the main culprit in our case was global coffee-script/register.

We have some async-await stuff in our codebase so we need babel-polyfill. I added it to the env and I could see the rise of the heap again. This time it was around 50% of the original space filled with coffee-script + babel-polyfill leaks. It is interesting that when I removed babel-polyfill only and kept coffee-script there were similar amounts of leaks as with coffee-script + babel-polyfill. But babel-polyfill itself had half of them too.

Since we don’t really need the whole babel-polyfill I replaced it with regenerator-runtime/runtime and I was able to get almost the same results as without it. I also tried Node 7 with --harmony --harmony-async-await but I was not able to make it work, I was still getting errors ReferenceError: regeneratorRuntime is not defined.

Anyway, I think the best solution would be to solve this on jest level. Not in jsdom or babel-polyfill and not in testEnv or testRunner (in afterAlls). Currently we run our tests with Mocha and everything works (with possibly present hidden leaks) quite fast. I guess there has to be more thorough environment reset in Mocha. Yes, we can delete/unset more things in dispose method as @cpojer suggested, but it shouldn’t be jsdom or babel-polyfill thinggie only, it should be something more radical as the leaks might be in variety of dependencies. Maybe it should be even higher up in the environment creation hierarchy? Could there be a particular reason, maybe a design decision, that leads to the leaks from the specs being leaked “globally” (copared to Mocha), @cpojer?

Are you sure jsdom.env fixes the problem for you? I’ve changed jest-environment-jsdom locally to use it once and didn’t see better results. Jest disposes jsdom (calls window.close()) not in the same tick. also wrapping the close() method in setTimeout didn’t help.

BTW, I appreciate your help on investigating that so much, thank you!

…unless you want to test with exactly the same setup as the “real” thing. The node and core.js implementations of the ES6/7 features could be different. 😉

Still, it’s better to get rid of it, instead of having OOM problems in bigger test suites. Or, at least, giving an option to disable auto-loading of babel-polyfill.