jsdom: memory leaks

Ubuntu version 9.8.3

<--- Last few GCs --->

  188996 ms: Mark-sweep 1270.6 (1438.5) -> 1270.5 (1438.5) MB, 1316.5 / 0.0 ms [allocation failure] [GC in old space requested].
  190319 ms: Mark-sweep 1270.5 (1438.5) -> 1270.5 (1438.5) MB, 1322.9 / 0.0 ms [allocation failure] [GC in old space requested].
  191677 ms: Mark-sweep 1270.5 (1438.5) -> 1276.3 (1416.5) MB, 1357.9 / 0.0 ms [last resort gc].
  193035 ms: Mark-sweep 1276.3 (1416.5) -> 1282.1 (1416.5) MB, 1357.6 / 0.0 ms [last resort gc].


<--- JS stacktrace --->

==== JS stack trace =========================================

Security context: 0x2022a52cfb51 <JS Object>
    1: createNamedNodeMap [/var/local/project/node_modules/jsdom/lib/jsdom/living/attributes.js:~124] [pc=0x2b8a8af2439b] (this=0xc5590909ae1 <an Object with map 0x25bed3869f1>,element=0x2ae6ff82af39 <an EventTargetImpl with map 0x42d480626b9>)
    2: new constructor(aka ElementImpl) [/var/local/project/node_modules/jsdom/lib/jsdom/living/nodes/Element-impl.js:99] [pc=0x2b8a8b3b741b] (this=0x2ae6ff82af...

FATAL ERROR: CALL_AND_RETRY_LAST Allocation failed - JavaScript heap out of memory
 1: node::Abort() [/usr/bin/node]
 2: 0x10b084c [/usr/bin/node]
 3: v8::Utils::ReportApiFailure(char const*, char const*) [/usr/bin/node]
 4: v8::internal::V8::FatalProcessOutOfMemory(char const*, bool) [/usr/bin/node]
 5: v8::internal::Factory::NewFillerObject(int, bool, v8::internal::AllocationSpace) [/usr/bin/node]
 6: v8::internal::Runtime_AllocateInTargetSpace(int, v8::internal::Object**, v8::internal::Isolate*) [/usr/bin/node]
 7: 0x2b8a883079a7
Aborted (core dumped)


Another memory leak:

<--- Last few GCs --->

  167580 ms: Mark-sweep 1311.0 (1435.1) -> 1299.1 (1435.1) MB, 1288.2 / 0.0 ms [allocation failure] [GC in old space requested].
  168860 ms: Mark-sweep 1299.1 (1435.1) -> 1299.1 (1435.1) MB, 1279.7 / 0.0 ms [allocation failure] [GC in old space requested].
  170198 ms: Mark-sweep 1299.1 (1435.1) -> 1303.8 (1405.1) MB, 1337.7 / 0.0 ms [last resort gc].
  171545 ms: Mark-sweep 1303.8 (1405.1) -> 1308.3 (1405.1) MB, 1347.5 / 0.0 ms [last resort gc].


<--- JS stacktrace --->

==== JS stack trace =========================================

Security context: 0x1bbb8e7cfb51 <JS Object>
    1: trim [native string.js:~364] [pc=0x3b0c6175d5c7] (this=0x1947dccf95c1 <Very long string[252184]>)
    2: _attach [/var/local/project/node_modules/jsdom/lib/jsdom/living/nodes/HTMLScriptElement-impl.js:~32] [pc=0x3b0c617e482c] (this=0x3f78f4f64599 <an EventTargetImpl with map 0x3df73fd78349>)
    3: _attach [/var/local/project/node_modules/jsdom/lib/jsdom/living/nodes/Node-impl.js:289] [pc=0x3b0c...

FATAL ERROR: CALL_AND_RETRY_LAST Allocation failed - JavaScript heap out of memory
 1: node::Abort() [/usr/bin/node]
 2: 0x10b084c [/usr/bin/node]
 3: v8::Utils::ReportApiFailure(char const*, char const*) [/usr/bin/node]
 4: v8::internal::V8::FatalProcessOutOfMemory(char const*, bool) [/usr/bin/node]
 5: v8::internal::Factory::NewRawTwoByteString(int, v8::internal::PretenureFlag) [/usr/bin/node]
 6: v8::internal::String::SlowFlatten(v8::internal::Handle<v8::internal::ConsString>, v8::internal::PretenureFlag) [/usr/bin/node]
 7: v8::internal::Runtime_StringTrim(int, v8::internal::Object**, v8::internal::Isolate*) [/usr/bin/node]
 8: 0x3b0c5e2092a7
Aborted (core dumped)

About this issue

  • Original URL
  • State: open
  • Created 8 years ago
  • Reactions: 2
  • Comments: 34 (9 by maintainers)

Most upvoted comments

fwiw I found the leak is more apparent when using the constructor. I was looping through an array of ~2000 filenames and loading each into memory so I could query with JSDOM.

when I was using new JSDOM('...') for each file, the heap eventually blew out, but when I used the same instance and merely replaced body.innerHTML, my script completed.

so in my case, it went from something like this…

const filenames = fs.readdirSync('./path/to/files');

const filteredFiles = filenames.filter(filename => {
  const html = fs.readFileSync(`./path/to/files/${filename}`, 'utf8')
  const { document } = new JSDOM(html).window;
  return !document.querySelector('table');
})

to this… diffs marked

+ const { document } = new JSDOM().window;
  const filenames = fs.readdirSync('./path/to/files');

  const filteredFiles = filenames.filter(filename => {
    const html = fs.readFileSync(`./path/to/files/${filename}`, 'utf8')
-   const { document } = new JSDOM(html).window;
+   document.body.innerHTML = html;
    return !document.querySelector('table');
  })

This approach also resolves the issue from this comment

compare this, which dies after a while (edited slightly to allow for reassigning to the body):

const { JSDOM } = require('JSDOM');

while (true) {
  const dom = new JSDOM('<html></html>');
}

to this, which goes on without trouble:

  const { JSDOM } = require('JSDOM');
+ const dom = new JSDOM();

  while (true) {
-   const dom = new JSDOM('<html></html>');
+   dom.window.document.body.innerHTML = '<html></html>';
  }

I also had memory overflows and then found out that JSDOM is not intended to be used synchronously. 10-20 calls do not make a problem but 50000 do. If we look at the sources then we see: (/jsdom/lib/jsdom/browser/Window) ... ///// INITIALIZATION process.nextTick(() => { ... }); It is called within JSDOM constructor so called synchronously many times it overflows node event loop. Possibly it’s the reason of many memory leaks so async using solves the problem.

This code works bad (heap grows): for (i = 0; i < 10000; i ++) { dom = new JSDOM(s); ... }

This code works good: var i = 0; function f() { dom = new JSDOM(s); ... if (i ++ < 10000) setImmediate(f); } f();

I stumbled upon your comment while searching for memory leak issues with JSDOM. May I learn how to reuse a single JSDOM instance? I was unable to find any information on how to reuse a JSDOM instance. Thank you.

It’s not clear that would solve the issue; in general, as described in @noslouch’s comment, creating 2000 JSDOM instances is always going to be more expensive than reusing a single one. But it couldn’t hurt.

A better workaround is to let the event loop take a breather after a while. This works:

const util = require('util');

const { JSDOM } = require('jsdom');

const waitImmediate = util.promisify(setImmediate);

(async () => {
  while (true) {
    const dom = new JSDOM('<html></html>');
    dom.window.close();
    await waitImmediate();
  }
})();

leak1

Here is the info of the heap, sorted by retained size. There are multiple window instances as i understand, one for each parsed page. And they are linked somewhere to global scope, i can’t find what reference keeps them alive and don’t allow GC to remove them.

leak2