Skip to content

Commit fbab3f2

Browse files
committed
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>
1 parent a90f99b commit fbab3f2

12 files changed

Lines changed: 197 additions & 19 deletions

benchmark/perf_hooks/nodetiming-uvmetricsinfo.js

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ const {
1111
const bench = common.createBenchmark(main, {
1212
n: [1e6],
1313
events: [1, 1000, 10000],
14+
api: ['number', 'bigint'],
1415
});
1516

1617
async function runEvents(events) {
@@ -19,11 +20,19 @@ async function runEvents(events) {
1920
}
2021
}
2122

22-
async function main({ n, events }) {
23+
async function main({ n, events, api }) {
2324
await runEvents(events);
24-
bench.start();
25-
for (let i = 0; i < n; i++) {
26-
assert.ok(performance.nodeTiming.uvMetricsInfo);
25+
if (api === 'bigint') {
26+
bench.start();
27+
for (let i = 0; i < n; i++) {
28+
assert.ok(performance.nodeTiming.uvMetricsInfoBigInt);
29+
}
30+
bench.end(n);
31+
} else {
32+
bench.start();
33+
for (let i = 0; i < n; i++) {
34+
assert.ok(performance.nodeTiming.uvMetricsInfo);
35+
}
36+
bench.end(n);
2737
}
28-
bench.end(n);
2938
}

doc/api/perf_hooks.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -862,6 +862,10 @@ added:
862862
This is a wrapper to the `uv_metrics_info` function.
863863
It returns the current set of event loop metrics.
864864

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

889+
### `performanceNodeTiming.uvMetricsInfoBigInt`
890+
891+
<!-- YAML
892+
added: REPLACEME
893+
-->
894+
895+
* Type: {Object}
896+
* `loopCount` {bigint} Number of event loop iterations.
897+
* `events` {bigint} Number of events that have been processed by the event handler.
898+
* `eventsWaiting` {bigint} Number of events that were waiting to be processed when the event provider was called.
899+
900+
The same as [`performanceNodeTiming.uvMetricsInfo`][], except that the values
901+
are {bigint}s carrying the full 64-bit range reported by libuv.
902+
903+
Because `JSON.stringify()` cannot serialize {bigint} values, this property is
904+
not enumerable and is not included in the output of
905+
`performanceNodeTiming.toJSON()`. Copies of `performance.nodeTiming` made by
906+
spreading its enumerable properties, for example, remain serializable.
907+
908+
```cjs
909+
const { performance } = require('node:perf_hooks');
910+
911+
setImmediate(() => {
912+
console.log(performance.nodeTiming.uvMetricsInfoBigInt);
913+
});
914+
```
915+
916+
```mjs
917+
import { performance } from 'node:perf_hooks';
918+
919+
setImmediate(() => {
920+
console.log(performance.nodeTiming.uvMetricsInfoBigInt);
921+
});
922+
```
923+
885924
### `performanceNodeTiming.v8Start`
886925

887926
<!-- YAML
@@ -3263,6 +3302,8 @@ dns.promises.resolve('localhost');
32633302
[`perf_hooks.importHistogram()`]: #perf_hooksimporthistogramdata
32643303
[`perf_hooks.monitorEventLoopDelay()`]: #perf_hooksmonitoreventloopdelayoptions
32653304
[`perf_hooks.timerify()`]: #perf_hookstimerifyfn-options
3305+
[`performanceNodeTiming.uvMetricsInfoBigInt`]: #performancenodetiminguvmetricsinfobigint
3306+
[`performanceNodeTiming.uvMetricsInfo`]: #performancenodetiminguvmetricsinfo
32663307
[`process.hrtime()`]: process.md#processhrtimetime
32673308
[`timeOrigin`]: https://w3c.github.io/hr-time/#dom-performance-timeorigin
32683309
[`window.performance.toJSON`]: https://developer.mozilla.org/en-US/docs/Web/API/Performance/toJSON

lib/internal/perf/nodetiming.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ const {
3030
loopIdleTime,
3131
uvMetricsInfo,
3232
uvMetricsBuffer,
33+
uvMetricsBigIntBuffer,
3334
} = internalBinding('performance');
3435

3536
class PerformanceNodeTiming {
@@ -138,6 +139,23 @@ class PerformanceNodeTiming {
138139
};
139140
},
140141
},
142+
143+
// Not enumerable, so that copying the enumerable properties, e.g. with
144+
// `{ ...performance.nodeTiming }`, does not produce an object that
145+
// JSON.stringify() cannot serialize.
146+
uvMetricsInfoBigInt: {
147+
__proto__: null,
148+
enumerable: false,
149+
configurable: true,
150+
get: () => {
151+
uvMetricsInfo();
152+
return {
153+
loopCount: uvMetricsBigIntBuffer[0],
154+
events: uvMetricsBigIntBuffer[1],
155+
eventsWaiting: uvMetricsBigIntBuffer[2],
156+
};
157+
},
158+
},
141159
});
142160
}
143161

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

