Skip to content

Commit e5ff259

Browse files
Merge pull request then#67 from then/fix-memory-leak
Fix memory leak
2 parents 191403a + 932b832 commit e5ff259

File tree

4 files changed

+139
-5
lines changed

4 files changed

+139
-5
lines changed

lib/core.js

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,16 @@
22

33
var asap = require('asap')
44

5+
// Use Symbol if it's available, but otherwise just
6+
// generate a random string as this is probably going
7+
// to be unique
8+
var handleSymbol = typeof Symbol === 'function' ?
9+
Symbol() :
10+
(
11+
'_promise_internal_key_handle_' +
12+
Math.random().toString(35).substr(2, 10) + '_'
13+
)
14+
515
module.exports = Promise;
616
function Promise(fn) {
717
if (typeof this !== 'object') throw new TypeError('Promises must be constructed via new')
@@ -16,8 +26,11 @@ function Promise(fn) {
1626
handle(new Handler(onFulfilled, onRejected, resolve, reject))
1727
})
1828
}
19-
29+
this.then[handleSymbol] = handle;
2030
function handle(deferred) {
31+
return internalHandle(deferred);
32+
}
33+
function internalHandle(deferred) {
2134
if (state === null) {
2235
deferreds.push(deferred)
2336
return
@@ -46,7 +59,17 @@ function Promise(fn) {
4659
if (newValue && (typeof newValue === 'object' || typeof newValue === 'function')) {
4760
var then = newValue.then
4861
if (typeof then === 'function') {
49-
doResolve(then.bind(newValue), resolve, reject)
62+
if (typeof then[handleSymbol] === 'function') {
63+
// to prevent a memory leak, we adopt the value of the other promise
64+
// allowing this promise to be garbage collected as soon as nobody
65+
// has a reference to it
66+
internalHandle = then[handleSymbol];
67+
deferreds.forEach(function (deferred) {
68+
internalHandle(deferred);
69+
});
70+
} else {
71+
doResolve(then.bind(newValue), resolve, reject)
72+
}
5073
return
5174
}
5275
}

package.json

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@
44
"description": "Bare bones Promises/A+ implementation",
55
"main": "index.js",
66
"scripts": {
7-
"test": "mocha --timeout 200 --slow 99999",
8-
"test-resolve": "mocha test/resolver-tests.js -R spec --timeout 200 --slow 999999",
9-
"test-extensions": "mocha test/extensions-tests.js -R spec --timeout 200 --slow 999999"
7+
"test": "mocha --timeout 200 --slow 99999 -R dot && npm run test-memory-leak",
8+
"test-resolve": "mocha test/resolver-tests.js --timeout 200 --slow 999999",
9+
"test-extensions": "mocha test/extensions-tests.js --timeout 200 --slow 999999",
10+
"test-memory-leak": "node --expose-gc test/memory-leak.js"
1011
},
1112
"repository": {
1213
"type": "git",

test/memory-leak.js

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
'use strict';
2+
3+
var assert = require('assert')
4+
var Promise = require('../')
5+
6+
var i = 0
7+
var sampleA, sampleB
8+
9+
function next() {
10+
return new Promise(function (resolve) {
11+
i++
12+
/*
13+
if (i % 100000 === 0) {
14+
global.gc()
15+
console.dir(process.memoryUsage())
16+
}
17+
*/
18+
if (i === 100000 && typeof global.gc === 'function') {
19+
global.gc()
20+
sampleA = process.memoryUsage()
21+
}
22+
if (i > 100000 * 5) {
23+
if (typeof global.gc === 'function') {
24+
global.gc()
25+
sampleB = process.memoryUsage()
26+
console.log('Memory usage at start:');
27+
console.dir(sampleA)
28+
console.log('Memory usage at end:');
29+
console.dir(sampleB)
30+
assert(sampleA.rss * 1.2 > sampleB.rss, 'RSS should not grow by more than 20%')
31+
assert(sampleA.heapTotal * 1.2 > sampleB.heapTotal, 'heapTotal should not grow by more than 20%')
32+
assert(sampleA.heapUsed * 1.2 > sampleB.heapUsed, 'heapUsed should not grow by more than 20%')
33+
}
34+
} else {
35+
setImmediate(resolve)
36+
}
37+
}).then(next)
38+
}
39+
40+
if (typeof global.gc !== 'function') {
41+
console.warn('You must run with --expose-gc to test for memory leak.')
42+
}
43+
next().done()

test/nested-promises.js

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
'use strict';
2+
3+
var assert = require('assert');
4+
var Promise = require('../');
5+
6+
describe('nested promises', function () {
7+
it('does not result in any wierd behaviour - 1', function (done) {
8+
var resolveA, resolveB, resolveC;
9+
var A = new Promise(function (resolve, reject) {
10+
resolveA = resolve;
11+
});
12+
var B = new Promise(function (resolve, reject) {
13+
resolveB = resolve;
14+
});
15+
var C = new Promise(function (resolve, reject) {
16+
resolveC = resolve;
17+
});
18+
resolveA(B);
19+
resolveB(C);
20+
resolveC('foo');
21+
A.done(function (result) {
22+
assert(result === 'foo');
23+
done();
24+
});
25+
});
26+
it('does not result in any wierd behaviour - 2', function (done) {
27+
var resolveA, resolveB, resolveC, resolveD;
28+
var A = new Promise(function (resolve, reject) {
29+
resolveA = resolve;
30+
});
31+
var B = new Promise(function (resolve, reject) {
32+
resolveB = resolve;
33+
});
34+
var C = new Promise(function (resolve, reject) {
35+
resolveC = resolve;
36+
});
37+
var D = new Promise(function (resolve, reject) {
38+
resolveD = resolve;
39+
});
40+
var Athen = A.then, Bthen = B.then, Cthen = C.then, Dthen = D.then;
41+
resolveA(B);
42+
resolveB(C);
43+
resolveC(D);
44+
resolveD('foo');
45+
A.done(function (result) {
46+
assert(result === 'foo');
47+
done();
48+
});
49+
});
50+
it('does not result in any wierd behaviour - 2', function (done) {
51+
var promises = [];
52+
var resolveFns = [];
53+
for (var i = 0; i < 100; i++) {
54+
promises.push(new Promise(function (resolve) {
55+
resolveFns.push(resolve);
56+
}));
57+
}
58+
for (var i = 0; i < 99; i++) {
59+
resolveFns[i](promises[i + 1]);
60+
}
61+
resolveFns[99]('foo');
62+
promises[0].done(function (result) {
63+
assert(result === 'foo');
64+
done();
65+
});
66+
});
67+
});

0 commit comments

Comments
 (0)