petkaantonov/bluebird

Memory leak of first fulfillment handler closures

jbreckman opened this issue · 0 comments

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.