Skip to content

node:perf_hooks: make performance the global object, like node - #33475

Closed
robobun wants to merge 2 commits into
mainfrom
farm/b4cc135f/perf-hooks-module-surface
Closed

node:perf_hooks: make performance the global object, like node#33475
robobun wants to merge 2 commits into
mainfrom
farm/b4cc135f/perf-hooks-module-surface

Conversation

@robobun

@robobun robobun commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator

What

require("node:perf_hooks").performance is now the global performance object itself, as in Node, instead of a separate wrapper.

Background

The module exported a hand-written facade that forwarded a hardcoded subset of the global performance to it. That subset had drifted from what the global actually carries:

  • markResourceTiming was never forwarded.
  • clearResourceTimings was a local function () {} no-op, shadowing the real method.
  • onresourcetimingbufferfull was a value snapshotted at module load, so assigning to it never reached the global.

And because it was a distinct object, require("node:perf_hooks").performance !== globalThis.performance, unlike Node where the two are the same object.

Fix

Export the global performance directly. The only members Node's performance carries that the web Performance lacks are nodeTiming, eventLoopUtilization, and timerify (the last from #31825, which has since merged); define those on the Performance prototype, non-enumerable, exactly as Node does (verified against node -e on v26.3.0: all three are PROTO, enumerable=false, and Object.keys(performance) is unchanged). With the node-only members on the prototype, the module object can be the global itself, and every per-method forwarder is gone.

eventLoopUtilization and timerify are also exported at the module level, read off performance after the prototype install so they share function identity with performance.<name>, matching Node's lib/perf_hooks.js module.exports.

One implementation note

The three node-only members are attached to the prototype from the node: layer (src/js/node/perf_hooks.ts) rather than ported into JSPerformance.cpp:

The one behavioral nuance versus Node: the members attach when node:perf_hooks is first required, rather than being present on a bare globalThis.performance before any require. They are node-only and weren't on the global at all before this change, so this is strictly closer to Node; making them eager is a follow-up that belongs with the C++ move.

Merge-conflict resolution against #31825

#31825 added timerify (and its processTimerifyComplete helper, plus the PerformanceNodeEntry class and enqueueNodeEntry) on the facade and as a module-level export. All of that is kept unchanged here; the only change relative to #31825 is where timerify lives on performance: on Performance.prototype alongside nodeTiming/eventLoopUtilization, instead of on a separate facade object. The timerify tests, PerformanceNodeEntry shape checks, and prototype-pollution tests from #31825 all still pass.

Verification

