Skip to content

fix(node:perf_hooks): report nodeTiming milestones as offsets from timeOrigin - #32481

Open
0xfandom wants to merge 1 commit into
oven-sh:mainfrom
0xfandom:claude/perf-hooks-node-timing
Open

fix(node:perf_hooks): report nodeTiming milestones as offsets from timeOrigin#32481
0xfandom wants to merge 1 commit into
oven-sh:mainfrom
0xfandom:claude/perf-hooks-node-timing

Conversation

@0xfandom

@0xfandom 0xfandom commented Jun 18, 2026

Copy link
Copy Markdown

What does this PR do?

Fixes #23041. performance.nodeTiming reports absolute epoch timestamps instead of offsets from performance.timeOrigin:

const nt = require("node:perf_hooks").performance.nodeTiming;
nt.nodeStart;   // before: 1.7715e12 (epoch ms)   node: 0.72
nt.startTime;   // before: 1.7715e12 (epoch ms)   node: 0

Node measures every startup milestone in milliseconds from timeOrigin, and a "node" entry is the timeOrigin reference, so its startTime is 0.

  • createPerformanceNodeTiming() set bootstrapComplete/environment/nodeStart/v8Start to performance.timeOrigin; they are now offsets (0), with loopExit staying -1. Bun does not record the individual startup milestones, so they are reported relative to process start.
  • startTime returned this.nodeStart (an epoch value); it now returns 0.
  • name/entryType/startTime/duration are now own enumerable properties of the nodeTiming object, not only prototype accessors, so Object.keys(performance.nodeTiming) matches Node. They use null-prototype descriptors so prototype pollution before the module loads cannot inject extra descriptor keys (value/writable/get/set).

The startTime/duration getters throwing The PerformanceEntry.startTime getter can only be used on instances of PerformanceEntry — the other half of #23041 — is already fixed on main by the switch to Object.setPrototypeOf, which preserves the class-body accessors. This PR was rebased onto that and reduced to the remaining gap.

How did you verify your code works?

Added a nodeTiming test in test/js/node/perf_hooks/perf_hooks.test.ts asserting startTime === 0, name/entryType, a numeric duration, that the entry accessors are own enumerable properties, and that every milestone is an offset (< 1e12) while timeOrigin is epoch-scale — plus the exact toJSON() shape. Values were checked against node -e on v22 and the documented Node behaviour (startTime: 0, small millisecond milestones).

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude Code Review

This pull request is from a fork — automated review is disabled. A repository maintainer can comment @claude review to run a one-time review.

@coderabbitai

coderabbitai Bot commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

PerformanceNodeTiming in perf_hooks.ts is refactored from an in-class getter approach to a prototype-based one using $toClass, with name, entryType, startTime, and duration defined via Object.defineProperties on the prototype. Milestone offsets are reset from epoch-like values to 0. A new test validates the corrected shape and toJSON output.

Changes

PerformanceNodeTiming prototype refactor and offset fix

Layer / File(s) Summary
PerformanceNodeTiming refactor: prototype-based implementation and factory function
src/js/node/perf_hooks.ts
PerformanceNodeTiming is converted to an empty class via $toClass with name, entryType, startTime, and duration accessors defined using Object.defineProperties on the prototype; toJSON is also moved to the prototype as a non-enumerable property. createPerformanceNodeTiming() constructs instances via Object.create(PerformanceNodeTiming.prototype), defines entry descriptors on the instance, and resets nodeStart, v8Start, environment, bootstrapComplete, loopStart to 0 and loopExit to -1 instead of previous epoch-derived values.
nodeTiming shape and toJSON test
test/js/node/perf_hooks/perf_hooks.test.ts
Adds a test asserting non-throwing access to startTime/duration, that timeOrigin is epoch-scale, that all milestone offsets are below 1e12, that loopExit === -1, that name/entryType/startTime/duration are enumerable own properties, that toJSON is non-enumerable on the prototype, and that toJSON() returns a numeric duration with the expected remaining property shape.
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The PR satisfies all three objectives from issue #23041: makes startTime/duration accessible returning correct values, converts milestone timestamps to offsets from timeOrigin, and includes required properties in the object shape matching Node.js behavior.
Out of Scope Changes check ✅ Passed All changes directly address the linked issue #23041: the prototype refactoring fixes property access, milestone offset conversion fixes timestamp semantics, and the test validates the fixes. No unrelated modifications detected.
Title check ✅ Passed The title is concise and accurately summarizes the main change to nodeTiming milestone offsets.
Description check ✅ Passed The description follows the template and includes both what changed and how it was verified.

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between f9530ae and 79827ec.

📒 Files selected for processing (2)
  • src/js/node/perf_hooks.ts
  • test/js/node/perf_hooks/perf_hooks.test.ts

Comment thread src/js/node/perf_hooks.ts Outdated
Comment thread src/js/node/perf_hooks.ts Outdated
Comment thread test/js/node/perf_hooks/perf_hooks.test.ts Outdated
@0xfandom

