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();
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...
An equivalent OOM is created if you substitute
Promise.anyforPromise.race...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
Additional information
If the promiseValue call incorporates an explicit scheduling on the event loop, there is no memory leak...
If the promiseValue call isn't composed via a Promise.race there is no leak...
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...