fix(node:perf_hooks): make PerformanceNodeTiming startTime/duration work#32481
fix(node:perf_hooks): make PerformanceNodeTiming startTime/duration work#324810xfandom wants to merge 1 commit into
Conversation
$toClass replaces the class prototype with a fresh object inheriting only from PerformanceEntry, so the name/entryType/startTime/duration getters and toJSON defined in the class body were discarded. Reading startTime or duration then fell through to PerformanceEntry's native getters, which throw for objects that are not real PerformanceEntry instances. Define the accessors and toJSON on the prototype after $toClass, return 0 for startTime to match Node, and report the startup milestones as offsets from performance.timeOrigin (0) instead of absolute epoch timestamps. Fixes oven-sh#23041
Walkthrough
ChangesPerformanceNodeTiming prototype refactor and offset fix
🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/js/node/perf_hooks.ts`:
- Around line 64-78: The PerformanceNodeTiming.prototype.toJSON assignment
creates an enumerable property which differs from Node.js behavior where toJSON
is non-enumerable. Replace the direct assignment of
PerformanceNodeTiming.prototype.toJSON with Object.defineProperty, passing the
prototype object, the property name "toJSON", and a property descriptor object
that defines the value as the toJSON function while setting enumerable to false
to match Node.js implementation.
- Around line 55-62: The properties `name`, `entryType`, `startTime`, and
`duration` are currently defined only on the PerformanceNodeTiming.prototype,
but they must also be defined as own properties on the instance to match Node.js
v22+ behavior and ensure Object.getOwnPropertyNames returns them. In the
createPerformanceNodeTiming function, after creating the instance, apply
Object.defineProperties to the instance itself using the same property
descriptors (with the same getter functions for name, entryType, startTime, and
duration) that are defined on the prototype.
In `@test/js/node/perf_hooks/perf_hooks.test.ts`:
- Around line 25-30: The milestone offset validation loop iterates over
nodeStart, v8Start, environment, bootstrapComplete, and idleTime but is missing
loopStart, which was also converted from epoch timestamps to offsets. Add
loopStart to the array of keys being checked in the for loop that validates each
milestone is less than 1e12, ensuring that loopStart is properly validated
alongside the other milestone offsets.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: 25088c16-7ff1-4e8f-9c5c-ee16f79530a6
📒 Files selected for processing (2)
src/js/node/perf_hooks.tstest/js/node/perf_hooks/perf_hooks.test.ts
| Object.defineProperties(PerformanceNodeTiming.prototype, { | ||
| name: { get: () => "node", configurable: true, enumerable: true }, | ||
| entryType: { get: () => "node", configurable: true, enumerable: true }, | ||
| // A "node" entry is the timeOrigin reference, so its startTime is always 0, | ||
| // matching Node. | ||
| startTime: { get: () => 0, configurable: true, enumerable: true }, | ||
| duration: { get: () => performance.now(), configurable: true, enumerable: true }, | ||
| }); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
bun bd -e '
const perf = require("node:perf_hooks");
const names = Object.getOwnPropertyNames(perf.performance.nodeTiming);
console.log(names);
if (!names.includes("startTime") || !names.includes("duration")) process.exit(1);
'Repository: oven-sh/bun
Length of output: 99
🏁 Script executed:
cd /tmp && find . -name "perf_hooks.ts" -type f 2>/dev/null | head -5Repository: oven-sh/bun
Length of output: 37
🏁 Script executed:
git -C /tmp ls-files "**perf_hooks.ts" 2>/dev/null | head -5Repository: oven-sh/bun
Length of output: 37
🏁 Script executed:
fd -i perf_hooks.tsRepository: oven-sh/bun
Length of output: 130
🏁 Script executed:
cat -n src/js/node/perf_hooks.ts | head -100Repository: oven-sh/bun
Length of output: 4431
🏁 Script executed:
cat -n src/js/node/perf_hooks.ts | sed -n '46,92p'Repository: oven-sh/bun
Length of output: 2384
🏁 Script executed:
python3 << 'PY'
import json
# Node.js behavior:
# In Node.js, performance.nodeTiming is a PerformanceNodeTiming instance
# that inherits from PerformanceEntry. Let me check how Node.js defines these properties.
# The key question: are name, entryType, startTime, duration own properties
# or prototype properties?
# Let's verify by understanding what Object.getOwnPropertyNames returns
# for a typical PerformanceEntry instance
test_code = """
const perf = require('perf_hooks').performance;
// What does getOwnPropertyNames return?
const ownProps = Object.getOwnPropertyNames(perf.nodeTiming);
console.log('Own properties:', ownProps);
// Check if startTime is an own property
console.log('startTime is own property:', ownProps.includes('startTime'));
console.log('startTime descriptor:', Object.getOwnPropertyDescriptor(perf.nodeTiming, 'startTime'));
// Check prototype
console.log('startTime on prototype:', Object.getOwnPropertyDescriptor(Object.getPrototypeOf(perf.nodeTiming), 'startTime'));
// Check what Node.js reports
console.log('nodeTiming.startTime:', perf.nodeTiming.startTime);
console.log('nodeTiming.name:', perf.nodeTiming.name);
"""
print("Expected Node.js test code:")
print(test_code)
PYRepository: oven-sh/bun
Length of output: 794
🏁 Script executed:
cat test/integration/bun-types/fixture/perf_hooks.tsRepository: oven-sh/bun
Length of output: 208
🏁 Script executed:
rg -i "nodeTiming" test/ --type ts --type js -A 3 -B 3Repository: oven-sh/bun
Length of output: 3457
🏁 Script executed:
rg -i "getOwnPropertyNames.*performance\|performance.*getOwnPropertyNames" test/ --type ts --type jsRepository: oven-sh/bun
Length of output: 37
🏁 Script executed:
rg "startTime.*duration\|duration.*startTime" src/js/node/ --type tsRepository: oven-sh/bun
Length of output: 37
🏁 Script executed:
rg -A 5 "nodeTiming" src/js/node/perf_hooks.tsRepository: oven-sh/bun
Length of output: 251
🏁 Script executed:
python3 << 'PY'
# The core issue:
# Object.getOwnPropertyNames() returns ONLY own properties, not inherited ones
#
# Current code:
# - Lines 55-62: defineProperties on PerformanceNodeTiming.prototype
# - Lines 80-89: createPerformanceNodeTiming() creates object with create(prototype)
# and assigns milestone fields as own properties
#
# Result: name, entryType, startTime, duration are on prototype, not on the instance
# So Object.getOwnPropertyNames(instance) won't include them
#
# Proposed fix:
# - Also defineProperties on the instance itself in createPerformanceNodeTiming()
#
# This would make them show up in getOwnPropertyNames()
# Let's verify this is correct by understanding standard PerformanceEntry behavior
print("Understanding the issue:")
print("1. Current: properties only on prototype")
print("2. Problem: Object.getOwnPropertyNames() doesn't include prototype properties")
print("3. Solution: Define properties on both prototype AND instance")
print("")
print("The question: Does Node.js expose name/entryType/startTime/duration")
print("as own properties or prototype properties on performance.nodeTiming?")
print("")
print("In Node.js, PerformanceEntry instances typically have these as own properties")
print("when they're created, or they're accessible but not enumerable via getOwnPropertyNames")
print("depending on the implementation.")
PYRepository: oven-sh/bun
Length of output: 598
🏁 Script executed:
rg "PerformanceEntry\|nodeTiming" src/ --type rs -i -A 2 -B 2 | head -50Repository: oven-sh/bun
Length of output: 82
🏁 Script executed:
cat -n test/js/node/perf_hooks/perf_hooks.test.ts | head -60Repository: oven-sh/bun
Length of output: 2679
🏁 Script executed:
rg "getOwnPropertyNames.*nodeTiming\|nodeTiming.*getOwnPropertyNames" test/Repository: oven-sh/bun
Length of output: 37
🏁 Script executed:
cat -n test/js/node/perf_hooks/perf_hooks.test.ts | tail -20Repository: oven-sh/bun
Length of output: 1001
🏁 Script executed:
rg "`#23041`" src/ test/ --type ts --type jsRepository: oven-sh/bun
Length of output: 37
🏁 Script executed:
web_search
What does Object.getOwnPropertyNames return for performance.nodeTiming in Node.jsRepository: oven-sh/bun
Length of output: 149
🌐 Web query:
Node.js performance.nodeTiming Object.getOwnPropertyNames behavior
💡 Result:
In Node.js, the performance.nodeTiming object is an instance of the PerformanceNodeTiming class, which provides performance metrics for specific Node.js operational milestones [1][2]. When you call Object.getOwnPropertyNames(performance.nodeTiming), it returns an array of all properties defined directly on that instance, including both enumerable and non-enumerable properties [3]. As of recent Node.js versions (e.g., v22+), this list typically includes the following [4]: - name - entryType - startTime - duration - nodeStart - v8Start - environment - loopStart - loopExit - bootstrapComplete - idleTime - uvMetricsInfo Because Object.getOwnPropertyNames() is designed to retrieve all own property names regardless of enumerability [3], it successfully returns these internal performance properties. This behavior is consistent with the standard JavaScript behavior for objects [3] and is used in Node.js test suites to inspect and verify the performance.nodeTiming object's structure [5]. Note that while these properties exist on the object, some may have specific behaviors or return values like -1 if a milestone has not yet been reached [6][7].
Citations:
- 1: https://nodejs.org/api/perf_hooks.html
- 2: https://nodejs.org/docs/latest-v26.x/api/perf_hooks.html
- 3: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/getOwnPropertyNames
- 4: perf_hooks: PerformanceNodeTiming startTime/duration throw; nodeTiming shape differs from Node #23041
- 5: nodejs/node@0261c5de56
- 6: PerformanceNodeTiming of perf_hooks nodejs/node#45958
- 7: https://cdn.jsdelivr.net/npm/@types/node@25.3.3/perf_hooks.d.ts
🏁 Script executed:
rg "getOwnPropertyNames.*performance\|getOwnPropertyNames.*nodeTiming" test/js/node/perf_hooks/Repository: oven-sh/bun
Length of output: 37
🏁 Script executed:
git log --oneline src/js/node/perf_hooks.ts | head -10Repository: oven-sh/bun
Length of output: 128
🏁 Script executed:
rg "23041" src/ test/ --type ts --type js -B 2 -A 2Repository: oven-sh/bun
Length of output: 503
Define entry accessors as own properties on the instance too.
Defining name, entryType, startTime, and duration on PerformanceNodeTiming.prototype prevents access from throwing (fixing #23041), but Object.getOwnPropertyNames(performance.nodeTiming) still omits them because the instance owns only the milestone fields assigned in createPerformanceNodeTiming(). In Node.js v22+, these entry properties are own properties on the instance, not just prototype properties. Without own property definitions, the behavior diverges from Node.js.
Apply the same property descriptors to the instance in createPerformanceNodeTiming():
+const performanceNodeTimingEntryDescriptors = {
+ name: { get: () => "node", configurable: true, enumerable: true },
+ entryType: { get: () => "node", configurable: true, enumerable: true },
+ startTime: { get: () => 0, configurable: true, enumerable: true },
+ duration: { get: () => performance.now(), configurable: true, enumerable: true },
+};
+
-Object.defineProperties(PerformanceNodeTiming.prototype, {
- name: { get: () => "node", configurable: true, enumerable: true },
- entryType: { get: () => "node", configurable: true, enumerable: true },
- startTime: { get: () => 0, configurable: true, enumerable: true },
- duration: { get: () => performance.now(), configurable: true, enumerable: true },
-});
+Object.defineProperties(PerformanceNodeTiming.prototype, performanceNodeTimingEntryDescriptors);
function createPerformanceNodeTiming() {
const object = Object.create(PerformanceNodeTiming.prototype);
+ Object.defineProperties(object, performanceNodeTimingEntryDescriptors);This ensures both prototype and instance define these properties, matching Node.js behavior.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/js/node/perf_hooks.ts` around lines 55 - 62, The properties `name`,
`entryType`, `startTime`, and `duration` are currently defined only on the
PerformanceNodeTiming.prototype, but they must also be defined as own properties
on the instance to match Node.js v22+ behavior and ensure
Object.getOwnPropertyNames returns them. In the createPerformanceNodeTiming
function, after creating the instance, apply Object.defineProperties to the
instance itself using the same property descriptors (with the same getter
functions for name, entryType, startTime, and duration) that are defined on the
prototype.
| PerformanceNodeTiming.prototype.toJSON = function toJSON() { | ||
| return { | ||
| name: this.name, | ||
| entryType: this.entryType, | ||
| startTime: this.startTime, | ||
| duration: this.duration, | ||
| nodeStart: this.nodeStart, | ||
| v8Start: this.v8Start, | ||
| bootstrapComplete: this.bootstrapComplete, | ||
| environment: this.environment, | ||
| loopStart: this.loopStart, | ||
| loopExit: this.loopExit, | ||
| idleTime: this.idleTime, | ||
| }; | ||
| }; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
bun bd -e '
const perf = require("node:perf_hooks");
const keys = [];
for (const key in perf.performance.nodeTiming) keys.push(key);
console.log(keys);
if (keys.includes("toJSON")) process.exit(1);
'Repository: oven-sh/bun
Length of output: 99
🏁 Script executed:
cat -n src/js/node/perf_hooks.ts | head -100Repository: oven-sh/bun
Length of output: 4431
🏁 Script executed:
# Test property enumeration behavior in JavaScript
# First test direct assignment vs Object.defineProperty
node -e "
const obj = {};
// Test 1: Direct assignment to prototype
function TestClass1() {}
TestClass1.prototype.toJSON = function() { return 'direct'; };
const keys1 = [];
for (const key in new TestClass1()) keys1.push(key);
console.log('Direct assignment enumerable:', keys1.includes('toJSON'));
// Test 2: Object.defineProperty with no descriptor
function TestClass2() {}
Object.defineProperty(TestClass2.prototype, 'toJSON', {
value: function() { return 'defined'; },
configurable: true,
writable: true
});
const keys2 = [];
for (const key in new TestClass2()) keys2.push(key);
console.log('Object.defineProperty (no enumerable) enumerable:', keys2.includes('toJSON'));
// Test 3: Object.defineProperty with enumerable: false
function TestClass3() {}
Object.defineProperty(TestClass3.prototype, 'toJSON', {
value: function() { return 'defined'; },
configurable: true,
writable: true,
enumerable: false
});
const keys3 = [];
for (const key in new TestClass3()) keys3.push(key);
console.log('Object.defineProperty (enumerable: false) enumerable:', keys3.includes('toJSON'));
"Repository: oven-sh/bun
Length of output: 206
🌐 Web query:
Node.js perf_hooks PerformanceNodeTiming.prototype.toJSON enumerable specification
💡 Result:
In Node.js, the toJSON() method on PerformanceNodeTiming.prototype is designed to be enumerable, consistent with the general convention for toJSON methods in the perf_hooks module [1][2]. Historical development in the Node.js codebase shows that PerformanceNodeTiming underwent changes to make it a first-class object, ensuring that its properties are properly exposed and enumerable to support serialization [3][4]. Similarly, the toJSON method has been explicitly added and defined on the prototypes of various performance-related classes within perf_hooks to allow for standard JSON representation, typically as an enumerable property to facilitate operations like JSON.stringify() [1]. While the official documentation for PerformanceNodeTiming details its properties, the specific enumeration behavior of toJSON follows the broader implementation pattern where these helper methods are enumerable to ensure they are discovered and included during serialization processes [1][5][6].
Citations:
- 1: nodejs/node@4711f57cf2
- 2: nodejs/node@dfe4237d77
- 3: nodejs/node@395b9a6
- 4: nodejs/node@ed8af3a
- 5: https://nodejs.org/dist/latest/docs/api/perf_hooks.html
- 6: https://nodejs.org/api/perf_hooks.html
🏁 Script executed:
# Verify Node.js actual behavior for PerformanceNodeTiming
node -e "
const perf = require('perf_hooks');
const nodeTiming = perf.performance.nodeTiming;
// Check if toJSON is enumerable
const descriptor = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(nodeTiming), 'toJSON');
console.log('toJSON descriptor:', descriptor);
// Check enumeration
const keys = [];
for (const key in nodeTiming) keys.push(key);
console.log('for...in keys:', keys);
console.log('toJSON in keys:', keys.includes('toJSON'));
// Check Object.keys
console.log('Object.keys:', Object.keys(nodeTiming));
"Repository: oven-sh/bun
Length of output: 591
🌐 Web query:
Node.js PerformanceNodeTiming toJSON method enumerable property specification
💡 Result:
In Node.js, the PerformanceNodeTiming.toJSON method is designed to provide a JSON-serializable representation of the performance timing object [1][2]. As part of a core architectural change to make nodeTiming a "first-class object" in Node.js, all properties of the PerformanceNodeTiming instance were made enumerable [3][4][5]. This ensures that when the toJSON method is invoked—or when the object is processed by methods like JSON.stringify—the relevant timing properties (such as startTime, duration, nodeStart, v8Start, etc.) are accessible and included in the output [3][4][5]. While the W3C Performance Timeline specification defines a generic [Default] toJSON behavior for PerformanceEntry objects [6], the Node.js implementation of PerformanceNodeTiming explicitly ensures its timing properties are enumerable [3][5]. This behavior allows developers to reliably serialize these performance metrics into JSON format for logging, debugging, or monitoring purposes [3][4].
Citations:
- 1: https://nodejs.org/api/perf_hooks.html
- 2: https://nodejs.org/dist/latest/docs/api/perf_hooks.html
- 3: nodejs/node@395b9a69a1
- 4: nodejs/node@ed8af3a
- 5: nodejs/node@395b9a6
- 6: https://www.w3.org/TR/performance-timeline/
Keep toJSON non-enumerable on the prototype.
Assigning PerformanceNodeTiming.prototype.toJSON = ... creates an enumerable property, but Node.js implements toJSON as non-enumerable. This causes for...in over nodeTiming to include toJSON in Bun but not in Node.js. Define it with Object.defineProperty to match Node.js behavior.
Proposed fix
-PerformanceNodeTiming.prototype.toJSON = function toJSON() {
- return {
- name: this.name,
- entryType: this.entryType,
- startTime: this.startTime,
- duration: this.duration,
- nodeStart: this.nodeStart,
- v8Start: this.v8Start,
- bootstrapComplete: this.bootstrapComplete,
- environment: this.environment,
- loopStart: this.loopStart,
- loopExit: this.loopExit,
- idleTime: this.idleTime,
- };
-};
+Object.defineProperty(PerformanceNodeTiming.prototype, "toJSON", {
+ value: function toJSON() {
+ return {
+ name: this.name,
+ entryType: this.entryType,
+ startTime: this.startTime,
+ duration: this.duration,
+ nodeStart: this.nodeStart,
+ v8Start: this.v8Start,
+ bootstrapComplete: this.bootstrapComplete,
+ environment: this.environment,
+ loopStart: this.loopStart,
+ loopExit: this.loopExit,
+ idleTime: this.idleTime,
+ };
+ },
+ configurable: true,
+ writable: true,
+});🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/js/node/perf_hooks.ts` around lines 64 - 78, The
PerformanceNodeTiming.prototype.toJSON assignment creates an enumerable property
which differs from Node.js behavior where toJSON is non-enumerable. Replace the
direct assignment of PerformanceNodeTiming.prototype.toJSON with
Object.defineProperty, passing the prototype object, the property name "toJSON",
and a property descriptor object that defines the value as the toJSON function
while setting enumerable to false to match Node.js implementation.
| // Milestones are offsets in ms from timeOrigin, not absolute epoch timestamps. | ||
| expect(perf.performance.timeOrigin).toBeGreaterThan(1e12); | ||
| for (const key of ["nodeStart", "v8Start", "environment", "bootstrapComplete", "idleTime"] as const) { | ||
| expect(nt[key]).toBeNumber(); | ||
| expect(nt[key]).toBeLessThan(1e12); | ||
| } |
There was a problem hiding this comment.
Include loopStart in the milestone offset check.
loopStart is one of the fields being fixed from epoch timestamps to offsets, but this loop skips it. The later toJSON() assertion only compares against nt.loopStart, so an epoch-valued loopStart would still pass.
Proposed fix
- for (const key of ["nodeStart", "v8Start", "environment", "bootstrapComplete", "idleTime"] as const) {
+ for (const key of ["nodeStart", "v8Start", "environment", "bootstrapComplete", "loopStart", "idleTime"] as const) {🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@test/js/node/perf_hooks/perf_hooks.test.ts` around lines 25 - 30, The
milestone offset validation loop iterates over nodeStart, v8Start, environment,
bootstrapComplete, and idleTime but is missing loopStart, which was also
converted from epoch timestamps to offsets. Add loopStart to the array of keys
being checked in the for loop that validates each milestone is less than 1e12,
ensuring that loopStart is properly validated alongside the other milestone
offsets.
What does this PR do?
Fixes #23041.
performance.nodeTiming.startTimeand.durationthrewTypeError: The PerformanceEntry.startTime getter can only be used on instances of PerformanceEntry, and the milestone fields were absolute epoch timestamps instead of offsets fromperformance.timeOrigin.PerformanceNodeTimingis faked with$toClass, which replaces the class prototype with a fresh object inheriting only fromPerformanceEntry. That discarded thename/entryType/startTime/durationgetters andtoJSONdefined in the class body, so readingstartTime/durationfell through toPerformanceEntry's native getters — which throw for any object that isn't a realPerformanceEntryinstance.toJSONon the prototype after$toClass(the same idiom every other$toClassuser follows).startTimereturns0to match Node (a "node" entry is the timeOrigin reference).nodeStart,v8Start,environment,bootstrapComplete,loopStart,idleTime) as offsets in ms fromtimeOrigin(0) rather than absolute epoch timestamps;loopExitstays-1.Before / after (
bun -e 'const {performance}=require("node:perf_hooks"); const nt=performance.nodeTiming; nt.startTime'):How did you verify your code works?
Added a
node:perf_hookstest assertingstartTime/durationno longer throw, thename/entryType/startTimevalues match Node, the milestones are offsets (< 1e12) whiletimeOriginis epoch-scale, and thetoJSON()shape. Verified the new test passes on the debug build and fails on the releasedbun(USE_SYSTEM_BUN=1).