Skip to content

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

Closed
robobun wants to merge 4 commits into
mainfrom
farm/57aa55ff/perf-hooks-nodetiming-offsets
Closed

fix(node:perf_hooks): report nodeTiming milestones as offsets from timeOrigin#36518
robobun wants to merge 4 commits into
mainfrom
farm/57aa55ff/perf-hooks-nodetiming-offsets

Conversation

@robobun

@robobun robobun commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator

Adopts #32481 by @0xfandom.

Repro

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

Cause

createPerformanceNodeTiming() in src/js/node/perf_hooks.ts set bootstrapComplete/environment/nodeStart/v8Start to performance.timeOrigin (an absolute epoch timestamp), and get startTime() returned this.nodeStart. Node measures every startup milestone in milliseconds from timeOrigin, and a "node" entry is the timeOrigin reference, so its startTime is 0.

Fix

  • Milestones are now offsets from timeOrigin (0; Bun does not record the individual startup milestones, so they are reported relative to process start). loopExit stays -1.
  • startTime returns 0.
  • name/entryType/startTime are defined as own enumerable data properties (writable:false) and duration as an own enumerable getter, matching Node's lib/internal/perf/nodetiming.js, so Object.keys(performance.nodeTiming) matches Node. Null-prototype descriptors so prototype pollution before the module loads cannot inject extra descriptor keys.

Why this is correct

Node's lib/internal/perf/nodetiming.js defines name/entryType/startTime as own data properties with value: and duration/milestones as own getters, with startTime: 0 hardcoded and each milestone computed as (ns - timeOrigin) / 1e6. Verified against Node v26.3.0: Object.getOwnPropertyDescriptor(nt, 'startTime') is {value: 0, writable: false, enumerable: true, configurable: true}. The OpenTelemetry bootstrap-span use case in #23041 depends on these being offsets, not epoch values.

Verification

$ bun bd test test/js/node/perf_hooks/perf_hooks.test.ts
 12 pass, 0 fail

Without the src/ change, the new tests fail:

error: expect(received).toBe(expected)
Expected: 0
Received: 1785472769724.3042

Fixes #23041. Supersedes #32481 (same patch, brought into a repo branch so CI can run, plus the descriptor-kind correction).


[review] gate passed · iteration 0 · 2 files touched

fails on main (without fix)
ASAN without fix: 2 FAILED
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/js/node/perf_hooks/perf_hooks.test.ts
bun test v1.4.0 (d074a22ce)

test/js/node/perf_hooks/perf_hooks.test.ts:
(pass) stubs [19.71ms]
16 |   const nt = perf.performance.nodeTiming;
17 | 
18 |   expect(nt.name).toBe("node");
19 |   expect(nt.entryType).toBe("node");
20 |   // A "node" entry is the timeOrigin reference, so startTime is 0 in Node.
21 |   expect(nt.startTime).toBe(0);
                            ^
error: expect(received).toBe(expected)

Expected: 0
Received: 1785475972805.419

      at <anonymous> (/workspace/bun/test/js/node/perf_hooks/perf_hooks.test.ts:21:24)
(fail) nodeTiming reports offsets from timeOrigin, not epoch timestamps [11.84ms]
(pass) doesn't throw [15.73ms]
(pass) Symbol name argument throws V8 wording [6.67ms]
(pass) measure(name, optionsWithoutStartOrEnd, endMark) honours the trailing endMark [5.25ms]
(pass) timerify entry shape [54.82ms]
(pass) timerify is exposed on both performance and as a top-level export (Node v25.2+) [2.10ms]
(pass) export surface matches Node v26.3.0 [8.53ms]
(pass) timerify 
... (truncated)

release without fix: 10 FAILED
bun test v1.4.0-canary.1 (1498d7b77)

