You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
@jeisingerIt's not a spec thing, adding a manual gc every e.g. 1e7 iterations fixes it.
Looks very similar to #6180 — scavege is triggered instead of mark-sweep, but the memory is reclaimed only by mark-sweep, not by scavenge.
Update: I made a mistake in the test, shame on me =).
Can anyone explain the result of this conversation? I'm using "Node v8.9.2" but there is still a leak for cyclic use Promise or async / await functions.
@fenixphp do you mean that you experiment the leak just with promises or that you experiment it both with promises and async functions ?
Theoretically this only happens on promises because they keep a chain that grows wild, which is not the case of async functions.
Thanks @danielo515, Experimentally, I came to the conclusion that promises begin to leak as the call chain looks like a tree. What can I replace the promises in a pair of async / await functions?
You can use async await function calls inside an infinite loop without any problem. But if you use recursion you will end with the same stack size problem.
Reading this thread, I am getting a little worried and confused.
The following code will make me able to pile tasks wherever in my codebase, and make sure that each task is executed only after all the previous ones are done.
function task() {
//dummy async treatment
return delay(100);
}
let stack = Promise.resolve('the stack is empty');
function addTask() {
return stack = stack.then(task);
}
When repeatedly calling addTask over the time, like every second, will I have a memory leak?
If I do, how should I work around this?
Thanks for your help
Reacted by Abdel Atencio, kolya182 and Mikis Woodwinter
@Sharcoux your code do not have the tail-recursive pattern like the example, and if your outer code do not have the pattern either, and only reference the tail of the promise chain, the GC should work and no memory leak.
From what I tested, the pattern for promise memory leaking must include a tail-recursive setup, the promise creating function always recursively appear in itself's promise.then code.
The recursive code cause the promise implementation need to keep extra reference/stack info so the GC can not claim those already-resolved promise from the chain.
If we build a long/infinite promise chain gradually without the recursive code pattern, and only keep reference to the tail of the chain, the GC can took those un-referenced promise from the chain, whether the promise is pending or not.
So I think chaining promise up is not always unsafe, it depends on the actual code.
And if the promise version of while (true) { /* async code */ } is needed, better use a async function instead of the tail-recursive promise pattern.
First part of test is 2 different OOM sample code and a fix attempt:
Then some common promise chain use pattern is tested:
[OOM] holding the head & tail promise of a chain of pending promise
[OOM] holding the head-resolve & tail promise of a chain of pending promise
[SAFE] holding the tail promise of a chain of pending promise
[SAFE] holding the head & tail promise of a chain of resolving promise
[SAFE] holding the tail promise of a chain of resolving promise
The test code I used: (and an sample output from v13.11.0 x64 win32)
console.log('Testing Env:',process.version,process.arch,process.platform)constspawnFuncWithExposeGC=(...funcList)=>{const{ status }=require('child_process').spawnSync(process.argv0,['--expose-gc',// allow `global.gc()` call'--max-old-space-size=64',// limit max memory usage for faster OOM'--eval',`(${funcList.reduce((o,func)=>`(${func})(global.gc, ${o})`,'undefined')})`],{stdio: 'inherit'})console.log(`process exit with status ${status} `.padEnd(64,'='))console.log('\n')}constcommonFunc=(triggerGC)=>{constsetTimeoutAsync=(wait=0)=>newPromise((resolve)=>setTimeout(resolve,wait))constformatMemory=(value)=>`${String(value).padStart(10,' ')}B`constmarkMemory=async()=>{triggerGC()awaitsetTimeoutAsync(10)triggerGC()const{ heapUsed, heapTotal, rss, external }=process.memoryUsage()console.log([`heapUsed: ${formatMemory(heapUsed)}`,`heapTotal: ${formatMemory(heapTotal)}`,`rss: ${formatMemory(rss)}`,`external: ${formatMemory(external)}`].join(' '))}constappendPromiseAdder=(promise,count=0)=>{letindex=0while(index++!==count)promise=promise.then((result)=>(result+1))returnpromise}return{ setTimeoutAsync, markMemory, appendPromiseAdder }}spawnFuncWithExposeGC(async()=>{console.log('[OOM] tail-recursive promise setup, GH sample, edit & formatted. https://github.com/promises-aplus/promises-spec/issues/179#issuecomment-93453094')construn=(i)=>newPromise((resolve)=>setImmediate(resolve)).then(()=>{if(i%1e5===0)console.log({ i })returni<99999999 ? run(i+1) : i})awaitrun(0).then((result)=>console.log(result))})spawnFuncWithExposeGC(async()=>{console.log('[OOM] tail-recursive promise setup, edit & formatted. https://alexn.org/blog/2017/10/11/javascript-promise-leaks-memory.html')constsignal=(i)=>newPromise((resolve)=>setImmediate(()=>resolve(i)))constloop=(n)=>signal(n).then(i=>{if(i%1e5===0)console.log({ i })returnloop(n+1)})awaitloop(0).catch(console.error)})spawnFuncWithExposeGC(commonFunc,async(triggerGC,{ markMemory })=>{console.log('[SAFE] no-recursive promise setup')leti=0letpromiseTail=Promise.resolve()consttoken=setInterval(()=>{// simulate user input or other outer timer adding batch of task to the queueif(i>=1e8)returnclearInterval(token)// check finishletn=0while(n++!==1e5){promiseTail=promiseTail.then(()=>{i=i+1if(i%1e6!==0)return// check logconsole.log({ i })markMemory()})}},0)})spawnFuncWithExposeGC(commonFunc,async(triggerGC,{ setTimeoutAsync, markMemory, appendPromiseAdder })=>{console.log('[OOM] holding the head & tail promise of a chain of pending promise')constpromiseHead=newPromise((resolve)=>{})letpromiseTail=promiseHeadletloop=0while(loop++!==128){awaitmarkMemory()promiseTail=appendPromiseAdder(promiseTail,64*1024)awaitsetTimeoutAsync(10)}console.log({ promiseHead, promiseTail })})spawnFuncWithExposeGC(commonFunc,async(triggerGC,{ setTimeoutAsync, markMemory, appendPromiseAdder })=>{console.log('[OOM] holding the head-resolve & tail promise of a chain of pending promise')letpendingResolveletpromiseTail=newPromise((resolve)=>{pendingResolve=resolve})letloop=0while(loop++!==128){awaitmarkMemory()promiseTail=appendPromiseAdder(promiseTail,64*1024)awaitsetTimeoutAsync(10)}console.log({ pendingResolve, promiseTail })})spawnFuncWithExposeGC(commonFunc,async(triggerGC,{ setTimeoutAsync, markMemory, appendPromiseAdder })=>{{console.log('[SAFE] holding the tail promise of a chain of pending promise')letpromiseTail=newPromise((resolve)=>{})letloop=0while(loop++!==128){awaitmarkMemory()promiseTail=appendPromiseAdder(promiseTail,64*1024)awaitsetTimeoutAsync(10)}console.log({ promiseTail })console.log('\n')}{console.log('[SAFE] holding the head & tail promise of a chain of resolving promise')constpromiseHead=newPromise((resolve)=>{resolve(0)})letpromiseTail=promiseHeadletloop=0while(loop++!==128){awaitmarkMemory()promiseTail=appendPromiseAdder(promiseTail,64*1024).then((result)=>{console.log({ result })returnresult})awaitsetTimeoutAsync(10)}console.log({ promiseHead, promiseTail })console.log('\n')}{console.log('[SAFE] holding the tail promise of a chain of resolving promise')letpromiseTail=newPromise((resolve)=>{resolve(0)})letloop=0while(loop++!==128){awaitmarkMemory()promiseTail=appendPromiseAdder(promiseTail,64*1024)promiseTail=promiseTail.then((result)=>{console.log({ result })returnresult})awaitsetTimeoutAsync(10)}console.log({ promiseTail })console.log('\n')}})
Activity
mscdex commentedon May 10, 2016
/cc @nodejs/v8
Fishrock123 commentedon May 10, 2016
Is this because each promise keeps a reference to the next?
ChALkeR commentedon May 10, 2016
@Fishrock123
Doesn't look like that, looks like mark-sweep isn't called for whatever reason.Could be a duplicate of #6180.Perhaps it's even fixed by #6398 in the current master, but I can't check atm.Update: it's not, #6180 isn't fixed yet.jeisinger commentedon May 10, 2016
filed https://bugs.chromium.org/p/v8/issues/detail?id=5002
Firefox also runs out of memory, so maybe it's a spec thing
according to the crash, MS is executed.
ChALkeR commentedon May 10, 2016
@jeisinger
It's not a spec thing, adding a manualgcevery e.g. 1e7 iterations fixes it.Looks very similar to #6180 — scavege is triggered instead of mark-sweep, but the memory is reclaimed only by mark-sweep, not by scavenge.
Update: I made a mistake in the test, shame on me =).
julien-f commentedon May 10, 2016
@jeisinger If it's a spec thing it's an issue because I don't know a better way to do a loop with promises :/
@petkaantonov What's your opinion on this?
ChALkeR commentedon May 10, 2016
@jeisinger No idea why the mark-sweep at the end doesn't reclaim most of that, though.ChALkeR commentedon May 10, 2016
@Fishrock123 @jeisinger I made a mistake in the test, ignore my previous comments. Sorry for the inconvenience.
Fishrock123 commentedon May 10, 2016
Unbounded recursive loops are typically not safe in JavaScript at the current time, promise or not apparently.
ChALkeR commentedon May 10, 2016
Yes, it looks like all Promise objects are retained in the heap. There are twice more of them than the number of iterations, btw.
vkurchatkin commentedon May 10, 2016
makes sense, 2 new promises are created on each iteration
julien-f commentedon May 10, 2016
Yes but there are no other way to do async loops than using recursion (without tricks likes
setImmediate()to break the stack).ChALkeR commentedon May 10, 2016
Ah, yes, that is correct.
28 remaining items
danielo515 commentedon Jul 28, 2017
@bnoordhuis update to which version ? With v.7 I mean 7.10. If you mean that I should update to node v.8 that may be a problem
bnoordhuis commentedon Jul 28, 2017
@danielo515 either v6.x or v8.x, both are LTS release lines.
danielo515 commentedon Jul 28, 2017
Thanks @bnoordhuis , I'll give a try to v8.x
fenixphp commentedon Jan 26, 2018
Can anyone explain the result of this conversation? I'm using "Node v8.9.2" but there is still a leak for cyclic use Promise or async / await functions.
danielo515 commentedon Jan 27, 2018
@fenixphp do you mean that you experiment the leak just with promises or that you experiment it both with promises and async functions ?
Theoretically this only happens on promises because they keep a chain that grows wild, which is not the case of async functions.
fenixphp commentedon Jan 27, 2018
Thanks @danielo515, Experimentally, I came to the conclusion that promises begin to leak as the call chain looks like a tree. What can I replace the promises in a pair of async / await functions?
danielo515 commentedon Jan 28, 2018
You can use async await function calls inside an infinite loop without any problem. But if you use recursion you will end with the same stack size problem.
Poller: remore recursion and memory leak. The problem is well describ…
Sharcoux commentedon Nov 18, 2019
Reading this thread, I am getting a little worried and confused.
The following code will make me able to pile tasks wherever in my codebase, and make sure that each task is executed only after all the previous ones are done.
Thanks for your help
dr-js commentedon Mar 15, 2020
@Sharcoux your code do not have the tail-recursive pattern like the example, and if your outer code do not have the pattern either, and only reference the tail of the promise chain, the GC should work and no memory leak.
dr-js commentedon Mar 15, 2020
From what I tested, the pattern for promise memory leaking must include a tail-recursive setup, the promise creating function always recursively appear in itself's
promise.thencode.The recursive code cause the promise implementation need to keep extra reference/stack info so the GC can not claim those already-resolved promise from the chain.
If we build a long/infinite promise chain gradually without the recursive code pattern, and only keep reference to the tail of the chain, the GC can took those un-referenced promise from the chain, whether the promise is pending or not.
So I think chaining promise up is not always unsafe, it depends on the actual code.
And if the promise version of
while (true) { /* async code */ }is needed, better use aasyncfunction instead of the tail-recursive promise pattern.First part of test is 2 different OOM sample code and a fix attempt:
Then some common promise chain use pattern is tested:
The test code I used: (and an sample output from
v13.11.0 x64 win32)Fix memory from leaking
Fix memory leaking
feat(utils): add sequential async iterators
refactor: clear Sonar maintainability issues (S9382, S7760, S2301) (#752