From 96dcb8458084977b00060668a7e604faf62aa0c7 Mon Sep 17 00:00:00 2001 From: James M Snell Date: Thu, 17 Sep 2026 20:31:58 +0000 Subject: [PATCH 1/3] perf_hooks: fix truncation of uvMetricsInfo counters libuv reports the event loop metrics exposed through `performance.nodeTiming.uvMetricsInfo` as `uint64_t` counters, but they were copied into an `Int32Array` (and, before that, converted using `v8::Integer::New()`), so they wrapped around after 2^31 on long-running processes. Store the metrics in a `Float64Array` instead. Values are now exact up to `Number.MAX_SAFE_INTEGER`. Assisted-by: OpenCode Signed-off-by: James M Snell --- src/node_perf.cc | 10 ++++++---- src/node_perf_common.h | 4 ++-- ...formance-nodetiming-uvmetricsinfo-buffer.js | 18 ++++++++++++++++++ ...est-performance-nodetiming-uvmetricsinfo.js | 5 ++++- typings/internalBinding/performance.d.ts | 2 +- 5 files changed, 31 insertions(+), 8 deletions(-) create mode 100644 test/parallel/test-performance-nodetiming-uvmetricsinfo-buffer.js diff --git a/src/node_perf.cc b/src/node_perf.cc index b4c74e9a09a7..d3348cd22fcb 100644 --- a/src/node_perf.cc +++ b/src/node_perf.cc @@ -280,10 +280,12 @@ void UvMetricsInfo(const FunctionCallbackInfo& args) { uv_metrics_t metrics; // uv_metrics_info always return 0 CHECK_EQ(uv_metrics_info(env->event_loop(), &metrics), 0); - AliasedInt32Array& buffer = env->performance_state()->uv_metrics; - buffer[0] = static_cast(metrics.loop_count); - buffer[1] = static_cast(metrics.events); - buffer[2] = static_cast(metrics.events_waiting); + // 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(metrics.loop_count); + buffer[1] = static_cast(metrics.events); + buffer[2] = static_cast(metrics.events_waiting); } void CreateELDHistogram(const FunctionCallbackInfo& args) { diff --git a/src/node_perf_common.h b/src/node_perf_common.h index aa84ba55b08e..3aa228a501dd 100644 --- a/src/node_perf_common.h +++ b/src/node_perf_common.h @@ -79,7 +79,7 @@ class PerformanceState { AliasedUint8Array root; AliasedFloat64Array milestones; AliasedUint32Array observers; - AliasedInt32Array uv_metrics; + AliasedFloat64Array uv_metrics; uint64_t performance_last_gc_start_mark = 0; uint16_t current_gc_type = 0; @@ -93,8 +93,8 @@ class PerformanceState { struct performance_state_internal { // doubles first so that they are always sizeof(double)-aligned double milestones[NODE_PERFORMANCE_MILESTONE_INVALID]; + double uv_metrics[3]; uint32_t observers[NODE_PERFORMANCE_ENTRY_TYPE_INVALID]; - int32_t uv_metrics[3]; }; }; diff --git a/test/parallel/test-performance-nodetiming-uvmetricsinfo-buffer.js b/test/parallel/test-performance-nodetiming-uvmetricsinfo-buffer.js new file mode 100644 index 000000000000..4882f946211b --- /dev/null +++ b/test/parallel/test-performance-nodetiming-uvmetricsinfo-buffer.js @@ -0,0 +1,18 @@ +// Flags: --expose-internals +'use strict'; + +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 +// used to transfer them to JavaScript must not truncate them to 32 bits. +const { uvMetricsBuffer, uvMetricsInfo } = internalBinding('performance'); +assert.ok(uvMetricsBuffer instanceof Float64Array); +assert.strictEqual(uvMetricsBuffer.length, 3); + +uvMetricsInfo(); +for (const value of uvMetricsBuffer) { + assert.ok(Number.isSafeInteger(value), `${value} is not a safe integer`); + assert.ok(value >= 0, `${value} is negative`); +} diff --git a/test/parallel/test-performance-nodetiming-uvmetricsinfo.js b/test/parallel/test-performance-nodetiming-uvmetricsinfo.js index b67682b0ff35..c7b1f7481b70 100644 --- a/test/parallel/test-performance-nodetiming-uvmetricsinfo.js +++ b/test/parallel/test-performance-nodetiming-uvmetricsinfo.js @@ -13,10 +13,13 @@ const fixtures = require('../common/fixtures'); const file = fixtures.path('test-nodetiming-uvmetricsinfo.js'); -{ +// Run both with and without the built-in startup snapshot, as the +// performance state buffers are initialized differently in each case. +for (const execArgv of [[], ['--no-node-snapshot']]) { const { status, stderr } = spawnSync( process.execPath, [ + ...execArgv, file, ], ); diff --git a/typings/internalBinding/performance.d.ts b/typings/internalBinding/performance.d.ts index cf3ef0a664f0..26c5335321d3 100644 --- a/typings/internalBinding/performance.d.ts +++ b/typings/internalBinding/performance.d.ts @@ -146,6 +146,6 @@ export interface PerformanceBinding { ): InternalPerformanceBinding.ELDHistogram; markBootstrapComplete(): void; uvMetricsInfo(): void; - uvMetricsBuffer: Int32Array; + uvMetricsBuffer: Float64Array; now(): number; } From a90f99b52a3d5cdb90aa83a02e52c693f925f498 Mon Sep 17 00:00:00 2001 From: James M Snell Date: Thu, 17 Sep 2026 20:36:32 +0000 Subject: [PATCH 2/3] doc: use Type for performanceNodeTiming.uvMetricsInfo `performanceNodeTiming.uvMetricsInfo` is a property, not a method, so document its value using `Type:` rather than `Returns:`. Assisted-by: OpenCode Signed-off-by: James M Snell --- doc/api/perf_hooks.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/api/perf_hooks.md b/doc/api/perf_hooks.md index 69a4d6613b9b..e044f578f4fa 100644 --- a/doc/api/perf_hooks.md +++ b/doc/api/perf_hooks.md @@ -854,7 +854,7 @@ added: - v20.18.0 --> -* Returns: {Object} +* Type: {Object} * `loopCount` {number} Number of event loop iterations. * `events` {number} Number of events that have been processed by the event handler. * `eventsWaiting` {number} Number of events that were waiting to be processed when the event provider was called. From fbab3f2808575b99764359469faa3e4dc7dceed8 Mon Sep 17 00:00:00 2001 From: James M Snell Date: Thu, 17 Sep 2026 20:46:06 +0000 Subject: [PATCH 3/3] 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 --- .../perf_hooks/nodetiming-uvmetricsinfo.js | 19 ++++++--- doc/api/perf_hooks.md | 41 +++++++++++++++++++ lib/internal/perf/nodetiming.js | 20 +++++++++ src/aliased_buffer.h | 3 +- src/node_perf.cc | 36 ++++++++++++---- src/node_perf_common.h | 5 ++- src/node_snapshotable.cc | 3 ++ .../fixtures/test-nodetiming-uvmetricsinfo.js | 28 ++++++++++++- ...ormance-nodetiming-uvmetricsinfo-buffer.js | 14 +++++-- ...ormance-nodetiming-uvmetricsinfo-worker.js | 26 ++++++++++++ test/parallel/test-performance-nodetiming.js | 20 +++++++++ typings/internalBinding/performance.d.ts | 1 + 12 files changed, 197 insertions(+), 19 deletions(-) create mode 100644 test/parallel/test-performance-nodetiming-uvmetricsinfo-worker.js diff --git a/benchmark/perf_hooks/nodetiming-uvmetricsinfo.js b/benchmark/perf_hooks/nodetiming-uvmetricsinfo.js index 1d8d174de14f..d646631f4e9c 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 e044f578f4fa..6b4ef8582a06 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` + + + +* 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`