test/js/node/perf_hooks/perf_hooks.test.ts:
(pass) stubs [0.11ms]
13 | 
14 | // https://github.com/oven-sh/bun/issues/23041
15 | test("nodeTiming reports offsets from timeOrigin, not epoch timestamps", () => {
16 |   const nt = perf.performance.nodeTiming;
17 | 
18 |   expect(nt.name).toBe("node");
              ^
TypeError: The PerformanceEntry.name getter can only be used on instances of PerformanceEntry
      at <anonymous> (/workspace/bun/test/js/node/perf_hooks/perf_hooks.test.ts:18:10)
(fail) nodeTiming reports offsets from timeOrigin, not epoch timestamps [0.40ms]
(pass) doesn't throw [0.98ms]
85 | 
86 | // Node coerces the name via `${name}` for mark/clearMarks/clearMeasures, so a
87 | // Symbol hits V8's ToString message. Verified against Node v26.3.0.
88 | test("Symbol name argument throws V8 wording", () => {
89 |   const msg = "Cannot convert a Symbol value to a string";
90 |   expect(() => performance.mark(Symbol())).toThrow(new TypeError(msg));
                                                ^
error: expect(received).toThrow(expected)

Expected message: "Cannot convert a Symbol value to a string"
Received message: 
... (truncated)
passes on PR (with fix)
ASAN with fix: all passed
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/js/node/perf_hooks/perf_hooks.test.ts
bun test v1.4.0 (d074a22ce)

test/js/node/perf_hooks/perf_hooks.test.ts:
(pass) stubs [15.16ms]
(pass) nodeTiming reports offsets from timeOrigin, not epoch timestamps [14.61ms]
(pass) doesn't throw [14.95ms]
(pass) Symbol name argument throws V8 wording [7.59ms]
(pass) measure(name, optionsWithoutStartOrEnd, endMark) honours the trailing endMark [4.96ms]
(pass) timerify entry shape [51.08ms]
(pass) timerify is exposed on both performance and as a top-level export (Node v25.2+) [1.77ms]
(pass) export surface matches Node v26.3.0 [7.35ms]
(pass) timerify and createHistogram survive Object.prototype option pollution [447.17ms]
(pass) nodeTiming initialization survives Object.prototype.value pollution [477.15ms]
(pass) timerify and AsyncResource.bind survive Object.prototype.get pollution [575.09ms]
(pass) net entries are instanceof PerformanceEntry [441.04ms]

 12 pass
 0 fail
 83 expect() calls
Ran 12 tests across 1 file. [4.41s]
__F:0:S:0

release with fix: all passed
$ bun scripts/build.ts --profile=release
[configured] bun-profile → bun (stripped)
  target       linux-x64-gnu
  build type   Release
  build dir    ./build/release
  revision     d074a22ce9
  features     baseline

22 deps, 108 codegen, 1171 objects in 914ms

ninja: Entering directory `/workspace/bun/build/release'
[1/1234] install /workspace/bun
bun install v1.4.0-canary.1 (1498d7b77)

Checked 124 installs across 170 packages (no changes) [25.00ms]
[2/1234] install /workspace/bun/packages/bun-error
bun install v1.4.0-canary.1 (1498d7b77)

Checked 1 install across 2 packages (no changes) [4.00ms]
[3/1234] gen ErrorCode+*.h
[4/1234] gen bindgenv2
[5/1234] fetch libjpeg-turbo
[libjpeg-turbo] up to date
[6/1234] install /workspace/bun/src/node-fallbacks
bun install v1.4.0-canary.1 (1498d7b77)

Checked 129 installs across 147 packages (no changes) [21.00ms]
[7/1234] fetch tinycc
[tinycc] up to date
[8/1234] fetch picohttpparser
[picohttpparser] up to date
[9/1234] gen .bind.ts → GeneratedBindings.cpp
[10/1234] gen JSBuffer.lut.h
Generating /workspace/bun/build/release/codegen/JSBuffer.lut.h from /workspace/bun/src/jsc/bindings/JSBuffer.cpp
[11/1234] fetch zlib
[zl
... (truncated)
diff hotspot
src/js/node/perf_hooks.ts                  | 18 +++++--
 test/js/node/perf_hooks/perf_hooks.test.ts | 81 ++++++++++++++++++++++++++++++
 2 files changed, 96 insertions(+), 3 deletions(-)

gate history · 1 passed · 0 rejected · iteration 0

evidence per changed file
file                                        reads  edits  tests
src/js/node/perf_hooks.ts                       4      7      0
test/js/node/perf_hooks/perf_hooks.test.ts      1      5      0

…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 #23041

Co-authored-by: 0xfandom <kashyapshivank01@gmail.com>
@robobun

robobun commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 1:31 AM PT - Jul 31st, 2026

@robobun, your commit d074a22ce9028cd7119c3bb86f17827a6f67df46 passed in Build #86095! 🎉


🧪   To try this PR locally:

bunx bun-pr 36518

That installs a local version of the PR into your bun-36518 executable, so you can run:

bun-36518 --bun

@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

PerformanceNodeTiming now exposes Node-compatible metadata, enumerable accessors, process-relative startup milestones, and regression coverage for values, serialization, and prototype pollution.

Changes

Performance node timing

Layer / File(s) Summary
Node timing shape and validation
src/js/node/perf_hooks.ts, test/js/node/perf_hooks/perf_hooks.test.ts
PerformanceNodeTiming now exposes enumerable name, entryType, startTime, and duration properties. Startup milestones use process-relative values, and startTime returns 0. Tests validate metadata, offsets, loopExit, enumerability, live duration, toJSON() output, and initialization with a polluted Object.prototype.

Possibly related PRs

  • oven-sh/bun#36069: Both PRs modify Node-compatible perf_hooks timing behavior and related tests.
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The changes satisfy issue #23041 by fixing startTime and duration access, offset semantics, and Node-compatible property shape.
Out of Scope Changes check ✅ Passed The descriptor and prototype-pollution changes support the linked issue’s requirement to match Node’s property shape.
Title check ✅ Passed The title clearly summarizes the primary change to report nodeTiming milestones as offsets from timeOrigin.
Description check ✅ Passed The description explains the change, cause, fix, correctness, and verification results with sufficient technical detail.

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: 1

🤖 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 `@test/js/node/perf_hooks/perf_hooks.test.ts`:
- Around line 14-52: Strengthen the nodeTiming coverage in the existing test by
asserting each listed milestone is exactly 0, not merely a number below the
epoch threshold. Replace the Object.keys check with own-property descriptor
checks for every nodeTiming field, requiring enumerable getter accessors. Add a
fresh bunExe() child test that sets Object.prototype.value before requiring
perf_hooks and verifies nodeTiming initializes successfully, covering
null-prototype accessor descriptor creation.
🪄 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: 51d78611-4273-430f-9462-b472ad1c634f

📥 Commits

Reviewing files that changed from the base of the PR and between 529adec and 81729ca.

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

Comment thread test/js/node/perf_hooks/perf_hooks.test.ts
@github-actions

Copy link
Copy Markdown
Contributor

This PR may be a duplicate of:

  1. fix(node:perf_hooks): report nodeTiming milestones as offsets from timeOrigin #32481 - Same fix for nodeTiming milestones as offsets from timeOrigin; explicitly superseded by this PR
  2. node:v8 profiling APIs, real perf_hooks nodeTiming, --diagnostic-dir and --heapsnapshot-signal (+11 tests) #35390 - Implements "real perf_hooks nodeTiming" which overlaps with the nodeTiming milestone changes in this PR

🤖 Generated with Claude Code

@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.

LGTM — small, well-scoped Node compat fix with a test that fails on the old behavior.

What was reviewed:

  • Verified the milestone values (0) and startTime (0) match Node's lib/internal/perf/nodetiming.js semantics (offsets from timeOrigin, not epoch ms).
  • Checked the own-property descriptor block: null-prototype outer + inner descriptors, consistent with the file's existing defineProperties calls; toJSON() still reads through correctly since the own getters shadow the prototype getters with identical values.
  • Confirmed no other tests in test/ assert on the old nodeTiming values (test-nodetiming-uvmetricsinfo.js only touches uvMetricsInfo; test-trace-events-bootstrap.js only lists milestone names).
  • loopStart/idleTime moving from 10 is a stub-value change with no downstream consumers.
Extended reasoning...

Overview

This PR touches two files: src/js/node/perf_hooks.ts (the node:perf_hooks compat module) and its test file. It fixes performance.nodeTiming to report startup milestones as offsets from timeOrigin (Node semantics) instead of absolute epoch timestamps, and adds own enumerable accessors for name/entryType/startTime/duration so Object.keys(nodeTiming) matches Node.

Security risks

None. This is a JS-only change to stub values in a Node compat module. The new descriptor object uses __proto__: null on both the outer object and each descriptor, following the file's existing prototype-pollution-safe pattern. No user input is parsed; no native code is touched.

Level of scrutiny

Low. nodeTiming in Bun is a stub — the previous values (performance.timeOrigin, 1) were already placeholders, and this PR just makes them the correct placeholders per Node's contract. The change is mechanical: replace epoch values with 0, hardcode startTime to 0, and mirror Node's own-property shape. The linked issue (#23041) and the OpenTelemetry use case make the motivation clear.

Other factors

  • The new test asserts startTime === 0, milestones < 1e12 (i.e., not epoch-scale), loopExit === -1, own-property enumerability, and the full toJSON() shape — good coverage for a stub fix.
  • The PR description confirms the test fails without the src change (Received: 1785472769724.3042), satisfying the "prove the test fails for the right reason" requirement.
  • I grepped for other test-suite consumers of nodeTiming values; none depend on the old behavior.
  • The duration own-getter closes over the global performance (not a captured local), but this is consistent with the pre-existing prototype getter on the same class, so it's not a regression introduced here.

…ties

name/entryType/startTime are own data properties (writable:false) in Node,
not accessor descriptors; only duration is a getter. Tighten the test to
assert the exact descriptor shape, assert duration > 0 (a live reading, not
just a number), and add coverage for Object.prototype.value pollution at
module load.

Co-authored-by: 0xfandom <kashyapshivank01@gmail.com>
Comment thread src/js/node/perf_hooks.ts Outdated
Comment thread src/js/node/perf_hooks.ts Outdated

@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: 1

🤖 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 `@test/js/node/perf_hooks/perf_hooks.test.ts`:
- Around line 44-53: Strengthen the live duration accessor assertions around nt
by requiring the property descriptor’s set field to be undefined. Capture the
initial positive json.duration value, await one macrotask, then obtain a later
duration reading and assert it is greater than the first, preserving the
existing toJSON verification.
🪄 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: 4c9c9bc1-804b-4b0f-9b53-c2cbe7dcbfef

📥 Commits

Reviewing files that changed from the base of the PR and between 81729ca and 9b2e271.

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

Comment thread test/js/node/perf_hooks/perf_hooks.test.ts

@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.

LGTM — small, well-scoped Node compat fix for nodeTiming stub values.

What was reviewed:

  • Milestone values switched from epoch timeOrigin to 0 offsets, startTime hardcoded to 0 — matches Node's lib/internal/perf/nodetiming.js contract.
  • Own-property descriptor shape (name/entryType/startTime as data props, duration as getter) with __proto__: null on each descriptor — verified the accessor descriptor for duration would otherwise collide with a polluted Object.prototype.value, and the subprocess test covers exactly that.
  • Checked that the module-level descriptor constant is safe to reuse (singleton nodeTiming), and that the now-shadowed prototype getters are harmless leftovers kept for the exported PerformanceNodeTiming class.
  • loopStart/idleTime moving 1 → 0 is still a placeholder but no less correct than before; loopExit stays -1.
Extended reasoning...

Overview

Two files touched: src/js/node/perf_hooks.ts (the built-in node:perf_hooks module) and its test file. The runtime change is confined to createPerformanceNodeTiming() and the prototype startTime getter: milestone fields (nodeStart, v8Start, environment, bootstrapComplete, loopStart, idleTime) are now initialized to 0 instead of performance.timeOrigin / 1, startTime returns 0, and four own enumerable properties (name, entryType, startTime as data; duration as a getter) are defined on the instance via a module-level null-prototype descriptor bag to match Node's own-property shape. No native code, no new API surface.

Security risks

None. This is a pure JS stub-value correction inside a Node compat module. The only security-adjacent aspect is the __proto__: null on each descriptor, which is defensive hardening against prototype pollution — the same pattern already used throughout this file for every other defineProperty/defineProperties call.

Level of scrutiny

Low. nodeTiming in Bun is a stub (Bun does not yet record real startup milestones), so the only observable contract is the shape and scale of the numbers. The previous values (~1.7e12 epoch ms) broke OpenTelemetry's bootstrap-span computation per #23041; 0 is the correct offset-from-timeOrigin placeholder and matches what Node returns for startTime. The own-property descriptor addition is verified against Node v26.3.0's actual descriptors in the PR description, and the prototype getters that are now shadowed remain in place only because PerformanceNodeTiming is (Bun-only) exported as a class.

Other factors

  • Two new tests added to the existing test file (correct location per repo conventions): one asserts the offset semantics, descriptor kinds, and toJSON() output; the other spawns a subprocess that pollutes Object.prototype.value before requiring the module to prove the null-prototype guard on the duration accessor descriptor is load-bearing. Both follow harness conventions (bunExe/bunEnv, concurrent pipe drain, exit-code asserted last).
  • All CodeRabbit and comment-cop threads are resolved; the author trimmed comments per the bot and justified declining the Bun.sleep(0) suggestion.
  • The duplicate-PR bot flags #35390 ("real nodeTiming"), but this stub fix is forward-compatible with that work — it just makes the placeholder values correct in the meantime.
  • No CODEOWNERS entry for src/js/node/perf_hooks.ts.

@robobun

robobun commented Aug 13, 2026

Copy link
Copy Markdown
Collaborator Author

Closing as a duplicate of #32481 by @0xfandom, which predates this PR, applies cleanly to current main and carries its own regression test. The only delta here (data-property descriptors for name/entryType/startTime, plus the descriptor and Object.prototype.value pollution tests) is noted on #32481 for the author to pick up if they want it. Review should continue there.

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

1 participant