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;
}