bun bd test test/js/node/perf_hooks/perf_hooks.test.ts → 13 pass. With src/js/node/perf_hooks.ts reverted to main, 4 of the 13 fail (the single-object assertions; #31825's timerify tests still pass against main as expected).

Node parity confirmed side by side on v26.3.0: perf_hooks.performance === globalThis.performance, nodeTiming/eventLoopUtilization/timerify present and on the prototype non-enumerable, module-level eventLoopUtilization/timerify with matching identity.

test-perf-hooks-timerify-histogram-async.mjs, test-performance-function-async.js, test-net-perf_hooks.js, test-performance-measure.js and test/js/web/timers/performance.test.js all pass.


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

fails on main (without fix)
ASAN without fix: 5 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
info: syncing channel updates for nightly-2026-05-06-x86_64-unknown-linux-gnu
info: latest update on 2026-05-06 for version 1.97.0-nightly (e95e73209 2026-05-05)
info: component rust-src is up to date
info: checking for self-update (current version: 1.29.0)
bun test v1.4.0 (491bf6b6d)

test/js/node/perf_hooks/perf_hooks.test.ts:
4 | import perf, { PerformanceObserver } from "perf_hooks";
5 | 
6 | // Like node, require("node:perf_hooks").performance is the global performance
7 | // object itself, not a separate wrapper.
8 | test("perf_hooks.performance is the global performance object", () => {
9 |   expect(perf.performance).toBe(globalThis.performance);
                               ^
error: expect(received).toBe(expected)

Expected: Performance {
  now: [Function: now],
  timeOrigin: 1784376391811.121,
  timing: PerformanceTiming {
    navigationStart: 0,
    unloadEventStart: 0,
    unloadEventEnd: 0,
    redirectStart: 0,
    redirectEnd: 0,
    fetchStart: 0,
    domainLookupStart: 0,
   
... (truncated)

release without fix: all passed
bun test v1.4.0-canary.1 (491bf6b6d)

test/js/node/perf_hooks/perf_hooks.test.ts:
(pass) perf_hooks.performance is the global performance object [0.95ms]
(pass) doesn't throw [0.29ms]
(pass) timerify entry shape [1.72ms]
(pass) timerify is exposed on both performance and as a top-level export (Node v25.2+) [0.03ms]
(pass) export surface matches Node v26.3.0 [0.16ms]
(pass) timerify and createHistogram survive Object.prototype option pollution [19.99ms]
(pass) timerify and AsyncResource.bind survive Object.prototype.get pollution [14.17ms]
(pass) net entries are instanceof PerformanceEntry [10.71ms]
(pass) node-only members are present on performance [0.11ms]
(pass) resource-timing methods are present and callable [0.07ms]
(pass) node-only members live on the prototype, non-enumerable [0.12ms]
(pass) node-only members are installed onto the prototype when node:perf_hooks is loaded [23.47ms]
(pass) onresourcetimingbufferfull is the global's accessor [0.18ms]
(pass) perf_hooks exports eventLoopUtilization at the module level [0.12ms]

 14 pass
 0 fail
 71 expect() calls
Ran 14 tests across 1 file. [440.00ms]
__F:0:S:0
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
info: syncing channel updates for nightly-2026-05-06-x86_64-unknown-linux-gnu
info: latest update on 2026-05-06 for version 1.97.0-nightly (e95e73209 2026-05-05)
info: component rust-src is up to date
info: checking for self-update (current version: 1.29.0)
bun test v1.4.0 (491bf6b6d)

test/js/node/perf_hooks/perf_hooks.test.ts:
(pass) perf_hooks.performance is the global performance object [8.35ms]
(pass) doesn't throw [25.13ms]
(pass) timerify entry shape [63.56ms]
(pass) timerify is exposed on both performance and as a top-level export (Node v25.2+) [2.23ms]
(pass) export surface matches Node v26.3.0 [8.91ms]
(pass) timerify and createHistogram survive Object.prototype option pollution [755.74ms]
(pass) timerify and AsyncResource.bind survive Object.prototype.get pollution [819.99ms]
(pass) net entries are instanceof PerformanceEntry [498.69ms]
(pass) node-only members are present on performance [5.06ms]
(pass) resource-timing methods are present and callable [5.18ms]
(pass) node-only membe
... (truncated)

release with fix: all passed
$ bun scripts/build.ts --profile=release
info: syncing channel updates for nightly-2026-05-06-x86_64-unknown-linux-gnu
info: latest update on 2026-05-06 for version 1.97.0-nightly (e95e73209 2026-05-05)
info: component rust-src is up to date
info: checking for self-update (current version: 1.29.0)
[configured] bun-profile → bun (stripped) in 742ms (unchanged)
ninja: Entering directory `/workspace/bun/build/release'
[1/20] gen JS modules (bundle-modules)
Preprocess modules (7934ms)
Bundle modules (37ms)
Postprocesss modules (152ms)
Bundle Functions (765ms)
Generate Code (81ms)

[8.99s] Bundled "src/js" for production
  2038 kb
  165 internal modules
  13 native modules
  90 internal functions across 19 files
[build] done
bun test v1.4.0-canary.1 (491bf6b6d)

test/js/node/perf_hooks/perf_hooks.test.ts:
(pass) perf_hooks.performance is the global performance object [0.58ms]
(pass) doesn't throw [0.18ms]
(pass) timerify entry shape [0.97ms]
(pass) timerify is exposed on both performance and as a top-level export (Node v25.2+) [0.02ms]
(pass) export surface matches Node v26.3.0 [0.12ms]
(pass) timerify and createHistogram survive Object.prototype option pollution [15.2
... (truncated)
diff hotspot
src/js/node/perf_hooks.ts                  |  93 +++++++++++--------------
 test/js/node/perf_hooks/perf_hooks.test.ts | 105 +++++++++++++++++++++++++++--
 2 files changed, 139 insertions(+), 59 deletions(-)

gate history · 6 passed · 1 rejected · iteration 9

evidence per changed file
file                                        reads  edits  tests
src/js/node/perf_hooks.ts                      11     17      0
test/js/node/perf_hooks/perf_hooks.test.ts     12     15      0

@coderabbitai

coderabbitai Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Changes

Performance API integration

Layer / File(s) Summary
Install Node-specific Performance members
src/js/node/perf_hooks.ts, test/js/node/perf_hooks/perf_hooks.test.ts
Node-specific properties are added to Performance.prototype as guarded, non-enumerable members, with coverage for timing, utilization, resource methods, enumeration, and shared accessors.
Export the underlying performance object
src/js/node/perf_hooks.ts, test/js/node/perf_hooks/perf_hooks.test.ts
perf_hooks.performance directly references the global performance object, and module-level timerify and eventLoopUtilization exports match its properties.

Possibly related PRs

Suggested reviewers: cirospaciari

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly states the main change: perf_hooks.performance now aliases the global performance object.
Description check ✅ Passed The PR includes the required What and Verification content, plus enough background and testing detail to understand the change.

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

@github-actions github-actions Bot added the claude label Jul 6, 2026
@robobun

robobun commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 7:57 AM PT - Jul 18th, 2026

@robobun, your commit 491bf6b has 4 failures in Build #75414 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 33475

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

bun-33475 --bun

@github-actions

github-actions Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Found 1 issue this PR may fix:

  1. performance.timerify is not a function #9271 - Reports performance.timerify is not a function from node:perf_hooks; this PR implements timerify() directly

If this is helpful, copy the block below into the PR description to auto-close this issue on merge.

Fixes #9271

🤖 Generated with Claude Code

@github-actions

github-actions Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Comment thread src/js/internal/perf_hooks/timerify.ts Outdated
Comment thread src/js/internal/perf_hooks/timerify.ts Outdated
Comment thread src/js/node/perf_hooks.ts Outdated
@robobun
robobun force-pushed the farm/b4cc135f/perf-hooks-module-surface branch from 71d50b4 to 89c5622 Compare July 6, 2026 12:15
@robobun robobun changed the title node:perf_hooks: complete the module's performance surface, add timerify node:perf_hooks: stop the module's performance from dropping the global's API Jul 6, 2026
@robobun

robobun commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator Author

Rescoped: the timerify implementation that was in the first push is gone, and the PR is now only about the performance facade.

#31825 already implements timerify, on the same node-entry observer registry, with a proper PerformanceNodeEntry rather than the plain entry object I had. No point carrying a second one, so I dropped mine and left a note there. Between the two PRs the whole gap is covered, and the diffs are disjoint.

On the three review comments from the first push:

One more thing that fell out of writing that test: the clearResourceTimings test I had originally passed both with and without the fix, since the old no-op was also a function. Both forwarding tests now shadow the global's method with a jest.fn() and assert the facade delegates to it with the right arguments. 6 of the 8 tests fail with src/js/node/perf_hooks.ts reverted to main.

Comment thread src/js/node/perf_hooks.ts Outdated
@robobun
robobun force-pushed the farm/b4cc135f/perf-hooks-module-surface branch from 89c5622 to 4bd4e7f Compare July 6, 2026 13:09
Comment thread src/js/node/perf_hooks.ts Outdated

@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 — the v26 clarification on the module-level eventLoopUtilization export addresses my last comment, and the remaining inline note is a test-quality nit that doesn't block.

Extended reasoning...

Overview

This PR fixes the node:perf_hooks module's performance facade so it forwards every member the global performance object carries, rather than a hand-maintained subset that had drifted. Concretely: adds forwarders for markResourceTiming, addEventListener/removeEventListener/dispatchEvent; converts clearResourceTimings from a local no-op to a real forwarder; converts onresourcetimingbufferfull from a load-time value snapshot to a live getter/setter; adds a module-level eventLoopUtilization export; removes a dead commented-out block. Two files touched: src/js/node/perf_hooks.ts (the facade) and test/js/node/perf_hooks/perf_hooks.test.ts (six new tests, including one that walks the global's full prototype chain and pins the missing set to exactly ["timing"]).

Security risks

None. This is a pure JS compat-layer change that adds forwarding shims to an existing global object. No native code, no user input parsing, no auth/crypto/permissions surface.

Level of scrutiny

Low-to-medium. The forwarders are mechanical one-liners following the file's existing method(_) { return performance.method(...arguments); } pattern verbatim. The getter/setter for onresourcetimingbufferfull is the obvious correct shape. The prototype-chain-walking test is a good drift guard. The one design-ish decision — the module-level eventLoopUtilization export — is permissive-only and the author has now cited Node v26's lib/perf_hooks.js module.exports with an explicit note that it's absent on v22 and earlier, which addresses my previous round's concern (I had verified against v22).

Other factors

This PR has been through three review rounds already. Round 1's timerify-related feedback became moot when the author rescoped (deferring timerify to #31825, which they cross-linked with the __proto__: null finding). Round 2's "missing timerify" was a stale-description misread on my part that the author corrected. Round 3's eventLoopUtilization comment was addressed in commit 5220ad2 by rewording the test comment to cite Node's source and the version boundary rather than the docs. The author has been responsive and precise throughout.

The one new inline nit this round (EventTarget test would hang to timeout rather than fail fast on regression, since dispatchEvent is synchronous) is valid but purely about failure-mode diagnostics — the test is correct today and would still fail on regression, just via timeout. Not worth another round-trip.

Comment thread test/js/node/perf_hooks/perf_hooks.test.ts Outdated
@robobun

robobun commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator Author

Addressed the EventTarget test nit in ae6915a. It was right on substance, though the specific regression it suggested behaves differently here: dropping the spread entirely (performance.addEventListener(arguments[0])) makes WebCore's addEventListener throw TypeError: Not enough arguments, so even the old test failed fast on that one.

The shape that actually bites is the listener reaching the global as undefined:

$ bun-debug -e 'performance.addEventListener("y", undefined); console.log(performance.dispatchEvent(new Event("y")))'
AddEventListenerArgumentTypeWarning: addEventListener called with undefined listener, which has no effect.
true

Silent no-op, and dispatchEvent still returns true. So I injected exactly that into the forwarder and ran it against both versions of the test:

old (Promise.withResolvers) new (jest.fn())
silent-no-op regression .toBe(true) passes, resolve never fires, promise never settles → 5s hang, "timed out" fails in 41ms: Expected number of calls: 1 / Received number of calls: 0

Which is the point: .toBe(true) on dispatchEvent's return value asserts !defaultPrevented, not that a listener ran, so it could never have caught this. The test now uses the same jest.fn() shape as expectForwardsToGlobal below it and reads the event off listener.mock.calls[0][0].

That makes three assertions in this PR that I'd originally written so they couldn't fail: this one, and the clearResourceTimings / markResourceTiming checks that only asserted the member was a function (the old no-op was also a function). All three now fail on the thing they claim to guard.

@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 — every prior review point has been addressed, and the drift-guard test that pins the facade's missing members to exactly ["timing"] should keep this from regressing.

Extended reasoning...

Overview

This PR touches two files: src/js/node/perf_hooks.ts (a built-in JS module) and test/js/node/perf_hooks/perf_hooks.test.ts. It fixes the node:perf_hooks module's performance facade, which had drifted from the global performance object it wraps: markResourceTiming was never forwarded, clearResourceTimings was a local no-op shadowing the real method, onresourcetimingbufferfull was a load-time value snapshot rather than a live getter/setter, and the three EventTarget methods were missing entirely. It also adds eventLoopUtilization as a module-level export to match Node v26.3.0's lib/perf_hooks.js.

Every new forwarder follows the file's existing method(_) { return performance.method(...arguments); } pattern verbatim. A dead comment block was removed. Six new tests cover each fix, including a drift-guard that walks the global's full prototype chain and asserts the facade's missing-member set is exactly ["timing"] (the one web-only member Node's performance lacks).

Security risks

None. This is pure JS forwarding code with no user-input parsing, no auth, no crypto, no filesystem or network access. The forwarders delegate verbatim to the existing global performance object.

Level of scrutiny

Low. The changes are mechanical additions following an established in-file pattern, in a Node-compat shim. No native code is touched. The one design question — whether the facade should exist at all vs. exporting the global directly — is pre-existing and out of scope; this PR correctly follows the file's convention rather than redesigning it. The PR description explicitly notes markResourceTiming remains a no-op stub in JSPerformance.cpp and that making it record entries is separate work.

Other factors

This PR has been through four review rounds from me, and every point was addressed:

  • The "every member forwarded" comment overclaim → fixed by actually forwarding the EventTarget methods and adding the prototype-chain drift-guard test.
  • The timerify rescope confusion → author clarified it was intentional (deferred to #31825, which has a better implementation) and rewrote title/description to match.
  • The eventLoopUtilization module-export claim → author demonstrated it against Node v26.3.0's actual module.exports and updated the test comment to cite that source rather than docs.
  • The Promise.withResolvers nit on the EventTarget test → replaced with the synchronous jest.fn() pattern in ae6915a.

The tests are well-constructed: the forwarding tests shadow the global's method with a spy and assert delegation with the exact arguments (so the old clearResourceTimings no-op would fail them), and the author verified 6 of 8 fail with src/ reverted. The bug hunting system found nothing this round. The PR notes disjoint overlap with #31825 and #32618 in the same file.

@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 — all prior feedback addressed, forwarders match the existing pattern in the file, and the prototype-chain test pins future drift.

Extended reasoning...

Overview

Two files: src/js/node/perf_hooks.ts (the built-in module facade) and its test file. The change adds five method forwarders (addEventListener/removeEventListener/dispatchEvent/markResourceTiming/clearResourceTimings) to the module's performance object, converts onresourcetimingbufferfull from a load-time value snapshot to a live getter/setter pair, adds a module-level eventLoopUtilization export (present in Node v26's lib/perf_hooks.js module.exports), and deletes a dead commented-out block. Every new forwarder uses the exact method(_) { return performance.method(...arguments); } shape already used by the ~10 existing forwarders in the same object literal.

Security risks

None. This is pure JS in a built-in module, forwarding to APIs the global performance object already exposes. No new attack surface, no user input parsing, no native code.

Level of scrutiny

Low. This is a mechanical Node-compat gap fix: the facade had drifted from the object it wraps, and the fix is to add the missing forwarders in the same style as the neighbours. The one non-mechanical decision — whether eventLoopUtilization belongs at the module level — was litigated in a prior review round and settled by citing Node v26.3.0's actual module.exports list.

Other factors

This PR has been through four review rounds with me, and every point has been addressed substantively rather than dismissed: the EventTarget methods were added (not just the comment reworded), the clearResourceTimings/markResourceTiming tests were rewritten to spy on the global so they actually fail without the fix, the EventTarget test was rewritten from a hang-on-regression Promise shape to a fail-fast jest.fn() assertion, and the eventLoopUtilization test comment now cites Node's source rather than its docs. The new "does not drop members" test walks the global's full prototype chain and asserts the missing set is exactly ["timing"], which is a real regression guard against the facade drifting again. The bug-hunting system found nothing on the current revision. All prior inline threads are resolved.

@robobun

robobun commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator Author

CI status: the diff is green, both red builds are infrastructure

Flagging this for a maintainer rather than pushing again. Two consecutive builds went red without a single test failure attributable to this diff, which is two files confined to node:perf_hooks.

Build 69029 — 281/287 jobs passed, zero jobs in a failed state. The build was marked failing by:

  • :debian: 13 x64-asan - test-bunexpired, i.e. no agent ever picked it up, so it never ran.
  • :eyes:broken, dependent on the above.

Build 69101 (the re-trigger) — one hard failure, :darwin: 26 aarch64 - test-bun, which ran no tests at all:

Error: buildkite-agent artifact download timed out after 120s for step 'darwin-aarch64-build-bun'.
Refusing to continue with a partial download (would silently fall back to the wrong binary).
    at getExecPathFromBuildKite (scripts/runner.node.mjs:2182:13)

It died fetching the build artifact before executing anything. The two other red marks on that build are soft-fail warning annotations on Windows 2019 x64-baseline, both already auto-retried and both network flakes unrelated to this change:

  • test-net-dns-custom-lookup.jsconnect ECONNREFUSED ::1:50279
  • node-http-connect.test.ts — "should handle partial writes and buffering"

Why none of it can be this diff

The change touches src/js/node/perf_hooks.ts and its own test file, nothing else. grep -rn perf_hooks src/bake/ test/bake/ is empty, there is no path from a node:perf_hooks facade to an artifact download, a DNS lookup, or an HTTP CONNECT partial write, and the one hard failure never reached a test.

Locally, on a debug+ASAN build: bun bd test test/js/node/perf_hooks/perf_hooks.test.ts → 8 pass. With src/js/node/perf_hooks.ts reverted to main, 6 of the 8 fail. cookie-map, test-net-perf_hooks, test-http-perf_hooks, test-performance-measure, test-performanceobserver-gc and test/js/web/timers/performance.test.js all pass.

I have used my one re-trigger (a1745422f, an empty commit that can be squashed away) and am not going to push another, since each push only queues more of the same. This needs someone with write_builds to re-run the two infra jobs, or a merge.

@dylan-conway

Copy link
Copy Markdown
Member

are the performance methods forwarded because the perf_hooks.performance object is not supposed to be the same object? if not, can we simplify this?

@robobun

robobun commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator Author

Good question. The forwarding isn't a design goal in itself, it falls out of two facts:

  1. Bun's global performance is the WebCore web Performance (a real EventTarget), and it carries none of the node-specific members (nodeTiming, eventLoopUtilization, and the timerify that async_hooks,events,http,http2,perf_hooks: port Node.js async compatibility tests and fix the gaps they surface — ALS run/disable + withScope/defaultValue, http client ALS across reused agent sockets, http2 ALS context, AsyncResource.bind, EventEmitterAsyncResource, timerify (+22 tests) #31825 adds). So node:perf_hooks needs an object that is a superset of the web global.

  2. The obvious way to build that superset, wrapping the global, doesn't work, because WebCore's methods brand-check their receiver:

$ bun -e 'Object.create(performance).mark("x")'
Can only call Performance.mark on instances of Performance

Object.create(performance) (or a Proxy) calls mark with the wrapper as this, the jsDynamicCast<JSPerformance> fails, and it throws. { ...performance } is no good either: the methods are non-enumerable prototype methods, so a spread only copies now and timeOrigin. That's why each method is a one-liner that re-binds this to the real global.

So within the current shape (a separate facade object) the forwarders are required, not incidental.

The real simplification is to drop the separate object: export globalThis.performance directly from node:perf_hooks, and move the node-only members onto the C++ JSPerformance global, next to where markResourceTiming already lives. Then node:perf_hooks is a couple of lines, there are no forwarders, and require("node:perf_hooks").performance === globalThis.performance like Node.

The tradeoff is what that does to the web global: globalThis.performance would then carry nodeTiming / eventLoopUtilization / timerify, which aren't part of the web Performance interface (browsers don't expose them). Node does exactly this, since its global and its perf_hooks.performance are one object. Bun's usual lean is the opposite, keep node-specific surface in the node: layer and leave the web global web-shaped, which is what the facade does today.

I'm happy to do the C++ version and delete the facade if you'd prefer the single-object model. Which way do you want it?

@dylan-conway

Copy link
Copy Markdown
Member

@robobun yes please do that. that's the proper fix

@robobun
robobun force-pushed the farm/b4cc135f/perf-hooks-module-surface branch from a174542 to 33a8fe1 Compare July 7, 2026 00:34
@robobun robobun changed the title node:perf_hooks: stop the module's performance from dropping the global's API node:perf_hooks: make performance the global object, like node Jul 7, 2026
@robobun

robobun commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator Author

Done, in 33a8fe1 (force-pushed, squashed to one commit). require("node:perf_hooks").performance === globalThis.performance now, and every forwarder is gone. Diff is net-negative.

The only members the web Performance lacked were nodeTiming and eventLoopUtilization; those go on the Performance prototype, non-enumerable, which is exactly where node keeps them (checked with node -e: both PROTO, enumerable=false, Object.keys(performance) unchanged).

One call I made that's worth your eyes: I attached those two from the node: layer rather than porting them into JSPerformance.cpp. nodeTiming is a faked PerformanceEntry and eventLoopUtilization is the zeroed stub, both already in JS, and #32618 (real eventLoopUtilization) and #31825 (timerify) are actively changing them there, so a C++ move now would collide with both. The if (!("nodeTiming" in prototype)) guards make it forward-compatible: if these do move into C++ later, the JS defs are skipped instead of clobbering. The one nuance is that they attach on first require("node:perf_hooks") rather than being present on a bare global beforehand; since they weren't on the global at all before, this is strictly closer to node, and eager-on-the-global is the natural follow-up when the C++ move happens. Happy to do that move too if you'd rather it land here. Details in the updated description.

Comment thread src/js/node/perf_hooks.ts Outdated
Comment thread src/js/node/perf_hooks.ts Outdated
@robobun
robobun force-pushed the farm/b4cc135f/perf-hooks-module-surface branch from 33a8fe1 to 9bc8660 Compare July 7, 2026 00:58
@robobun

robobun commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator Author

Build 69519 on the restructured 9bc8660 hit the same :darwin: 26 aarch64 - test-bun infra failure:

Error: buildkite-agent artifact download timed out after 120s for step 'darwin-aarch64-build-bun'.
Refusing to continue with a partial download (would silently fall back to the wrong binary).

That lane checked out 9bc8660 and then died fetching the build artifact before running any test. That's the third consecutive build where this lane never got past artifact download (69029 expired waiting for an agent, 69101 and 69519 both timed out on the download). 187 other jobs on this build are green; 69 were still scheduled when it was marked failing.

The code here is now the single-object restructure, so the earlier CI status note about the facade-era builds is superseded. Locally on debug+ASAN: 7 pass, 5 fail with src/ reverted to main, and test/js/web/timers/performance.test.js + the test-net-perf_hooks / test-performance-measure / test-performanceobserver-gc parallel tests all pass.

My one re-trigger is spent. Needs someone with write_builds to re-run the darwin lane, or a merge.

@robobun

robobun commented Jul 18, 2026

Copy link
Copy Markdown
Collaborator Author

Rebased onto main after #31825 merged and resolved the conflict (566c107).

#31825's timerify(), processTimerifyComplete(), PerformanceNodeEntry and enqueueNodeEntry are all kept verbatim. The only change relative to #31825 is where timerify lands on performance: on Performance.prototype (non-enumerable, same slot as node) alongside nodeTiming/eventLoopUtilization, rather than on the facade object this PR deletes. The module-level timerify export reads off performance post-install, so perf.timerify === perf.performance.timerify holds the same way it does for eventLoopUtilization.

All 13 tests pass (#31825's timerify / PerformanceNodeEntry / prototype-pollution tests plus this PR's single-object tests), as do test-perf-hooks-timerify-histogram-async.mjs and test-performance-function-async.js. 4 of the 13 fail with src/ reverted to main.

This also resolves #9271, now that timerify is reachable on both perf_hooks.performance and the global.

@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 184-196: Add a fresh-subprocess test near the existing
prototype-enumerability test that checks node-only performance members are
absent before loading node:perf_hooks and present afterward. Cover the relevant
flag/state variants using isolated subprocesses, and assert the post-require
properties are installed on the prototype without changing the existing
in-process assertions.
🪄 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: 3803e03a-438d-4063-b9d1-bbd31796eab7

📥 Commits

Reviewing files that changed from the base of the PR and between 0170259 and 566c107.

📒 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
Comment thread src/js/node/perf_hooks.ts Outdated
@robobun
robobun force-pushed the farm/b4cc135f/perf-hooks-module-surface branch from 5b07c1b to 017ac0b Compare July 18, 2026 04:17

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/js/node/perf_hooks.ts (1)

292-299: 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Implement snapshot-based eventLoopUtilization semantics
src/js/node/perf_hooks.ts#L117 still returns { idle: 0, active: 0, utilization: 0 }, so the prototype and module-level exports are exposing a stub instead of Node’s delta-based API. Tighten the tests in test/js/node/perf_hooks/perf_hooks.test.ts#L163-L173 and #L245-L253 to assert snapshot/delta behavior, not just numeric fields.

🤖 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 292 - 299, Replace the stub
implementation used by eventLoopUtilization in src/js/node/perf_hooks.ts
(including the prototype site at lines 292-299 and module export site at lines
314-317) with snapshot-based Node-compatible behavior supporting initial and
previous-snapshot delta calls. Update the assertions in
test/js/node/perf_hooks/perf_hooks.test.ts lines 163-173 and 245-253 to verify
snapshot/delta relationships rather than only numeric fields.
🤖 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.

Outside diff comments:
In `@src/js/node/perf_hooks.ts`:
- Around line 292-299: Replace the stub implementation used by
eventLoopUtilization in src/js/node/perf_hooks.ts (including the prototype site
at lines 292-299 and module export site at lines 314-317) with snapshot-based
Node-compatible behavior supporting initial and previous-snapshot delta calls.
Update the assertions in test/js/node/perf_hooks/perf_hooks.test.ts lines
163-173 and 245-253 to verify snapshot/delta relationships rather than only
numeric fields.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 448d27ed-6b1e-434c-a743-45c75ea07e7e

📥 Commits

Reviewing files that changed from the base of the PR and between 566c107 and 017ac0b.

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

@robobun

robobun commented Jul 18, 2026

Copy link
Copy Markdown
Collaborator Author

Not taking the eventLoopUtilization implementation suggestion. The zeroed stub is what main has today and this PR does not change a line of its body; it only moves the function from a separate facade onto Performance.prototype. A real snapshot-based eventLoopUtilization needs native event-loop idle/active timing (usockets loop data, Rust-side wiring, worker-thread plumbing), which is exactly what #32618 is building. Doing that here would duplicate an open PR and double the scope of this one, whose job is the single-object identity.

The tests deliberately assert the shape ({ idle: Number, active: Number, utilization: Number }) rather than the stub's zeros, so they will keep passing when #32618 lands the real values.

Comment thread test/js/node/perf_hooks/perf_hooks.test.ts Outdated
@robobun
robobun force-pushed the farm/b4cc135f/perf-hooks-module-surface branch from 017ac0b to db8aa96 Compare July 18, 2026 04:30
Comment thread src/js/node/perf_hooks.ts Outdated
@robobun
robobun force-pushed the farm/b4cc135f/perf-hooks-module-surface branch from db8aa96 to d9f9767 Compare July 18, 2026 04:51

@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 — maintainer-endorsed simplification, all prior review threads resolved.

What was reviewed:

  • Prototype install block: targets captured Performance.prototype, hasOwn-guarded, __proto__: null descriptors, wrapped in if (PerformancePrototype) so a deleted global degrades rather than throws.
  • Module-level eventLoopUtilization/timerify exports read off the same prototype with local fallbacks, so identity holds and a replaced global can't undefine them.
  • Tests cover object identity, prototype placement/non-enumerability, fresh-subprocess before/after install, onresourcetimingbufferfull accessor round-trip, and resource-timing methods that the old facade dropped; subprocess stderr assertion is guarded per house style.
Extended reasoning...

Overview

Two files: src/js/node/perf_hooks.ts replaces the hand-written forwarding facade with performance (the global) directly, and installs the three Node-only members (nodeTiming, eventLoopUtilization, timerify) on Performance.prototype non-enumerable, matching Node v26.3.0. test/js/node/perf_hooks/perf_hooks.test.ts adds ~95 lines of coverage for the identity, prototype placement, and the accessor/method drift the facade had accumulated. Net: −52/+40 in src, deleting more forwarder boilerplate than it adds.

Security risks

None identified. JS-only change in a node: compat module; no native code, no auth/crypto/fs. The prototype-install descriptors use __proto__: null and the guards use Object.hasOwn, so prototype pollution can neither redirect the define nor suppress it — both concerns were raised in earlier review rounds and fixed.

Level of scrutiny

Medium. It does introduce a process-global side effect (mutating Performance.prototype on first require), but that is exactly what Node does and was the design dylan-conway explicitly requested ("that's the proper fix"). The change has been through five rounds of inline review on this PR (tamper-resistance of the define target, hasOwn vs in, export identity, comment length, subprocess stderr flake, degrade-not-throw guard, read-site consistency) — every thread is resolved with a corresponding commit, and the bug-hunting pass on the final revision found nothing.

Other factors

  • Maintainer (dylan-conway) endorsed the single-object design before implementation.
  • Tests fail on main and pass on the branch per the gate evidence in the PR body; the eventLoopUtilization shape assertions use expect.any(Number) so #32618's real implementation won't break them.
  • The one known behavioral nuance (members attach lazily on first require rather than eagerly) is documented in the PR body and pinned by a fresh-subprocess test so a future C++ move will trip CI rather than drift silently.
  • No outstanding reviewer comments; CodeRabbit's suggestion was addressed and confirmed.

@robobun
robobun force-pushed the farm/b4cc135f/perf-hooks-module-surface branch from d9f9767 to a7e00ad Compare July 18, 2026 05:13

@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 — the facade → global-object restructure is what dylan-conway asked for, and every review thread since has been addressed.

What was reviewed:

  • Prototype-install block: targets Performance.prototype (not getPrototypeOf), hasOwn guards, __proto__: null descriptors, and the if (PerformancePrototype) degrade-not-throw guard all match the file's existing hardening conventions.
  • Module-level eventLoopUtilization/timerify exports read off the same prototype the defines target, with local fallbacks — identity with performance.<name> holds and survives a replaced global or a future native move.
  • Tests: identity, prototype placement/non-enumerability, resource-timing methods now reachable, onresourcetimingbufferfull accessor round-trips, and a fresh-subprocess lazy-install contract; subprocess assertions use the guarded-stderr / combined-object pattern.
Extended reasoning...

Overview

Two files: src/js/node/perf_hooks.ts replaces the hand-written per-method facade with the global performance object itself, installing the three node-only members (nodeTiming, eventLoopUtilization, timerify) onto Performance.prototype non-enumerably so the module can export the global directly. test/js/node/perf_hooks/perf_hooks.test.ts adds coverage for identity, prototype placement, the previously-missing resource-timing methods, the onresourcetimingbufferfull accessor, module-level export identity, and a fresh-subprocess check pinning the lazy-install contract.

Security risks

None. This is a Node-compat reshaping of an existing built-in JS module — no native code, no auth/crypto/permissions, no untrusted-input parsing. The prototype install uses __proto__: null descriptors and Object.hasOwn guards, and targets the captured Performance.prototype rather than getPrototypeOf(performance), so a preload-replaced global cannot redirect the define onto Object.prototype.

Level of scrutiny

Medium. It changes user-visible Node-compat surface (mutates Performance.prototype on first require), but the direction was explicitly requested by a maintainer ("that's the proper fix"), it matches Node v26.3.0's observable placement (verified in the PR body against node -e), and it has been through five rounds of review on this thread with every point applied and re-verified. The one behavioral nuance vs. Node — lazy install on first require rather than eager — is documented, strictly closer to Node than before, and now CI-enforced by the fresh-subprocess test.

Other factors

All prior inline comments (mine, CodeRabbit's, and the maintainer's structural request) are resolved in the current diff: Performance.prototype over getPrototypeOf, hasOwn over in, module-level exports read off PerformancePrototype with ?? local fallbacks, the 3-line comment cap, the guarded expect(stderr) pattern, and the if (PerformancePrototype) degrade-not-throw guard matching the file's existing if (PerformanceEntry) convention. #31825's timerify implementation is preserved verbatim; only its attachment point moved. The bug-hunting system found nothing this run. Net diff is ~-13 lines of source with substantially more test coverage than before.

@robobun

robobun commented Jul 18, 2026

Copy link
Copy Markdown
Collaborator Author

Build 75149 on a7e00ad finalized 283 pass / 3 fail, with zero perf_hooks failures on any lane. The three red lanes are two main breaks, both already owned:

  • test/bake/deinitialization.test.ts — segfault at 0xfffffffffffffff8 in the bake dev-server deinit path on alpine aarch64. No node:perf_hooks in that test.
  • test/cli/run/no-orphans.test.ts — the --no-orphans (perl) fast-exit-intermediate case timed out at 30s on both darwin aarch64 lanes. Process-reaping test, no perf_hooks.

Both were reported and deduped against existing main-break sessions. This PR's diff is two files (src/js/node/perf_hooks.ts, test/js/node/perf_hooks/perf_hooks.test.ts), and the test file it adds passed on every lane that ran. Ready for a maintainer with these two main breaks set aside; my re-trigger is spent so I'm not pushing again.

@robobun
robobun force-pushed the farm/b4cc135f/perf-hooks-module-surface branch from a7e00ad to d434d9f Compare July 18, 2026 07:59

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

No bugs found across four review iterations, and every prior inline nit (prototype target, hasOwn guards, export identity, comment length, subprocess-stderr pattern, degrade-not-throw guard) has been applied and re-verified. Deferring to a maintainer only because the core change — installing node-only members onto the web Performance.prototype at first require from the JS layer rather than in JSPerformance.cpp — is an architectural call on a global's shape that's worth a human sign-off.

What was reviewed:

  • Prototype-install block: Performance?.prototype target, hasOwn guards, __proto__: null descriptors, degrade-not-throw on deleted/replaced globals — all consistent with the file's existing if (PerformanceEntry) convention.
  • Module-level eventLoopUtilization/timerify exports read off the same prototype with local fallback, so identity holds under both a replaced global and a future native own-prop.
  • Tests: fresh-subprocess lazy-install contract, non-enumerability, onresourcetimingbufferfull accessor round-trip, export-surface parity with Node v26.3.0; subprocess stderr assertion is guarded per house style.
  • CI on a7e00ad: perf_hooks green on every lane; the 3 red jobs are known main breaks (bake deinit segfault, --no-orphans (perl) timeout) unrelated to this diff.
Extended reasoning...

Overview

This PR replaces the hand-written performance facade in src/js/node/perf_hooks.ts with the global performance object itself, matching Node's require('node:perf_hooks').performance === globalThis.performance. To make that identity hold, three node-only members (nodeTiming, eventLoopUtilization, timerify) are installed onto Performance.prototype (non-enumerable, hasOwn-guarded, __proto__: null descriptors) at first require. Module-level eventLoopUtilization/timerify re-exports read off that prototype with a local-function fallback. Net: ~50 lines deleted from src/js/node/perf_hooks.ts, ~35 added; ~100 lines of new test coverage in test/js/node/perf_hooks/perf_hooks.test.ts.

Security risks

None identified. No untrusted-input parsing, no auth/crypto/permissions surface. The tamper-resistance concerns raised in earlier rounds (a preload replacing globalThis.performance redirecting the define onto Object.prototype; a polluted Object.prototype suppressing the install via in; a deleted globalThis.Performance throwing at module load) were all addressed: Performance?.prototype as the target, Object.hasOwn guards, if (PerformancePrototype) wrapper, and prototype-read exports with ?? local fallback.

Level of scrutiny

Moderate-to-high. This is Node-compat work in src/js/node/ (which .claude/docs/landing-prs.md flags for extra care) and it mutates a web global's prototype from the node: layer at first-require rather than making the members eager in JSPerformance.cpp. The PR body explicitly justifies that layering (avoids reimplementing #31825's timerify and #32618's in-flight eventLoopUtilization in C++; hasOwn guards make a later native move a no-op here), and the fresh-subprocess test pins the lazy-install contract so a future eager move fails loudly. That reasoning is sound, but whether "lazy install on Performance.prototype from JS" is the right interim shape vs. going straight to C++ is a maintainer call, not a mechanical correctness question.

Other factors

  • All 11 prior inline comments (mine and CodeRabbit's) are resolved with fix commits; no open threads.
  • The bug-hunting system found nothing on the current head.
  • Test coverage is thorough: object identity, prototype placement/enumerability, subprocess-isolated before/after install state, onresourcetimingbufferfull accessor round-trip, module-export identity, and Node v26.3.0 export-surface parity. The old stubs test's assertions are preserved under node-only members are present on performance.
  • CI build 75149 on a7e00ad: 283 pass / 3 fail, all three fails are pre-existing main breaks with no perf_hooks involvement; the modified test file passed on every lane.
  • The recent-commits list shows d434d9f and c67ff0a already on main with this PR's title, so this may already be merged — in which case this review is informational only.

@robobun
robobun force-pushed the farm/b4cc135f/perf-hooks-module-surface branch from d434d9f to 9b34a71 Compare July 18, 2026 08:23

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

No issues found in this pass — all earlier review feedback has been applied and CI is green on every perf_hooks lane. Deferring to a maintainer only because the core approach (installing nodeTiming/eventLoopUtilization/timerify onto the WebCore Performance.prototype from the node: layer at first require) is a layering choice worth a human sign-off.

What was reviewed:

  • Prototype-install block: Performance?.prototype guard, hasOwn gates, __proto__: null descriptors, and the PerformancePrototype?.x ?? local export fallbacks — all consistent now.
  • Confirmed the deleted facade's members (markResourceTiming, clearResourceTimings, onresourcetimingbufferfull, EventTarget methods) are all reachable via the exported global.
  • Subprocess tests: pipes drained concurrently, no bare stderr === "", combined {stdout, exitCode} assertions; onresourcetimingbufferfull test restores the accessor in finally.
Extended reasoning...

Overview

Two files: src/js/node/perf_hooks.ts replaces the hand-written facade object with the global performance itself, and defines the three node-only members (nodeTiming, eventLoopUtilization, timerify) on Performance.prototype (non-enumerable, matching Node v26.3.0). Module-level eventLoopUtilization/timerify exports are read off the same prototype with a local fallback. test/js/node/perf_hooks/perf_hooks.test.ts adds ~100 lines covering object identity, prototype placement/enumerability, resource-timing method presence, the onresourcetimingbufferfull accessor, module-level export identity, and a fresh-subprocess test pinning the lazy-install contract.

Security risks

None identified. The descriptor literals carry __proto__: null, guards use Object.hasOwn, and the define target is the captured Performance.prototype (not getPrototypeOf of a replaceable global) — all hardened per earlier review rounds. No user input reaches a syscall, path, or eval.

Level of scrutiny

Medium. This is a Node-compat API-shape change, not a hot path or memory-safety change. But it does mutate a WebCore global's prototype from src/js/node/ as a first-require side effect — before this PR the web Performance.prototype was untouched. The PR body argues this is strictly closer to Node (where these live on the prototype) and that a native C++ move is the follow-up; that reasoning is sound, but whether Bun wants node-only members on the web global's prototype (visible to code that never imported node:perf_hooks, once anything else in the process has) is the kind of layering call REVIEW.md's "Never change a Bun-native default to fix Node compatibility" rule is adjacent to. It's defensible either way, so a maintainer should ratify it rather than a bot.

Other factors

This PR has been through five review iterations; every inline comment (tamper-resistance, forward-compat export identity, comment length, subprocess-stderr pattern, guarded prototype dereference) was applied and is visible in the current diff. CI build 75149 passed on all lanes that ran perf_hooks; the three red lanes are documented main breaks unrelated to this diff. Tests are well-structured, fail on main, and follow harness conventions. The bug-hunting system found nothing in this pass.

robobun added 2 commits July 18, 2026 11:56
`require("node:perf_hooks").performance` was a hand-written facade that
forwarded a hardcoded subset of the global `performance` object, so it drifted
from what the global actually carries: `markResourceTiming` was never forwarded,
`clearResourceTimings` was a local no-op shadowing the real method,
`onresourcetimingbufferfull` was a load-time value snapshot rather than live, and
the EventTarget methods were missing. It was also a different object than
`globalThis.performance`, unlike node where the two are identical.

Export the global `performance` directly. The only members node's performance
carries that the web `Performance` lacks are `nodeTiming` and
`eventLoopUtilization`; define those on the Performance prototype
(non-enumerable, matching node), so the module object can be the global itself
with no per-method forwarding. `eventLoopUtilization` is also exported at the
module level, sharing the same function identity, as node's lib/perf_hooks.js
does.
The in-process test file already has node:perf_hooks loaded, so it cannot
see the bare web global. Spawn a subprocess that observes the node-only
members absent on Performance.prototype before require("node:perf_hooks")
and present (non-enumerable) afterward, making the documented attach point
a CI-enforced contract.
@robobun
robobun force-pushed the farm/b4cc135f/perf-hooks-module-surface branch from 9b34a71 to 491bf6b Compare July 18, 2026 11:58

@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 — the facade → global-object switch is what dylan-conway asked for, and every review round since has converged.

What was reviewed:

  • Prototype-install block: Performance?.prototype guard, hasOwn gates, __proto__: null descriptors, and the PerformancePrototype?.x ?? local export fallbacks are all consistent now.
  • Confirmed the deleted facade's members (markResourceTiming, real clearResourceTimings, live onresourcetimingbufferfull accessor) are all reachable via the exported global and covered by the new tests.
  • Subprocess tests follow house style (concurrent pipe drain, guarded stderr, combined {stdout, exitCode} assertion); the fresh-process test pins the lazy-install contract.
  • CI build 75149: perf_hooks green on every lane; the 3 red jobs are unrelated main breaks (bake deinit segfault, --no-orphans perl timeout).
Extended reasoning...

Overview

Two files: src/js/node/perf_hooks.ts drops the hand-rolled facade object (which forwarded a hardcoded, drifted subset of performance methods) and instead exports globalThis.performance directly, after installing the three node-only members (nodeTiming, eventLoopUtilization, timerify) onto Performance.prototype non-enumerable — exactly matching Node v26.3.0's placement. test/js/node/perf_hooks/perf_hooks.test.ts gains 7 new tests covering object identity, prototype placement/enumerability, resource-timing method reachability, the onresourcetimingbufferfull accessor, module-level export identity, and a fresh-subprocess lazy-install contract.

Security risks

None. This is a pure Node-compat shape change in a built-in JS module. The prototype-install block was hardened over several review rounds against prototype pollution and replaced-global scenarios: Performance.prototype (not getPrototypeOf(performance)) so a shimmed global can't redirect the define onto Object.prototype; Object.hasOwn guards so a polluted Object.prototype can't suppress the define; __proto__: null on every descriptor; Performance?.prototype + if (PerformancePrototype) so a pre-require delete globalThis.Performance degrades rather than throws. No auth, crypto, filesystem, or network surface.

Level of scrutiny

Medium. It mutates a global prototype from a node: module, which is an observable process-wide side effect — but that is precisely Node's behavior, and the maintainer (dylan-conway) explicitly requested this design over the facade on 2026-07-07 ("yes please do that. that's the proper fix"). The one behavioral nuance vs Node — the members appear on the prototype only after first require('node:perf_hooks') rather than eagerly — is documented in the PR body, pinned by a fresh-subprocess test, and is strictly closer to Node than the pre-PR state (where they weren't on the global at all).

Other factors

This PR has been through 7 iterations. Every inline finding I raised in prior runs (tamper-resistance of the define target, hasOwn vs in, module-level export identity under a future native move, 7-line comment, bare expect(stderr).toBe(""), reading exports off the replaceable global, unguarded Performance.prototype dereference) is resolved in the current diff and marked resolved on the thread. CodeRabbit's fresh-process-coverage request is also addressed. The bug-hunting system found nothing this round. CI on the final commit is green for the touched test file across all lanes; the three failing jobs are pre-existing main breaks with no perf_hooks in scope. #31825's timerify implementation is preserved verbatim; only its attachment point moved from the facade to the prototype.

@alii

alii commented Aug 12, 2026

Copy link
Copy Markdown
Member

landed in #34518, perf_hooks.performance is the global on main

@alii alii closed this Aug 12, 2026
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.

3 participants