Commit 97af3d7da56 for nodejs

commit 97af3d7da562406b75d1baeecb79ef8105d4b92a
Author: James M Snell <jasnell@gmail.com>
Date:   Thu Sep 17 20:46:06 2026 +0000

    perf_hooks: add performanceNodeTiming.uvMetricsInfoBigInt

    `performance.nodeTiming.uvMetricsInfo` returns the libuv event loop
    metrics as numbers, which are only exact up to
    `Number.MAX_SAFE_INTEGER`. Add `uvMetricsInfoBigInt`, which returns
    the same metrics as bigints backed by `uint64_t` storage, carrying the
    full 64-bit range reported by libuv.

    A single native call fills both a `Float64Array` and a
    `BigUint64Array`, so `uvMetricsInfo` does not pay for bigint
    allocation and conversion. The new property is omitted from
    `toJSON()`, as `JSON.stringify()` cannot serialize bigints.

    Assisted-by: OpenCode
    Signed-off-by: James M Snell <jasnell@gmail.com>
    PR-URL: https://github.com/nodejs/node/pull/66094
    Reviewed-By: Rafael Gonzaga <rafael.nunu@hotmail.com>
    Reviewed-By: Chengzhong Wu <legendecas@gmail.com>

diff --git a/benchmark/perf_hooks/nodetiming-uvmetricsinfo.js b/benchmark/perf_hooks/nodetiming-uvmetricsinfo.js
index 1d8d174de14..d646631f4e9 100644
--- a/benchmark/perf_hooks/nodetiming-uvmetricsinfo.js
+++ b/benchmark/perf_hooks/nodetiming-uvmetricsinfo.js
@@ -11,6 +11,7 @@ const {
 const bench = common.createBenchmark(main, {
   n: [1e6],
   events: [1, 1000, 10000],
+  api: ['number', 'bigint'],
 });

 async function runEvents(events) {
@@ -19,11 +20,19 @@ async function runEvents(events) {
   }
 }

-async function main({ n, events }) {
+async function main({ n, events, api }) {
   await runEvents(events);
-  bench.start();
-  for (let i = 0; i < n; i++) {
-    assert.ok(performance.nodeTiming.uvMetricsInfo);
+  if (api === 'bigint') {
+    bench.start();
+    for (let i = 0; i < n; i++) {
+      assert.ok(performance.nodeTiming.uvMetricsInfoBigInt);
+    }
+    bench.end(n);
+  } else {
+    bench.start();
+    for (let i = 0; i < n; i++) {
+      assert.ok(performance.nodeTiming.uvMetricsInfo);
+    }
+    bench.end(n);
   }
-  bench.end(n);
 }
diff --git a/doc/api/perf_hooks.md b/doc/api/perf_hooks.md
index e044f578f4f..6b4ef8582a0 100644
--- a/doc/api/perf_hooks.md
+++ b/doc/api/perf_hooks.md
@@ -862,6 +862,10 @@ added:
 This is a wrapper to the `uv_metrics_info` function.
 It returns the current set of event loop metrics.

+The values are exact up to `Number.MAX_SAFE_INTEGER`. Use
+[`performanceNodeTiming.uvMetricsInfoBigInt`][] to obtain the full 64-bit
+values reported by libuv.
+
 It is recommended to use this property inside a function whose execution was
 scheduled using `setImmediate` to avoid collecting metrics before finishing all
 operations scheduled during the current loop iteration.
@@ -882,6 +886,41 @@ setImmediate(() => {
 });
 ```

+### `performanceNodeTiming.uvMetricsInfoBigInt`
+
+<!-- YAML
+added: REPLACEME
+-->
+
+* Type: {Object}
+  * `loopCount` {bigint} Number of event loop iterations.
+  * `events` {bigint} Number of events that have been processed by the event handler.
+  * `eventsWaiting` {bigint} Number of events that were waiting to be processed when the event provider was called.
+
+The same as [`performanceNodeTiming.uvMetricsInfo`][], except that the values
+are {bigint}s carrying the full 64-bit range reported by libuv.
+
+Because `JSON.stringify()` cannot serialize {bigint} values, this property is
+not enumerable and is not included in the output of
+`performanceNodeTiming.toJSON()`. Copies of `performance.nodeTiming` made by
+spreading its enumerable properties, for example, remain serializable.
+
+```cjs
+const { performance } = require('node:perf_hooks');
+
+setImmediate(() => {
+  console.log(performance.nodeTiming.uvMetricsInfoBigInt);
+});
+```
+
+```mjs
+import { performance } from 'node:perf_hooks';
+
+setImmediate(() => {
+  console.log(performance.nodeTiming.uvMetricsInfoBigInt);
+});
+```
+
 ### `performanceNodeTiming.v8Start`

 <!-- YAML
@@ -3263,6 +3302,8 @@ dns.promises.resolve('localhost');
 [`perf_hooks.importHistogram()`]: #perf_hooksimporthistogramdata
 [`perf_hooks.monitorEventLoopDelay()`]: #perf_hooksmonitoreventloopdelayoptions
 [`perf_hooks.timerify()`]: #perf_hookstimerifyfn-options
+[`performanceNodeTiming.uvMetricsInfoBigInt`]: #performancenodetiminguvmetricsinfobigint
+[`performanceNodeTiming.uvMetricsInfo`]: #performancenodetiminguvmetricsinfo
 [`process.hrtime()`]: process.md#processhrtimetime
 [`timeOrigin`]: https://w3c.github.io/hr-time/#dom-performance-timeorigin
 [`window.performance.toJSON`]: https://developer.mozilla.org/en-US/docs/Web/API/Performance/toJSON
diff --git a/lib/internal/perf/nodetiming.js b/lib/internal/perf/nodetiming.js
index 5de5e3e6644..0c846f2bfa6 100644
--- a/lib/internal/perf/nodetiming.js
+++ b/lib/internal/perf/nodetiming.js
@@ -30,6 +30,7 @@ const {
   loopIdleTime,
   uvMetricsInfo,
   uvMetricsBuffer,
+  uvMetricsBigIntBuffer,
 } = internalBinding('performance');

 class PerformanceNodeTiming {
@@ -138,6 +139,23 @@ class PerformanceNodeTiming {
           };
         },
       },
+
+      // Not enumerable, so that copying the enumerable properties, e.g. with
+      // `{ ...performance.nodeTiming }`, does not produce an object that
+      // JSON.stringify() cannot serialize.
+      uvMetricsInfoBigInt: {
+        __proto__: null,
+        enumerable: false,
+        configurable: true,
+        get: () => {
+          uvMetricsInfo();
+          return {
+            loopCount: uvMetricsBigIntBuffer[0],
+            events: uvMetricsBigIntBuffer[1],
+            eventsWaiting: uvMetricsBigIntBuffer[2],
+          };
+        },
+      },
     });
   }

@@ -153,6 +171,8 @@ class PerformanceNodeTiming {
   }

   toJSON() {
+    // uvMetricsInfoBigInt is intentionally omitted: JSON.stringify() cannot
+    // serialize bigint values.
     return {
       name: 'node',
       entryType: 'node',
diff --git a/src/aliased_buffer.h b/src/aliased_buffer.h
index ff2b961724d..568398ef764 100644
--- a/src/aliased_buffer.h
+++ b/src/aliased_buffer.h
@@ -191,7 +191,8 @@ class AliasedBufferBase final : public MemoryRetainer {
   V(uint32_t, Uint32Array)                                                     \
   V(float, Float32Array)                                                       \
   V(double, Float64Array)                                                      \
-  V(int64_t, BigInt64Array)
+  V(int64_t, BigInt64Array)                                                    \
+  V(uint64_t, BigUint64Array)

 #define V(NativeT, V8T)                                                        \
   typedef AliasedBufferBase<NativeT, v8::V8T> Aliased##V8T;
diff --git a/src/node_perf.cc b/src/node_perf.cc
index d3348cd22fc..fea9c8e7cde 100644
--- a/src/node_perf.cc
+++ b/src/node_perf.cc
@@ -61,7 +61,12 @@ PerformanceState::PerformanceState(Isolate* isolate,
                  offsetof(performance_state_internal, uv_metrics),
                  3,
                  root,
-                 MAYBE_FIELD_PTR(info, uv_metrics)) {
+                 MAYBE_FIELD_PTR(info, uv_metrics)),
+      uv_metrics_bigint(isolate,
+                        offsetof(performance_state_internal, uv_metrics_bigint),
+                        3,
+                        root,
+                        MAYBE_FIELD_PTR(info, uv_metrics_bigint)) {
   if (info == nullptr) {
     // For performance states initialized from scratch, reset
     // all the milestones and initialize the time origin.
@@ -89,11 +94,15 @@ PerformanceState::SerializeInfo PerformanceState::Serialize(
   for (size_t i = 0; i < uv_metrics.Length(); ++i) {
     uv_metrics[i] = 0;
   }
+  for (size_t i = 0; i < uv_metrics_bigint.Length(); ++i) {
+    uv_metrics_bigint[i] = 0;
+  }

   SerializeInfo info{root.Serialize(context, creator),
                      milestones.Serialize(context, creator),
                      observers.Serialize(context, creator),
-                     uv_metrics.Serialize(context, creator)};
+                     uv_metrics.Serialize(context, creator),
+                     uv_metrics_bigint.Serialize(context, creator)};
   return info;
 }

@@ -116,6 +125,7 @@ void PerformanceState::Deserialize(v8::Local<v8::Context> context,
   milestones.Deserialize(context);
   observers.Deserialize(context);
   uv_metrics.Deserialize(context);
+  uv_metrics_bigint.Deserialize(context);

   // Re-initialize the time origin and timestamp i.e. the process start time.
   Initialize(time_origin, time_origin_timestamp);
@@ -128,6 +138,7 @@ std::ostream& operator<<(std::ostream& o,
     << "  " << i.milestones << ",  // milestones\n"
     << "  " << i.observers << ",  // observers\n"
     << "  " << i.uv_metrics << ",  // uv_metrics\n"
+    << "  " << i.uv_metrics_bigint << ",  // uv_metrics_bigint\n"
     << "}";
   return o;
 }
@@ -280,12 +291,16 @@ void UvMetricsInfo(const FunctionCallbackInfo<Value>& args) {
   uv_metrics_t metrics;
   // uv_metrics_info always return 0
   CHECK_EQ(uv_metrics_info(env->event_loop(), &metrics), 0);
-  // libuv reports 64-bit counters. Store them as doubles so that they are
-  // exact up to Number.MAX_SAFE_INTEGER instead of wrapping at 2^31.
-  AliasedFloat64Array& buffer = env->performance_state()->uv_metrics;
-  buffer[0] = static_cast<double>(metrics.loop_count);
-  buffer[1] = static_cast<double>(metrics.events);
-  buffer[2] = static_cast<double>(metrics.events_waiting);
+  // libuv reports 64-bit counters. The doubles backing uvMetricsInfo are
+  // exact up to Number.MAX_SAFE_INTEGER, while the uint64_t values backing
+  // uvMetricsInfoBigInt carry the full range.
+  PerformanceState* state = env->performance_state();
+  const uint64_t values[] = {
+      metrics.loop_count, metrics.events, metrics.events_waiting};
+  for (size_t i = 0; i < arraysize(values); ++i) {
+    state->uv_metrics[i] = static_cast<double>(values[i]);
+    state->uv_metrics_bigint[i] = values[i];
+  }
 }

 void CreateELDHistogram(const FunctionCallbackInfo<Value>& args) {
@@ -382,6 +397,11 @@ void CreatePerContextProperties(Local<Object> target,
             FIXED_ONE_BYTE_STRING(isolate, "uvMetricsBuffer"),
             state->uv_metrics.GetJSArray())
       .Check();
+  target
+      ->Set(context,
+            FIXED_ONE_BYTE_STRING(isolate, "uvMetricsBigIntBuffer"),
+            state->uv_metrics_bigint.GetJSArray())
+      .Check();

   Local<Object> constants = Object::New(isolate);

diff --git a/src/node_perf_common.h b/src/node_perf_common.h
index 3aa228a501d..43ff1122d2a 100644
--- a/src/node_perf_common.h
+++ b/src/node_perf_common.h
@@ -63,6 +63,7 @@ class PerformanceState {
     AliasedBufferIndex milestones;
     AliasedBufferIndex observers;
     AliasedBufferIndex uv_metrics;
+    AliasedBufferIndex uv_metrics_bigint;
   };

   explicit PerformanceState(v8::Isolate* isolate,
@@ -80,6 +81,7 @@ class PerformanceState {
   AliasedFloat64Array milestones;
   AliasedUint32Array observers;
   AliasedFloat64Array uv_metrics;
+  AliasedBigUint64Array uv_metrics_bigint;

   uint64_t performance_last_gc_start_mark = 0;
   uint16_t current_gc_type = 0;
@@ -91,9 +93,10 @@ class PerformanceState {
   void Initialize(uint64_t time_origin, double time_origin_timestamp);
   void ResetMilestones();
   struct performance_state_internal {
-    // doubles first so that they are always sizeof(double)-aligned
+    // 64-bit fields first so that they are always 8-byte aligned
     double milestones[NODE_PERFORMANCE_MILESTONE_INVALID];
     double uv_metrics[3];
+    uint64_t uv_metrics_bigint[3];
     uint32_t observers[NODE_PERFORMANCE_ENTRY_TYPE_INVALID];
   };
 };
diff --git a/src/node_snapshotable.cc b/src/node_snapshotable.cc
index fa1de1917e9..faf95133bf4 100644
--- a/src/node_snapshotable.cc
+++ b/src/node_snapshotable.cc
@@ -408,6 +408,7 @@ size_t SnapshotSerializer::Write(const ImmediateInfo::SerializeInfo& data) {
 // [ 4/8 bytes ]  snapshot index of milestones
 // [ 4/8 bytes ]  snapshot index of observers
 // [ 4/8 bytes ]  snapshot index of uv_metrics
+// [ 4/8 bytes ]  snapshot index of uv_metrics_bigint
 template <>
 performance::PerformanceState::SerializeInfo SnapshotDeserializer::Read() {
   Debug("Read<PerformanceState::SerializeInfo>()\n");
@@ -417,6 +418,7 @@ performance::PerformanceState::SerializeInfo SnapshotDeserializer::Read() {
   result.milestones = ReadArithmetic<AliasedBufferIndex>();
   result.observers = ReadArithmetic<AliasedBufferIndex>();
   result.uv_metrics = ReadArithmetic<AliasedBufferIndex>();
+  result.uv_metrics_bigint = ReadArithmetic<AliasedBufferIndex>();
   if (is_debug) {
     std::string str = ToStr(result);
     Debug("Read<PerformanceState::SerializeInfo>() %s\n", str);
@@ -436,6 +438,7 @@ size_t SnapshotSerializer::Write(
   written_total += WriteArithmetic<AliasedBufferIndex>(data.milestones);
   written_total += WriteArithmetic<AliasedBufferIndex>(data.observers);
   written_total += WriteArithmetic<AliasedBufferIndex>(data.uv_metrics);
+  written_total += WriteArithmetic<AliasedBufferIndex>(data.uv_metrics_bigint);

   Debug("Write<PerformanceState::SerializeInfo>() wrote %d bytes\n",
         written_total);
diff --git a/test/fixtures/test-nodetiming-uvmetricsinfo.js b/test/fixtures/test-nodetiming-uvmetricsinfo.js
index 038ca8b7990..7df75389166 100644
--- a/test/fixtures/test-nodetiming-uvmetricsinfo.js
+++ b/test/fixtures/test-nodetiming-uvmetricsinfo.js
@@ -13,6 +13,8 @@ function safeMetricsInfo(cb) {
   });
 }

+const kZeroBigInt = { loopCount: 0n, events: 0n, eventsWaiting: 0n };
+
 {
   const info = nodeTiming.uvMetricsInfo;
   assert.strictEqual(info.loopCount, 0);
@@ -21,6 +23,7 @@ function safeMetricsInfo(cb) {
   // Adding checks for this property will make the test flaky
   // as it can be highly influenced by race conditions.
   assert.strictEqual(info.eventsWaiting, 0);
+  assert.deepStrictEqual(nodeTiming.uvMetricsInfoBigInt, kZeroBigInt);
 }

 {
@@ -31,24 +34,47 @@ function safeMetricsInfo(cb) {
   assert.strictEqual(info.loopCount, 0);
   assert.strictEqual(info.events, 0);
   assert.strictEqual(info.eventsWaiting, 0);
+  assert.deepStrictEqual(nodeTiming.uvMetricsInfoBigInt, kZeroBigInt);
 }

 {
   function openFile(info) {
     assert.strictEqual(info.loopCount, 1);
+    const infoBigInt = nodeTiming.uvMetricsInfoBigInt;
+    assert.strictEqual(infoBigInt.loopCount, 1n);

     fs.open(__filename, 'r', (err) => {
       assert.ifError(err);
     });

     const saved = { ...info };
+    const savedBigInt = { ...infoBigInt };
     safeMetricsInfo((nextInfo) => {
       assert.notStrictEqual(nextInfo, info);
       assert.ok(nextInfo.loopCount > saved.loopCount);
-      // Updating the shared buffer must not change earlier results.
+      const nextInfoBigInt = nodeTiming.uvMetricsInfoBigInt;
+      assert.notStrictEqual(nextInfoBigInt, infoBigInt);
+      assert.ok(nextInfoBigInt.loopCount > savedBigInt.loopCount);
+      // Updating the shared buffers must not change earlier results.
       assert.deepStrictEqual(info, saved);
+      assert.deepStrictEqual(infoBigInt, savedBigInt);
     });
   }

   safeMetricsInfo(openFile);
 }
+
+{
+  // Both representations are filled by the same native call, and libuv only
+  // updates the metrics while the event loop is running, so back-to-back
+  // synchronous reads must agree.
+  safeMetricsInfo(() => {
+    const info = nodeTiming.uvMetricsInfo;
+    const infoBigInt = nodeTiming.uvMetricsInfoBigInt;
+    for (const key of ['loopCount', 'events', 'eventsWaiting']) {
+      assert.strictEqual(typeof info[key], 'number');
+      assert.strictEqual(typeof infoBigInt[key], 'bigint');
+      assert.strictEqual(BigInt(info[key]), infoBigInt[key]);
+    }
+  });
+}
diff --git a/test/parallel/test-performance-nodetiming-uvmetricsinfo-buffer.js b/test/parallel/test-performance-nodetiming-uvmetricsinfo-buffer.js
index 4882f946211..41d80607841 100644
--- a/test/parallel/test-performance-nodetiming-uvmetricsinfo-buffer.js
+++ b/test/parallel/test-performance-nodetiming-uvmetricsinfo-buffer.js
@@ -5,14 +5,22 @@ require('../common');
 const assert = require('node:assert');
 const { internalBinding } = require('internal/test/binding');

-// The event loop metrics reported by libuv are 64-bit counters. The buffer
+// The event loop metrics reported by libuv are 64-bit counters. The buffers
 // used to transfer them to JavaScript must not truncate them to 32 bits.
-const { uvMetricsBuffer, uvMetricsInfo } = internalBinding('performance');
+const {
+  uvMetricsBuffer,
+  uvMetricsBigIntBuffer,
+  uvMetricsInfo,
+} = internalBinding('performance');
 assert.ok(uvMetricsBuffer instanceof Float64Array);
 assert.strictEqual(uvMetricsBuffer.length, 3);
+assert.ok(uvMetricsBigIntBuffer instanceof BigUint64Array);
+assert.strictEqual(uvMetricsBigIntBuffer.length, 3);

 uvMetricsInfo();
-for (const value of uvMetricsBuffer) {
+for (let i = 0; i < uvMetricsBuffer.length; i++) {
+  const value = uvMetricsBuffer[i];
   assert.ok(Number.isSafeInteger(value), `${value} is not a safe integer`);
   assert.ok(value >= 0, `${value} is negative`);
+  assert.strictEqual(BigInt(value), uvMetricsBigIntBuffer[i]);
 }
diff --git a/test/parallel/test-performance-nodetiming-uvmetricsinfo-worker.js b/test/parallel/test-performance-nodetiming-uvmetricsinfo-worker.js
new file mode 100644
index 00000000000..236ba80a7b7
--- /dev/null
+++ b/test/parallel/test-performance-nodetiming-uvmetricsinfo-worker.js
@@ -0,0 +1,26 @@
+'use strict';
+
+const common = require('../common');
+const assert = require('node:assert');
+const { Worker } = require('node:worker_threads');
+
+// The event loop metrics are tracked per event loop, so they are also
+// available in worker threads.
+const worker = new Worker(`
+  const { parentPort } = require('node:worker_threads');
+  const { performance } = require('node:perf_hooks');
+  setImmediate(() => {
+    const info = performance.nodeTiming.uvMetricsInfo;
+    const infoBigInt = performance.nodeTiming.uvMetricsInfoBigInt;
+    parentPort.postMessage({ info, infoBigInt });
+  });
+`, { eval: true });
+
+worker.on('message', common.mustCall(({ info, infoBigInt }) => {
+  for (const key of ['loopCount', 'events', 'eventsWaiting']) {
+    assert.strictEqual(typeof info[key], 'number');
+    assert.strictEqual(typeof infoBigInt[key], 'bigint');
+    assert.strictEqual(BigInt(info[key]), infoBigInt[key]);
+  }
+  assert.ok(infoBigInt.loopCount > 0n);
+}));
diff --git a/test/parallel/test-performance-nodetiming.js b/test/parallel/test-performance-nodetiming.js
index cc76c80a647..c066131c244 100644
--- a/test/parallel/test-performance-nodetiming.js
+++ b/test/parallel/test-performance-nodetiming.js
@@ -10,6 +10,26 @@ assert.strictEqual(nodeTiming.name, 'node');
 assert.strictEqual(nodeTiming.entryType, 'node');

 assert.strictEqual(nodeTiming.startTime, 0);
+
+// uvMetricsInfoBigInt holds bigint values, which JSON.stringify() cannot
+// serialize, so it must not be part of the JSON representation, nor be copied
+// along with the enumerable properties.
+assert.strictEqual(typeof nodeTiming.uvMetricsInfoBigInt.loopCount, 'bigint');
+assert.strictEqual(
+  Object.getOwnPropertyDescriptor(nodeTiming, 'uvMetricsInfoBigInt').enumerable,
+  false);
+assert.strictEqual(
+  Object.hasOwn(JSON.parse(JSON.stringify(nodeTiming)), 'uvMetricsInfoBigInt'),
+  false);
+assert.strictEqual(
+  Object.hasOwn(JSON.parse(JSON.stringify(performance)).nodeTiming,
+                'uvMetricsInfoBigInt'),
+  false);
+{
+  const copy = { ...nodeTiming };
+  assert.strictEqual(Object.hasOwn(copy, 'uvMetricsInfoBigInt'), false);
+  assert.strictEqual(typeof JSON.stringify(copy), 'string');
+}
 const now = performance.now();
 assert.ok(nodeTiming.duration >= now);

diff --git a/typings/internalBinding/performance.d.ts b/typings/internalBinding/performance.d.ts
index 26c5335321d..4bd3a9b805e 100644
--- a/typings/internalBinding/performance.d.ts
+++ b/typings/internalBinding/performance.d.ts
@@ -147,5 +147,6 @@ export interface PerformanceBinding {
   markBootstrapComplete(): void;
   uvMetricsInfo(): void;
   uvMetricsBuffer: Float64Array;
+  uvMetricsBigIntBuffer: BigUint64Array;
   now(): number;
 }