Repository: nodejs/node
Pull request: process: optimize asyncHandledRejections by using FixedQueue

branch:

```sh
./node benchmark/run.js --filter handled-rejections process

process/handled-rejections.js
process/handled-rejections.js n=10000: 237,537.91758393912
process/handled-rejections.js n=50000: 306,685.9060958424
process/handled-rejections.js n=100000: 324,124.4183992467
```

main:

```sh
./node benchmark/run.js --filter handled-rejections process

process/handled-rejections.js
process/handled-rejections.js n=10000: 148,219.51309889948
process/handled-rejections.js n=50000: 42,906.982381963964
process/handled-rejections.js n=100000: 23,234.612142965194
```

Using a regular Array for a queue that only shifts elements is inefficient due to O(N) time complexity per shift operation. FixedQueue provides O(1) dequeue performance

In the benchmark, to really highlight the effect, I used the `--unhandled-rejections=warn` flag since it uses the queue even more 

Changed files:
filename: benchmark/process/handled-rejections.js
patch: @@ -0,0 +1,43 @@
+'use strict';
+
+const common = require('../common.js');
+
+// Benchmarks the throughput of processing many promise rejections that are
+// initially unhandled, get warned, and then handled asynchronously, exercising
+// asyncHandledRejections + processPromiseRejections.
+//
+// Note: This benchmark uses --unhandled-rejections=warn to avoid crashing
+// when promises are temporarily unhandled.
+
+const bench = common.createBenchmark(main, {
+  n: [1e4, 5e4, 1e5],
+}, {
+  flags: ['--unhandled-rejections=warn'],
+});
+
+function main({ n }) {
+  const rejections = [];
+
+  // Suppress warning output during the benchmark
+  process.removeAllListeners('warning');
+
+  for (let i = 0; i < n; i++) {
+    rejections.push(Promise.reject(i));
+  }
+
+  // Wait for them to be processed as unhandled and warned.
+  setImmediate(() => {
+    setImmediate(() => {
+      bench.start();
+
+      for (let i = 0; i < n; i++) {
+        rejections[i].catch(() => {});
+      }
+
+      // Let processPromiseRejections drain asyncHandledRejections.
+      setImmediate(() => {
+        bench.end(n);
+      });
+    });
+  });
+}

filename: lib/internal/process/promises.js
patch: @@ -1,14 +1,14 @@
 'use strict';
 
 const {
-  ArrayPrototypePush,
-  ArrayPrototypeShift,
   Error,
   ObjectPrototypeHasOwnProperty,
   SafeMap,
   SafeWeakMap,
 } = primordials;
 
+const FixedQueue = require('internal/fixed_queue');
+
 const {
   tickInfo,
   promiseRejectEvents: {
@@ -119,9 +119,9 @@ const maybeUnhandledPromises = new SafeWeakMap();
 let pendingUnhandledRejections = new SafeMap();
 
 /**
- * @type {Array<{promise: Promise, warning: Error}>}
+ * @type {import('internal/fixed_queue')<{promise: Promise, warning: Error}>}
  */
-const asyncHandledRejections = [];
+const asyncHandledRejections = new FixedQueue();
 
 /**
  * @type {number}
@@ -219,7 +219,7 @@ function handledRejection(promise) {
     if (promiseInfo.warned) {
       // Generate the warning object early to get a good stack trace.
       const warning = new PromiseRejectionHandledWarning(promiseInfo.uid);
-      ArrayPrototypePush(asyncHandledRejections, { promise, warning });
+      asyncHandledRejections.push({ promise, warning });
       setHasRejectionToWarn(true);
     }
   }
@@ -375,10 +375,10 @@ function getUnhandledRejectionsMode() {
 // a warning to be emitted which requires the microtask and next tick
 // queues to be drained again.
 function processPromiseRejections() {
-  let maybeScheduledTicksOrMicrotasks = asyncHandledRejections.length > 0;
+  let maybeScheduledTicksOrMicrotasks = !asyncHandledRejections.isEmpty();
 
-  while (asyncHandledRejections.length !== 0) {
-    const { promise, warning } = ArrayPrototypeShift(asyncHandledRejections);
+  while (!asyncHandledRejections.isEmpty()) {
+    const { promise, warning } = asyncHandledRejections.shift();
     if (!process.emit('rejectionHandled', promise)) {
       process.emitWarning(warning);
     }