Description
Version
21.5.0
Platform
Linux penguin 6.1.55-06877-gc83437f2949f #1 SMP PREEMPT_DYNAMIC Thu Dec 14 19:17:39 PST 2023 x86_64 GNU/Linux
Subsystem
async_hooks or async/await
What steps will reproduce the bug?
Run the following code. Failure is quicker and less likely to interfere with system stability if you run with a low heap ceiling like this, but it will fail without...
node --max-old-space-size=16 test/examples/ephemeralPromiseMemoryLeak.js
//ephemeralPromiseMemoryLeak.js
async function promiseValue(value) {
return value;
}
async function run() {
for (;;) {
await Promise.race([promiseValue("foo"), promiseValue("bar")]);
}
}
run();
An equivalent OOM is created if you substitute Promise.any
for Promise.race
...
async function promiseValue(value) {
return value;
}
async function run() {
for (;;) {
await Promise.any([promiseValue("foo"), promiseValue("bar")]);
}
}
run();
How often does it reproduce? Is there a required condition?
It always fails.
What is the expected behavior? Why is that the expected behavior?
I would expect it not to accumulate references in memory and fail.
What do you see instead?
Fails with the following error
✗ node test/examples/ephemeralPromiseMemoryLeak.js
<--- Last few GCs --->
[31511:0x6de4330] 39874 ms: Mark-Compact (reduce) 2048.2 (2083.6) -> 2047.3 (2083.9) MB, 1308.67 / 0.00 ms (average mu = 0.071, current mu = 0.001) allocation failure; scavenge might not succeed
<--- JS stacktrace --->
FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory
----- Native stack trace -----
1: 0xcc062a node::OOMErrorHandler(char const*, v8::OOMDetails const&) [node]
2: 0x104eb90 v8::Utils::ReportOOMFailure(v8::internal::Isolate*, char const*, v8::OOMDetails const&) [node]
3: 0x104ee77 v8::internal::V8::FatalProcessOutOfMemory(v8::internal::Isolate*, char const*, v8::OOMDetails const&) [node]
4: 0x126e0b5 [node]
5: 0x126e58e [node]
6: 0x12837b6 v8::internal::Heap::PerformGarbageCollection(v8::internal::GarbageCollector, v8::internal::GarbageCollectionReason, char const*) [node]
7: 0x12842d9 [node]
8: 0x12848e8 [node]
9: 0x19d4311 [node]
[1] 31511 abort (core dumped) node test/examples/ephemeralPromiseMemoryLeak.js
Additional information
If the promiseValue call incorporates an explicit scheduling on the event loop, there is no memory leak...
// setImmediateNoLeak.js
function promiseValue(value) {
return new Promise((resolve) => {
setImmediate(() => resolve(value));
});
}
async function run() {
for (;;) {
await Promise.race([promiseValue("foo"), promiseValue("bar")]);
}
}
run();
If the promiseValue call isn't composed via a Promise.race there is no leak...
// noRaceNoLeak.js
async function promiseValue(value) {
return value;
}
async function run() {
for (;;) {
await promiseValue("foo");
await promiseValue("bar");
}
}
run();
Maybe obviously, but putting it here for completeness, if you don't use an async await loop, but compose the loop itself with setImmediate there is no leak...
// noLoopNoLeak.js
async function promiseValue(value) {
return value;
}
async function doRace() {
await Promise.race([promiseValue("foo"), promiseValue("bar")]);
}
function run() {
doRace().then(() => setImmediate(run));
}
run();