Copy link
Copy Markdown
Author

Addressed the review in ea92c93:

  • Own properties (major): name/entryType/startTime/duration are now defined on the nodeTiming instance as well as the prototype, so Object.getOwnPropertyNames(performance.nodeTiming) includes them, matching Node v22+.
  • toJSON (minor): defined via Object.defineProperty as non-enumerable to match Node.
  • Test (minor): loopStart added to the milestone-offset loop, plus assertions that the entry accessors are own enumerable props and toJSON is non-enumerable.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 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 58-63: The performanceNodeTimingEntryDescriptors object is
vulnerable to prototype-pollution attacks because it inherits from
Object.prototype. Add __proto__: null as the first property in the
performanceNodeTimingEntryDescriptors descriptor object to create a
null-prototype object that prevents prototype chain pollution. Since
performanceNodeTimingEntryDescriptors is reused in the
createPerformanceNodeTiming() function, this single change will secure both
locations.

In `@test/js/node/perf_hooks/perf_hooks.test.ts`:
- Around line 25-29: The current assertion uses Object.getOwnPropertyNames()
which returns all own properties regardless of enumerability, but the comment
states these should be "own enumerable properties". Replace or augment the check
to use Object.keys() instead, which only returns enumerable own properties. This
will properly verify that the "name", "entryType", "startTime", and "duration"
fields are actually enumerable, aligning the test with its documented intent and
providing the same rigor as the separate Object.getOwnPropertyDescriptor() check
for toJSON non-enumerability.
🪄 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: 143415b2-88c2-4de1-97a1-867e61b14d32

📥 Commits

Reviewing files that changed from the base of the PR and between 79827ec and ea92c93.

📒 Files selected for processing (2)
  • src/js/node/perf_hooks.ts
  • test/js/node/perf_hooks/perf_hooks.test.ts

Comment thread src/js/node/perf_hooks.ts Outdated
Comment thread test/js/node/perf_hooks/perf_hooks.test.ts Outdated
…meOrigin

performance.nodeTiming reported absolute epoch timestamps: the startup
milestones were set to performance.timeOrigin, and startTime returned
nodeStart. Node measures all of them in milliseconds from timeOrigin, and a
"node" entry is the timeOrigin reference, so its startTime is 0.

Also define the name/entryType/startTime/duration accessors as own enumerable
properties of the nodeTiming object, matching Node, using null-prototype
descriptors so prototype pollution cannot inject extra descriptor keys.

Fixes oven-sh#23041
@0xfandom
0xfandom force-pushed the claude/perf-hooks-node-timing branch from 91843ed to 95a2d0c Compare July 23, 2026 08:01
@0xfandom 0xfandom changed the title fix(node:perf_hooks): make PerformanceNodeTiming startTime/duration work fix(node:perf_hooks): report nodeTiming milestones as offsets from timeOrigin Jul 23, 2026
@0xfandom

Copy link
Copy Markdown
Author

Rebased onto current main and force-pushed (95a2d0c) — noting the history rewrite for review continuity.

Main has since switched PerformanceNodeTiming from $toClass to Object.setPrototypeOf, which preserves the class-body accessors and already fixes the startTime/duration getters throwing. That made the original diff both conflicting and largely redundant, so this PR is now scoped down to the part of #23041 that is still open: the milestones being absolute epoch timestamps rather than offsets from timeOrigin, startTime returning nodeStart instead of 0, and the entry accessors not being own properties.

The earlier CodeRabbit points (own enumerable properties, null-prototype descriptors) are carried over into the smaller diff. toJSON is left as the class method, which is already non-enumerable.

@robobun

robobun commented Aug 13, 2026

Copy link
Copy Markdown
Collaborator

Still relevant after #34518: that change fixed the throwing getters and closed #23041, but createPerformanceNodeTiming in src/js/node/perf_hooks.ts on current main still assigns performance.timeOrigin to the milestones, so nodeStart and friends are epoch timestamps (bun 1.4.0 prints nodeStart equal to timeOrigin, node v26 prints 0.61). This PR is the one that fixes that. #36518 duplicated it and is being closed in favor of this one.

One small difference from node you may want to pick up while you are here: node defines name, entryType and startTime as own data properties (value, writable: false) and only duration as a getter (lib/internal/perf/nodetiming.js). On node v26.3.0, Object.getOwnPropertyDescriptor(performance.nodeTiming, "startTime") is { value: 0, writable: false, enumerable: true, configurable: true }, while this branch defines an accessor. The descriptor block in #36518 (https://github.com/oven-sh/bun/pull/36518/files) has node's shape, and its test asserts the descriptors and also covers Object.prototype.value pollution during module init. Both are free to copy.

For context, #35390 wires up real milestone timestamps; until that lands this PR is the right fix for the offsets.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

perf_hooks: PerformanceNodeTiming startTime/duration throw; nodeTiming shape differs from Node

2 participants