Skip to content
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

Memory leak of first fulfillment handler closures #1660

Open
jbreckman opened this issue Sep 16, 2020 · 0 comments
Open

Memory leak of first fulfillment handler closures #1660

jbreckman opened this issue Sep 16, 2020 · 0 comments

Comments

@jbreckman
Copy link

jbreckman commented Sep 16, 2020

I saved this as promisetest.js and then ran node --inspect-brk promisetest.js. I attached chrome's debugger to my node process and took a snapshot of the memory and it holds onto 20MB of memory for 100 seconds. (it's important to take a snapshot since that forces the GC to run).

I tested mostly on node 12, but the problem was identified in node 10.

const Promise = require('bluebird');

// any bluebird promise that doesn't resolve immediately will do.
let p = Promise.delay(5);

// create a fn that has a large data object closed inside of it
function createCbHandler() {
  var lotsOfStrings = [];
  const count = 333000;
  for (var i = 0; i < count; i++) {
    lotsOfStrings.push(String(Math.random()));
  }
  return () => {
    return new Promise((resolve, reject) => {
      resolve(lotsOfStrings[Math.floor(Math.random()*count)]);
    })
  }
}

// operate on the results of the promise, but retain the original promise reference
p.then(createCbHandler());

setTimeout(() => {
  // make sure the original promise reference doesn't get garbage collected.  It will hold onto 20MB of memory
  // until this timer fires.
  console.log('stop the process from quitting', p)
}, 100000);

Almost identical code, but with a no-op as the first fulfillment handler. This retains only 2MB of memory:

const Promise = require('bluebird');

// any bluebird promise that doesn't resolve immediately will do.
let p = Promise.delay(5);

// create a fn that has a large data object closed inside of it
function createCbHandler() {
  var lotsOfStrings = [];
  const count = 333000;
  for (var i = 0; i < count; i++) {
    lotsOfStrings.push(String(Math.random()));
  }
  return () => {
    return new Promise((resolve, reject) => {
      resolve(lotsOfStrings[Math.floor(Math.random()*count)]);
    })
  }
}

// have the first handler (_fulfillmentHandler0) be a noop, but retain the original promise reference
p.then(Object);

// operate on the results of the promise, but retain the original promise reference
p.then(createCbHandler());

setTimeout(() => {
  // make sure the original promise reference doesn't get garbage collected.  It will now hold onto 
  // only 2MB of memory
  console.log('stop the process from quitting', p)
}, 100000)

I tried to narrow this down the best I could. I also tried to fix it --- but it looks like promise cancellation makes that very difficult. _fulfillmentHandler0 seems to be a special case that is retained as long as the promise itself is retained. The other fulfillment handlers get cleared out correctly.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant