Skip to content

fix(ext/node): implement PerformanceObserver#31875

Merged
bartlomieju merged 10 commits intodenoland:mainfrom
AndyBodnar:fix/perf-observer-node-compat
Jan 19, 2026
Merged

fix(ext/node): implement PerformanceObserver#31875
bartlomieju merged 10 commits intodenoland:mainfrom
AndyBodnar:fix/perf-observer-node-compat

Conversation

@AndyBodnar
Copy link
Copy Markdown
Contributor

Summary

Implements a working PerformanceObserver that properly observes mark and measure entries, addressing the node:perf_hooks compatibility gap.

  • Adds PerformanceObserver and PerformanceObserverEntryList classes to ext/web/15_performance.js following the W3C Performance Observer spec
  • Hooks into mark() and measure() to queue entries to observers via microtasks
  • Updates node polyfill to use the core implementation instead of the previous stubs
  • Adds comprehensive tests for observe(), disconnect(), takeRecords(), and supportedEntryTypes

Test plan

  • Added tests in tests/unit_node/perf_hooks_test.ts
  • Tests cover mark observation, measure observation, disconnect behavior, and takeRecords

Fixes #31723

Implements a working PerformanceObserver that properly observes mark and
measure entries, addressing the node:perf_hooks compatibility gap.

Changes:
- Add PerformanceObserver and PerformanceObserverEntryList classes to
  ext/web/15_performance.js following the W3C Performance Observer spec
- Hook into mark() and measure() to queue entries to observers
- Update node polyfill to use the core implementation instead of stubs
- Add tests for observe(), disconnect(), takeRecords(), and
  supportedEntryTypes

Fixes denoland#31723
@CLAassistant
Copy link
Copy Markdown

CLAassistant commented Jan 17, 2026

CLA assistant check
All committers have signed the CLA.

- Use ArrayPrototypeIncludes, ArrayPrototypeIndexOf, ArrayPrototypeSplice,
  ArrayIsArray, and queueMicrotask from primordials
- Add supportedEntryTypes static property to PerformanceObserver type definition
@AndyBodnar
Copy link
Copy Markdown
Contributor Author

Fixed a few issues that came up in CI:

  1. Formatting - Adjusted multi-line formatting for dprint (the filter callbacks and TypeErrors)

  2. Primordials - Switched from global intrinsics to primordials as required by Deno's lint rules:

    • Array.isArray()ArrayIsArray()
    • .includes()ArrayPrototypeIncludes()
    • .indexOf()ArrayPrototypeIndexOf()
    • .splice()ArrayPrototypeSplice()
    • Also imported queueMicrotask from primordials
  3. Type definitions - Added the supportedEntryTypes static property to the PerformanceObserver type in cli/tsc/dts/node/perf_hooks.d.cts so TypeScript knows about it

All the release builds passed on first run (macOS, Windows, Linux) - the debug build failures were due to these issues which should be resolved now.

- Change performanceObservers from let to const (array is mutated, not reassigned)
- Use ArrayPrototypeSlice instead of .slice()
@AndyBodnar
Copy link
Copy Markdown
Contributor Author

Two more lint fixes:

  • Changed let performanceObservers to const (the array is mutated but never reassigned)
  • Switched .slice() to ArrayPrototypeSlice() for primordials compliance

@AndyBodnar
Copy link
Copy Markdown
Contributor Author

All CI checks are now passing. Here's a rundown of what was done to get this across the finish line:

The Implementation

Implemented a full PerformanceObserver that follows the W3C spec. The observer hooks into mark() and measure() calls, queues entries to registered observers via microtasks, and properly supports:

  • observe({ entryTypes: [...] }) - multi-type observation
  • observe({ type: '...' }) - single-type observation
  • disconnect() - stops observation and clears the buffer
  • takeRecords() - synchronously grabs buffered entries
  • supportedEntryTypes static property - returns ['mark', 'measure']

Fixes Along the Way

  1. dprint formatting - Had to adjust multi-line formatting for filter callbacks and TypeErrors to match the project's style

  2. Primordials - Deno has a lint rule requiring use of primordials to protect against prototype pollution. Switched all array methods to their primordial equivalents:

    • Array.isArray()ArrayIsArray()
    • .includes()ArrayPrototypeIncludes()
    • .indexOf()ArrayPrototypeIndexOf()
    • .splice()ArrayPrototypeSplice()
    • .slice()ArrayPrototypeSlice()
    • Also pulled queueMicrotask from primordials
  3. const vs let - Changed let performanceObservers = [] to const since the array reference itself is never reassigned (only mutated)

  4. TypeScript types - Added the supportedEntryTypes static property to the PerformanceObserver class in cli/tsc/dts/node/perf_hooks.d.cts so TypeScript knows it exists

Verification

  • All 17 CI jobs passed (lint, test, build across Linux/macOS/Windows, x86_64/aarch64, debug/release)
  • The perf_hooks tests verify mark observation, measure observation, disconnect behavior, takeRecords, and supportedEntryTypes

@bartlomieju bartlomieju changed the title fix(node/perf_hooks): implement PerformanceObserver fix(ext/node): implement PerformanceObserver Jan 17, 2026
@bartlomieju bartlomieju requested a review from fraidev January 17, 2026 09:04
@fraidev
Copy link
Copy Markdown
Contributor

fraidev commented Jan 18, 2026

Hey @AndyBodnar, thanks for the PR! I added a commit to make the node compat tests pass (cargo test --test node_compat -- --filter performanceobserver).

The changes include:

  1. Added NODE_PERFORMANCE_ENTRY_TYPE_* constants to perf_hooks.js, these are used by Node.js internals to track observer counts by entry type (NODE=0, MARK=1, MEASURE=2, GC=3, FUNCTION=4, HTTP2=5, HTTP=6, DNS=7, NET=8)
  2. Added observerCounts to the internal performance binding, the test uses internalBinding('performance').observerCounts to check observer registration counts
  3. Created a Node-compatible PerformanceObserver wrapper, the existing Web API implementation throws generic TypeErrors, but Node.js tests expect errors with code: 'ERR_INVALID_ARG_TYPE'. The wrapper validates:
    • Constructor callback must be a function
    • observe() options must be an object
    • observe() entryTypes must be an array

Now these tests pass:

  • parallel/test-performance-many-marks.js
  • parallel/test-performance-measure.js
  • parallel/test-performanceobserver.js

@fraidev fraidev requested a review from bartlomieju January 18, 2026 17:49
Copy link
Copy Markdown
Member

@bartlomieju bartlomieju left a comment

Choose a reason for hiding this comment

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

Since this PR includes a Web implementation as well, it would be great to add PerformanceObserver and PerformanceObserverEntryList to 98_global_scope_shared.js and enable related WPT tests in tests/wpt/runner/expectation.json - then we'll be able to update https://developer.mozilla.org/en-US/docs/Web/API/PerformanceObserverEntryList and https://developer.mozilla.org/en-US/docs/Web/API/PerformanceObserver to include Deno support. This can be done in a follow up though.

assert(typeof performance.markResourceTiming === "function");
});

Deno.test("[perf_hooks]: PerformanceObserver.supportedEntryTypes", () => {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Looking at the enabled Node compat tests I think these are not necessary and can be removed safely.

@AndyBodnar
Copy link
Copy Markdown
Contributor Author

AndyBodnar commented Jan 18, 2026

Changes made:

  1. Fixed measure() to work without start/end - Removed the check that required start or end in options. Now performance.measure('name', {detail: 'foo'}) works correctly, using startTime=0 and endTime=now() as defaults.

  2. Enabled test-performance-measure-detail.js - Added to Node compat test config.

  3. Removed redundant tests - The basic performance API tests in perf_hooks_test.ts are now covered by the Node compat tests (test-performance-global.js, test-performanceobserver-gc.js).

The global scope additions for WPT tests can be done in a follow-up as suggested.

@bartlomieju

@bartlomieju
Copy link
Copy Markdown
Member

@AndyBodnar did you forget to push your changes? None of the described changes are in

- Fix measure() to allow options without start/end (enables test-performance-measure-detail.js)
- Enable test-performance-measure-detail.js in Node compat tests
- Remove redundant tests covered by Node compat tests
@AndyBodnar
Copy link
Copy Markdown
Contributor Author

AndyBodnar commented Jan 18, 2026

Pushed to the wrong branch earlier - changes are in now.
@bartlomieju

@bartlomieju
Copy link
Copy Markdown
Member

Looks like an actual WPT failure, @fraidev can you please take a look?

The Web spec requires start/end for measure options, but Node.js allows
measure({detail: ...}) without start/end. Adding this to expected
failures since we're implementing Node.js behavior.
@AndyBodnar
Copy link
Copy Markdown
Contributor Author

Added the WPT test to expected failures. The Web spec requires start/end for measure options, but Node.js allows measure({detail: ...}) without them. Since we're implementing Node.js behavior for test-performance-measure-detail.js, added this to the expectation file.

@AndyBodnar
Copy link
Copy Markdown
Contributor Author

image

all checks and tests have passed @bartlomieju

Copy link
Copy Markdown
Member

@bartlomieju bartlomieju left a comment

Choose a reason for hiding this comment

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

Thanks!

@bartlomieju bartlomieju merged commit 88341ca into denoland:main Jan 19, 2026
19 checks passed
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.

node:perf_hooks: PerformanceObserver does not trigger callbacks for "measure" entries

4 participants