Repository: nodejs/node
Pull request: perf_hooks: fix stack overflow error

Alternative to https://github.com/nodejs/node/pull/54772, as suggested by https://github.com/nodejs/node/pull/54772#discussion_r1744625844
<!--
Before submitting a pull request, please read:

- the CONTRIBUTING guide at https://github.com/nodejs/node/blob/HEAD/CONTRIBUTING.md
- the commit message formatting guidelines at
  https://github.com/nodejs/node/blob/HEAD/doc/contributing/pull-requests.md#commit-message-guidelines

For code changes:
1. Include tests for any bug fixes or new features.
2. Update documentation if relevant.
3. Ensure that `make -j4 test` (UNIX), or `vcbuild test` (Windows) passes.

If you believe this PR should be highlighted in the Node.js CHANGELOG
please add the `notable-change` label.

Developer's Certificate of Origin 1.1

By making a contribution to this project, I certify that:

(a) The contribution was created in whole or in part by me and I
    have the right to submit it under the open source license
    indicated in the file; or

(b) The contribution is based upon previous work that, to the best
    of my knowledge, is covered under an appropriate open source
    license and I have the right under that license to submit that
    work with modifications, whether created in whole or in part
    by me, under the same open source license (unless I am
    permitted to submit under a different license), as indicated
    in the file; or

(c) The contribution was provided directly to me by some other
    person who certified (a), (b) or (c) and I have not modified
    it.

(d) I understand and agree that this project and the contribution
    are public and that a record of the contribution (including all
    personal information I submit with it, including my sign-off) is
    maintained indefinitely and may be redistributed consistent with
    this project or the open source license(s) involved.
-->


Changed files:
filename: lib/internal/per_context/primordials.js
patch: @@ -270,6 +270,8 @@ const {
   Array: ArrayConstructor,
   ArrayPrototypeForEach,
   ArrayPrototypeMap,
+  ArrayPrototypePushApply,
+  ArrayPrototypeSlice,
   FinalizationRegistry,
   FunctionPrototypeCall,
   Map,
@@ -720,5 +722,26 @@ primordials.SafeStringPrototypeSearch = (str, regexp) => {
   return match ? match.index : -1;
 };
 
+/**
+ * Variadic functions with lots of arguments will cause stack overflow errors.
+ * Use this function when `items` can be arbitrarily large, this function splits
+ * it into chunks of size 2**16 making stack overflow less likely.
+ * @param {Array<unknown>} arr
+ * @param {Parameters<typeof Array.prototype.push>} items
+ * @returns {ReturnType<typeof Array.prototype.push>}
+ */
+primordials.SafeArrayPrototypePushApply = (arr, items) => {
+  let end = 0x10000;
+  if (end < items.length) {
+    let start = 0;
+    do {
+      ArrayPrototypePushApply(arr, ArrayPrototypeSlice(items, start, start = end));
+      end += 0x10000;
+    } while (end < items.length);
+    items = ArrayPrototypeSlice(items, start);
+  }
+  return ArrayPrototypePushApply(arr, items);
+};
+
 ObjectSetPrototypeOf(primordials, null);
 ObjectFreeze(primordials);

filename: lib/internal/perf/observe.js
patch: @@ -6,14 +6,14 @@ const {
   ArrayPrototypeFilter,
   ArrayPrototypeIncludes,
   ArrayPrototypePush,
-  ArrayPrototypePushApply,
   ArrayPrototypeSlice,
   ArrayPrototypeSort,
   Error,
   MathMax,
   MathMin,
   ObjectDefineProperties,
   ObjectFreeze,
+  SafeArrayPrototypePushApply,
   SafeMap,
   SafeSet,
   Symbol,
@@ -300,7 +300,7 @@ class PerformanceObserver {
       maybeIncrementObserverCount(type);
       if (buffered) {
         const entries = filterBufferMapByNameAndType(undefined, type);
-        ArrayPrototypePushApply(this.#buffer, entries);
+        SafeArrayPrototypePushApply(this.#buffer, entries);
         kPending.add(this);
         if (kPending.size)
           queuePending();
@@ -507,9 +507,9 @@ function filterBufferMapByNameAndType(name, type) {
     return [];
   } else {
     bufferList = [];
-    ArrayPrototypePushApply(bufferList, markEntryBuffer);
-    ArrayPrototypePushApply(bufferList, measureEntryBuffer);
-    ArrayPrototypePushApply(bufferList, resourceTimingBuffer);
+    SafeArrayPrototypePushApply(bufferList, markEntryBuffer);
+    SafeArrayPrototypePushApply(bufferList, measureEntryBuffer);
+    SafeArrayPrototypePushApply(bufferList, resourceTimingBuffer);
   }
   if (name !== undefined) {
     bufferList = ArrayPrototypeFilter(bufferList, (buffer) => buffer.name === name);

filename: test/parallel/test-performance-many-marks.js
patch: @@ -0,0 +1,9 @@
+'use strict';
+require('../common');
+
+for (let i = 0; i < 1e6; i++) {
+  performance.mark(`mark-${i}`);
+}
+
+performance.getEntriesByName('mark-0');
+performance.clearMarks();

filename: test/parallel/test-primordials-apply.js
patch: @@ -10,6 +10,7 @@ const {
   ArrayPrototypeUnshiftApply,
   MathMaxApply,
   MathMinApply,
+  SafeArrayPrototypePushApply,
   StringPrototypeConcatApply,
   TypedArrayOfApply,
 } = require('internal/test/binding').primordials;
@@ -43,6 +44,26 @@ const {
   assert.deepStrictEqual(arr1, expected);
 }
 
+{
+  const arr1 = [1, 2, 3];
+  const arr2 = [4, 5, 6];
+
+  const expected = [...arr1, ...arr2];
+
+  assert.strictEqual(SafeArrayPrototypePushApply(arr1, arr2), expected.length);
+  assert.deepStrictEqual(arr1, expected);
+}
+
+{
+  const arr1 = [1, 2, 3];
+  const arr2 = Array.from({ length: 1e6 }, (_, i) => i);
+
+  const expected = [...arr1, ...arr2];
+
+  assert.strictEqual(SafeArrayPrototypePushApply(arr1, arr2), expected.length);
+  assert.deepStrictEqual(arr1, expected);
+}
+
 {
   const arr1 = [1, 2, 3];
   const arr2 = [4, 5, 6];

filename: typings/primordials.d.ts
patch: @@ -374,6 +374,7 @@ declare namespace primordials {
   export const RegExpPrototypeGetUnicode: UncurryGetter<typeof RegExp.prototype, "unicode">;
   export const RegExpPrototypeSymbolReplace: UncurryMethod<typeof RegExp.prototype, typeof Symbol.replace>
   export const RegExpPrototypeSymbolSplit: UncurryMethod<typeof RegExp.prototype, typeof Symbol.split>
+  export const SafeArrayPrototypePushApply: typeof ArrayPrototypePushApply;
   export import Set = globalThis.Set;
   export const SetLength: typeof Set.length
   export const SetName: typeof Set.name