Repository navigation
vm Script memory leak in Node.js 14 / 16 #40014
Copy link
Copy link
Closed
Labels
confirmed-bugIssues and PRs for confirmed bugs.Issues and PRs for confirmed bugs.v8 engineIssues and PRs related to the V8 dependency.Issues and PRs related to the V8 dependency.vmIssues and PRs related to the vm subsystem.Issues and PRs related to the vm subsystem.
Description
Version
14 / 16
Platform
x64 macOS / Linux
Subsystem
vm
What steps will reproduce the bug?
'use strict';
let times = 0;
function run() {
let str = 'var a = "';
for (let i = 0; i < 100 * 1024 / 18; i++) str += Math.random().toString();
str += '";';
const script = new (require('vm').Script)(str);
times++;
if (times % 1000 === 0) console.log(times);
}
(async () => {
while (true) {
run();
await new Promise(resolve => {
setTimeout(() => {
resolve();
}, 10);
});
gc();
}
})();Run with:
$ node --expose-gc --max-heap-size=100 test.jsIt will occur heap OOM under Node.js 14 and 16, and Node.js 12 is OK.
<--- Last few GCs --->
[1430950:0x55695b1dc4b0] 6372 ms: Mark-sweep 72.0 (91.6) -> 71.6 (91.8) MB, 4.3 / 0.0 ms (average mu = 0.728, current mu = 0.778) testing GC in old space requested
[1430950:0x55695b1dc4b0] 6388 ms: Mark-sweep 72.2 (91.8) -> 71.8 (92.1) MB, 3.1 / 0.0 ms (average mu = 0.760, current mu = 0.804) testing GC in old space requested
[1430950:0x55695b1dc4b0] 6403 ms: Mark-sweep 72.4 (92.1) -> 72.0 (92.3) MB, 2.9 / 0.0 ms (average mu = 0.781, current mu = 0.806) testing GC in old space requested
<--- JS stacktrace --->
FATAL ERROR: MarkCompactCollector: young object promotion failed Allocation failed - JavaScript heap out of memory
How often does it reproduce? Is there a required condition?
Every time.
What is the expected behavior?
Not OOM.
What do you see instead?
OOM.
Additional information
No response
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
confirmed-bugIssues and PRs for confirmed bugs.Issues and PRs for confirmed bugs.v8 engineIssues and PRs related to the V8 dependency.Issues and PRs related to the V8 dependency.vmIssues and PRs related to the vm subsystem.Issues and PRs related to the vm subsystem.
Activity
XadillaX commentedon Sep 6, 2021
And another amazing thing:
node --expose-gc --max-heap-size=100 --inspect=9229 test.jsfor a while, OOM.node --expose-gc --max-heap-size=100 --inspect=9229 test.jsand then runnode inspect localhost:9229, everything will be OK and there's no OOM any more.XadillaX commentedon Sep 6, 2021
I added
To the code, and found there're full of the
strobject like:XadillaX commentedon Sep 7, 2021
The leak seems in
CompilationCache.That means when I run command below in Node.js 14 / 16:
The behavior is right - no longer OOM.
targos commentedon Sep 7, 2021
/cc @nodejs/v8
XadillaX commentedon Sep 7, 2021
In Node.js 12, the heap size will up to about 100M either. But after it cannot allocate more space, it will trigger
kLastResortGC because:https://github.com/nodejs/node/blob/v12.22.6/deps/v8/src/heap/heap.cc#L4904-L4925
After
kLastResortGC, the heap size will down to very small.And Node.js 14 / 16 will never trigger
kLastResortGC:https://github.com/nodejs/node/blob/v14.16.0/deps/v8/src/heap/heap.cc#L5205-L5226
And heap size still increases until OOM.
XadillaX commentedon Sep 7, 2021
https://github.com/nodejs/node/blob/master/deps/v8/src/heap/mark-compact.cc#L1458-L1469
EvacuateNewSpaceVisitor::Visit()in GC, this new code (After Node.js 14) requires allocating an old space block and returns anAllocateResult.IsRetry()object.heap_->FatalProcessOutOfMemory()here at last.So we should think what should we do when we're under GC but we have no enough space to allocate old space block.
Old V8 (Node.js 12) just no this
iflogic, and allocate old space anyway. And it will dokLastResortGC later.What's more, it seems that I can't disable
always promote young mcby--no-always-promote-young-mcbecause ofDEFINE_IMPLICATION(array_buffer_extension, always_promote_young_mc)in v8'ssrc/flags/flag-defination.h.Refs
XadillaX commentedon Sep 7, 2021
I think we have 2 ways to fix this bug:
_always_promote_young's logic in GC - make allocate old space not throw error when heap size up to exceeded.array_buffer_extensionin gyp back to 0, and set--always-promote-young-mcdefaults tofalse.btw. why UnboundScript's script cache and its code string won't GCed until
kLastResort(aka. heap size up to exceeded)? Can this be fixed?XadillaX commentedon Sep 8, 2021
There're several problems (bugs) occur this one issue:
Script'sCompilationCachein the heap and won't be GCed until Last Resort GC; (It will make heap size to the limit and--no-compilation-cachecan resolve it)--always-promote-young-mcwill OOM when old space cannot allocate more memory (e.g. when heap size limit exceeded); (I think it should do a Last Resort GC when we meet this situation like Node.js 12's V8)--always-promote-young-mcbecause--array-buffer-extensionalwaystrueat compile time./cc @targos
/ping @nodejs/v8
XadillaX commentedon Sep 8, 2021
https://bugs.chromium.org/p/v8/issues/detail?id=12198
sidverma32 commentedon Oct 29, 2021
This answer helped me: StackOverflow
lukeapage commentedon Dec 15, 2021
@XadillaX given you did such detailed analysis do you have any idea what is the path forward now v8 closed the bug you raised as won’t fix ? With some suggestions for the node team ?
XadillaX commentedon Dec 16, 2021
Hmmm... I'm not sure since V8 closed that issue. I think we can't do anything without v8's change.
5 remaining items
mbyrne00 commentedon Jun 28, 2022
Oh yes - sorry for the bad link, you are right. I'll also update my comment
benjamingr commentedon Jul 28, 2022
@XadillaX @nodejs/vm @nodejs/v8 are there any updates on this issue or anything that changed in the last year?
benjamingr commentedon Jul 28, 2022
Also more context jestjs/jest#12205 which implies maybe @nodejs/loaders is also the right ping since adding importModuleDynamically causes the leak
legendecas commentedon Jul 28, 2022
The problem will be gone in the example of the OP if we simply remove the
gc()call, as explained in https://bugs.chromium.org/p/v8/issues/detail?id=12198#c4. So I'd believe this issue can be closed.benjamingr commentedon Jul 28, 2022
@legendecas my understanding is that the "explicit gc calling" issue is in addition and not instead of Jest's issue which they are experiencing without any
gc()calls.Namely jestjs/jest#12205 changes code with 0 gc calls to 0 gc calls and resolves the issue except if they also pass
importModuleDynamicallywhich causes the leak again.legendecas commentedon Jul 28, 2022
@benjamingr I would take it as a different problem so I'd suggest opening a dedicated issue to track it.
legendecas commentedon Aug 15, 2022
@benjamingr I've opened #44211 to track the issues with
vm.compileFunction/importModuleDynamically.Update dependency jest to v29 (#241)
sunilsurana commentedon Sep 1, 2023
Hi, We have been unable to upgrade our node beyond 16.10.0 and have been stuck since higher version regresses the performance of our jest tests. Is there any solution coming for this from node?
RaphaelMechali commentedon Sep 5, 2023
Same here...
nicolo-ribaudo commentedon Sep 5, 2023
You might try using https://www.npmjs.com/package/jest-light-runner to use Jest without
vm.Script.joyeecheung commentedon Sep 5, 2023
FYI there is a rewrite of the
vm.Script/vm.*Modulememory management that might fix many issues in #48510 (the OP referenced the bugs with small reproducible cases that can be confirmed to be fixed. Can't say it definitely fixes other bugs without reproducible examples, but it should fix a bunch more issues in the issue tracker that I didn't dig out). It is pending on a V8 patch which is probably going to be relanded soon. (The fix however relies on ABI-breaking changes in V8 so it may or may not get released in v20.x)Update dependency jest to v29 (#108)
joyeecheung commentedon Oct 27, 2023
I think this can be closed now - for normal uses of vm.Script, #48510 should have fixed the leaks. For "leaks" caused by the use of
--expose-gcandgc()(which disables code aging), it was marked as wontfix by https://bugs.chromium.org/p/v8/issues/detail?id=12198, which I think is fair becausegc()is not a publicly supported feature, and it's more of a bug on the repro's side to use it as a way to "clean up memory thoroughly" (which can backfire).