Commit adc26bc82f5 for nodejs
commit adc26bc82f5835dca7b89326cd3cfa85f29eb358
Author: James M Snell <jasnell@gmail.com>
Date: Thu Sep 17 20:31:58 2026 +0000
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 <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/src/node_perf.cc b/src/node_perf.cc
index b4c74e9a09a..d3348cd22fc 100644
--- a/src/node_perf.cc
+++ b/src/node_perf.cc
@@ -280,10 +280,12 @@ 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);
- AliasedInt32Array& buffer = env->performance_state()->uv_metrics;
- buffer[0] = static_cast<int32_t>(metrics.loop_count);
- buffer[1] = static_cast<int32_t>(metrics.events);
- buffer[2] = static_cast<int32_t>(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<double>(metrics.loop_count);
+ buffer[1] = static_cast<double>(metrics.events);
+ buffer[2] = static_cast<double>(metrics.events_waiting);
}
void CreateELDHistogram(const FunctionCallbackInfo<Value>& args) {
diff --git a/src/node_perf_common.h b/src/node_perf_common.h
index aa84ba55b08..3aa228a501d 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 00000000000..4882f946211
--- /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 b67682b0ff3..c7b1f7481b7 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 cf3ef0a664f..26c5335321d 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;
}