Repository: node
Issue: Racing immediately-resolving Promises leads to memory leak
Labels: confirmed-bug,performance,promises

### 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
```

```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`...

```js
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

```sh
✗ 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...

```js
// 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...

```js
// 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...


```js
// 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();
```