155173
toJSON() {
174+
// uvMetricsInfoBigInt is intentionally omitted: JSON.stringify() cannot
175+
// serialize bigint values.
156176
return {
157177
name: 'node',
158178
entryType: 'node',

src/aliased_buffer.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,8 @@ class AliasedBufferBase final : public MemoryRetainer {
191191
V(uint32_t, Uint32Array) \
192192
V(float, Float32Array) \
193193
V(double, Float64Array) \
194-
V(int64_t, BigInt64Array)
194+
V(int64_t, BigInt64Array) \
195+
V(uint64_t, BigUint64Array)
195196

196197
#define V(NativeT, V8T) \
197198
typedef AliasedBufferBase<NativeT, v8::V8T> Aliased##V8T;

src/node_perf.cc

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,12 @@ PerformanceState::PerformanceState(Isolate* isolate,
6161
offsetof(performance_state_internal, uv_metrics),
6262
3,
6363
root,
64-
MAYBE_FIELD_PTR(info, uv_metrics)) {
64+
MAYBE_FIELD_PTR(info, uv_metrics)),
65+
uv_metrics_bigint(isolate,
66+
offsetof(performance_state_internal, uv_metrics_bigint),
67+
3,
68+
root,
69+
MAYBE_FIELD_PTR(info, uv_metrics_bigint)) {
6570
if (info == nullptr) {
6671
// For performance states initialized from scratch, reset
6772
// all the milestones and initialize the time origin.
@@ -89,11 +94,15 @@ PerformanceState::SerializeInfo PerformanceState::Serialize(
8994
for (size_t i = 0; i < uv_metrics.Length(); ++i) {
9095
uv_metrics[i] = 0;
9196
}
97+
for (size_t i = 0; i < uv_metrics_bigint.Length(); ++i) {
98+
uv_metrics_bigint[i] = 0;
99+
}
92100

93101
SerializeInfo info{root.Serialize(context, creator),
94102
milestones.Serialize(context, creator),
95103
observers.Serialize(context, creator),
96-
uv_metrics.Serialize(context, creator)};
104+
uv_metrics.Serialize(context, creator),
105+
uv_metrics_bigint.Serialize(context, creator)};
97106
return info;
98107
}
99108

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

120130
// Re-initialize the time origin and timestamp i.e. the process start time.
121131
Initialize(time_origin, time_origin_timestamp);
@@ -128,6 +138,7 @@ std::ostream& operator<<(std::ostream& o,
128138
<< " " << i.milestones << ", // milestones\n"
129139
<< " " << i.observers << ", // observers\n"
130140
<< " " << i.uv_metrics << ", // uv_metrics\n"
141+
<< " " << i.uv_metrics_bigint << ", // uv_metrics_bigint\n"
131142
<< "}";
132143
return o;
133144
}
@@ -280,12 +291,16 @@ void UvMetricsInfo(const FunctionCallbackInfo<Value>& args) {
280291
uv_metrics_t metrics;
281292
// uv_metrics_info always return 0
282293
CHECK_EQ(uv_metrics_info(env->event_loop(), &metrics), 0);
283-
// libuv reports 64-bit counters. Store them as doubles so that they are
284-
// exact up to Number.MAX_SAFE_INTEGER instead of wrapping at 2^31.
285-
AliasedFloat64Array& buffer = env->performance_state()->uv_metrics;
286-
buffer[0] = static_cast<double>(metrics.loop_count);
287-
buffer[1] = static_cast<double>(metrics.events);
288-
buffer[2] = static_cast<double>(metrics.events_waiting);
294+
// libuv reports 64-bit counters. The doubles backing uvMetricsInfo are
295+
// exact up to Number.MAX_SAFE_INTEGER, while the uint64_t values backing
296+
// uvMetricsInfoBigInt carry the full range.
297+
PerformanceState* state = env->performance_state();
298+
const uint64_t values[] = {
299+
metrics.loop_count, metrics.events, metrics.events_waiting};
300+
for (size_t i = 0; i < arraysize(values); ++i) {
301+
state->uv_metrics[i] = static_cast<double>(values[i]);
302+
state->uv_metrics_bigint[i] = values[i];
303+
}
289304
}
290305

291306
void CreateELDHistogram(const FunctionCallbackInfo<Value>& args) {
@@ -382,6 +397,11 @@ void CreatePerContextProperties(Local<Object> target,
382397
FIXED_ONE_BYTE_STRING(isolate, "uvMetricsBuffer"),
383398
state->uv_metrics.GetJSArray())
384399
.Check();
400+
target
401+
->Set(context,
402+
FIXED_ONE_BYTE_STRING(isolate, "uvMetricsBigIntBuffer"),
403+
state->uv_metrics_bigint.GetJSArray())
404+
.Check();
385405

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

src/node_perf_common.h

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ class PerformanceState {
6363
AliasedBufferIndex milestones;
6464
AliasedBufferIndex observers;
6565
AliasedBufferIndex uv_metrics;
66+
AliasedBufferIndex uv_metrics_bigint;
6667
};
6768

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

8486
uint64_t performance_last_gc_start_mark = 0;
8587
uint16_t current_gc_type = 0;
@@ -91,9 +93,10 @@ class PerformanceState {
9193
void Initialize(uint64_t time_origin, double time_origin_timestamp);
9294
void ResetMilestones();
9395
struct performance_state_internal {
94-
// doubles first so that they are always sizeof(double)-aligned
96+
// 64-bit fields first so that they are always 8-byte aligned
9597
double milestones[NODE_PERFORMANCE_MILESTONE_INVALID];
9698
double uv_metrics[3];
99+
uint64_t uv_metrics_bigint[3];
97100
uint32_t observers[NODE_PERFORMANCE_ENTRY_TYPE_INVALID];
98101
};
99102
};

src/node_snapshotable.cc

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -408,6 +408,7 @@ size_t SnapshotSerializer::Write(const ImmediateInfo::SerializeInfo& data) {
408408
// [ 4/8 bytes ] snapshot index of milestones
409409
// [ 4/8 bytes ] snapshot index of observers
410410
// [ 4/8 bytes ] snapshot index of uv_metrics
411+
// [ 4/8 bytes ] snapshot index of uv_metrics_bigint
411412
template <>
412413
performance::PerformanceState::SerializeInfo SnapshotDeserializer::Read() {
413414
Debug("Read<PerformanceState::SerializeInfo>()\n");
@@ -417,6 +418,7 @@ performance::PerformanceState::SerializeInfo SnapshotDeserializer::Read() {
417418
result.milestones = ReadArithmetic<AliasedBufferIndex>();
418419
result.observers = ReadArithmetic<AliasedBufferIndex>();
419420
result.uv_metrics = ReadArithmetic<AliasedBufferIndex>();
421+
result.uv_metrics_bigint = ReadArithmetic<AliasedBufferIndex>();
420422
if (is_debug) {
421423
std::string str = ToStr(result);
422424
Debug("Read<PerformanceState::SerializeInfo>() %s\n", str);
@@ -436,6 +438,7 @@ size_t SnapshotSerializer::Write(
436438
written_total += WriteArithmetic<AliasedBufferIndex>(data.milestones);
437439
written_total += WriteArithmetic<AliasedBufferIndex>(data.observers);
438440
written_total += WriteArithmetic<AliasedBufferIndex>(data.uv_metrics);
441+
written_total += WriteArithmetic<AliasedBufferIndex>(data.uv_metrics_bigint);
439442

440443
Debug("Write<PerformanceState::SerializeInfo>() wrote %d bytes\n",
441444
written_total);

test/fixtures/test-nodetiming-uvmetricsinfo.js

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ function safeMetricsInfo(cb) {
1313
});
1414
}
1515

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

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

3640
{
3741
function openFile(info) {
3842
assert.strictEqual(info.loopCount, 1);
43+
const infoBigInt = nodeTiming.uvMetricsInfoBigInt;
44+
assert.strictEqual(infoBigInt.loopCount, 1n);
3945

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

4450
const saved = { ...info };
51+
const savedBigInt = { ...infoBigInt };
4552
safeMetricsInfo((nextInfo) => {
4653
assert.notStrictEqual(nextInfo, info);
4754
assert.ok(nextInfo.loopCount > saved.loopCount);
48-
// Updating the shared buffer must not change earlier results.
55+
const nextInfoBigInt = nodeTiming.uvMetricsInfoBigInt;
56+
assert.notStrictEqual(nextInfoBigInt, infoBigInt);
57+
assert.ok(nextInfoBigInt.loopCount > savedBigInt.loopCount);
58+
// Updating the shared buffers must not change earlier results.
4959
assert.deepStrictEqual(info, saved);
60+
assert.deepStrictEqual(infoBigInt, savedBigInt);
5061
});
5162
}
5263

5364
safeMetricsInfo(openFile);
5465
}
66+
67+
{
68+
// Both representations are filled by the same native call, and libuv only
69+
// updates the metrics while the event loop is running, so back-to-back
70+
// synchronous reads must agree.
71+
safeMetricsInfo(() => {
72+
const info = nodeTiming.uvMetricsInfo;
73+
const infoBigInt = nodeTiming.uvMetricsInfoBigInt;
74+
for (const key of ['loopCount', 'events', 'eventsWaiting']) {
75+
assert.strictEqual(typeof info[key], 'number');
76+
assert.strictEqual(typeof infoBigInt[key], 'bigint');
77+
assert.strictEqual(BigInt(info[key]), infoBigInt[key]);
78+
}
79+
});
80+
}

test/parallel/test-performance-nodetiming-uvmetricsinfo-buffer.js

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,22 @@ require('../common');
55
const assert = require('node:assert');
66
const { internalBinding } = require('internal/test/binding');
77

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

1420
uvMetricsInfo();
15-
for (const value of uvMetricsBuffer) {
21+
for (let i = 0; i < uvMetricsBuffer.length; i++) {
22+
const value = uvMetricsBuffer[i];
1623
assert.ok(Number.isSafeInteger(value), `${value} is not a safe integer`);
1724
assert.ok(value >= 0, `${value} is negative`);
25+
assert.strictEqual(BigInt(value), uvMetricsBigIntBuffer[i]);
1826
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
'use strict';
2+
3+
const common = require('../common');
4+
const assert = require('node:assert');
5+
const { Worker } = require('node:worker_threads');
6+
7+
// The event loop metrics are tracked per event loop, so they are also
8+
// available in worker threads.
9+
const worker = new Worker(`
10+
const { parentPort } = require('node:worker_threads');
11+
const { performance } = require('node:perf_hooks');
12+
setImmediate(() => {
13+
const info = performance.nodeTiming.uvMetricsInfo;
14+
const infoBigInt = performance.nodeTiming.uvMetricsInfoBigInt;
15+
parentPort.postMessage({ info, infoBigInt });
16+
});
17+
`, { eval: true });
18+
19+
worker.on('message', common.mustCall(({ info, infoBigInt }) => {
20+
for (const key of ['loopCount', 'events', 'eventsWaiting']) {
21+
assert.strictEqual(typeof info[key], 'number');
22+
assert.strictEqual(typeof infoBigInt[key], 'bigint');
23+
assert.strictEqual(BigInt(info[key]), infoBigInt[key]);
24+
}
25+
assert.ok(infoBigInt.loopCount > 0n);
26+
}));

0 commit comments

Comments
 (0)