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>
This commit is contained in:
James M Snell authored and Node.js GitHub Bot committed 2026-09-19 22:02:10 +00:00
1 parent 01a7ef6b14
commit 97af3d7da5
12 files changed
+197 -19

No files matched your search

@@ -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);
}
+41
View File
@@ -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
+20
View File
@@ -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',
+2 -1
View File
@@ -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;
+28 -8
View File
@@ -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);
+4 -1
View File
@@ -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];
};
};
+3
View File
@@ -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);
+27 -1
View File
@@ -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]);
}
});
}
@@ -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]);
}
@@ -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);
}));
@@ -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);
+1
View File
@@ -147,5 +147,6 @@ export interface PerformanceBinding {
markBootstrapComplete(): void;
uvMetricsInfo(): void;
uvMetricsBuffer: Float64Array;
uvMetricsBigIntBuffer: BigUint64Array;
now(): number;
}