Skip to content

perf: EventEmitter methods on one shared prototype with node's _events store (Refs #10508) - #11809

Merged
proggeramlug merged 2 commits into
mainfrom
perf-eventemitter-shared-prototype
Oct 3, 2026
Merged

proggeramlug merged 2 commits into
mainfrom
perf-eventemitter-shared-prototype

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Oct 3, 2026 •

Copy link
Copy Markdown
Contributor

Refs #10508. EventEmitter subclass instances no longer own their methods: they inherit from one shared EventEmitter.prototype, and listener state uses node's _events store.

Why it was per-instance (#6316). super() stamped 15 closures onto every instance, which shadowed a subclass's emit() override, so #6316 hid overridden base methods under a private key. With a real prototype chain the override wins naturally. super.m() walks the chain to the native method and checks it is the native one.

What changed

  • super(opts) and EventEmitter.call(this, opts) behave like node's EventEmitter.init: own _events, _eventsCount, _maxListeners, plus captureRejections.
  • EventEmitter.prototype gets node's defaults and its methods in node's order, with addListener === on and off === removeListener.
  • Listeners are stored node's way (one function or an array per event); once wrappers carry .listener.
  • captureRejections now works for subclasses. Rejections go to Symbol.for('nodejs.rejection') or to emit('error'); a non-boolean option throws ERR_INVALID_ARG_TYPE.

Instructions (n=5)

Case before this PR
new of an EventEmitter subclass 151.3k 77.1k (−49%)
RSS of the new loop 108 MB 47 MB
emit_ee 51.1k 34.2k (−33%)
commander parse_argv 13.39G 10.43G (−22.1%)
tsc / Zod – flat; full collections equal; output matches node

Tests

  • Two new gap tests (ESM and CJS) match node 26.5.1 byte-for-byte: own keys and names, hasOwnProperty('on'), overrides with super, seven heritage forms, util.inherits, EventEmitter.call(this), Object.create, mixins, the listener API, errors, captureRejections, and a 3000-emitter loop under moving GC.
  • Both new tests fail on the base build, and a sabotage that puts one method back per instance turns both red.
  • runtime 4823/0. The node-suite events (70) and stream (802) tests have the same failures before and after.

Not done

Ralph Küpper added 2 commits October 3, 2026 15:30
A class extending EventEmitter (and util.inherits + EventEmitter.call,
Object.create(EventEmitter.prototype), setPrototypeOf mixins) no longer gets
the 15 emitter closures stamped onto every instance. super() and
EventEmitter.call(this, opts) run node's EventEmitter.init: own _events
(null prototype), _eventsCount and _maxListeners, plus captureRejections.
EventEmitter.prototype carries node's defaults, the methods in lib/events.js
order and the addListener/on and off/removeListener aliases, as IMPLICIT_THIS
closures shared by every emitter.

The listener store is node's _events (one function or an array per type, once
wrappers with .listener, newListener/removeListener through this.emit), shared
with the node:stream objects. A subclass override sits on its own prototype,
and super.m() walks the receiver's chain to the native body, which keeps the
#6316 behavior without the per-instance stash. A captured rejection reaches
[Symbol.for('nodejs.rejection')] or emit('error'); the capture flag is read
only for a listener that returned a promise.

t508: new 151.3k -> 77.1k instructions (RSS 108 -> 47 MB), emit 51.1k ->
34.2k, method call unchanged; commander parse_argv -22.1%; tsc and Zod flat.

Refs #10508
@proggeramlug
proggeramlug merged commit 681d702 into main Oct 3, 2026
21 of 23 checks passed
@proggeramlug
proggeramlug deleted the perf-eventemitter-shared-prototype branch October 3, 2026 13:31
@coderabbitai

coderabbitai Bot commented Oct 3, 2026

Copy link
Copy Markdown

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

Note

Currently processing new changes in this PR. This may take a few minutes, please wait...

⚙️ Run configuration
  • Configuration used: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: c7aea314-3645-44be-bf6a-cbf89142b535
📥 Commits

Reviewing files that changed from the base of the PR and between 34acf64 and 9ab2ac6.

⛔ Files ignored due to path filters (1)
  • crates/perry-codegen/src/wasm32/runtime_abi.tsv is excluded by !**/*.tsv
📒 Files selected for processing (17)
  • changelog.d/PENDING-eventemitter-shared-prototype.md
  • crates/perry-codegen/src/expr/this_super_call.rs
  • crates/perry-codegen/src/expr/write_barrier.rs
  • crates/perry-codegen/src/lower_call/new_helpers.rs
  • crates/perry-codegen/src/runtime_decls/stdlib_ffi/streams_events.rs
  • crates/perry-runtime/src/closure/dispatch/value_call.rs
  • crates/perry-runtime/src/node_stream.rs
  • crates/perry-runtime/src/node_stream_constructors/builders.rs
  • crates/perry-runtime/src/node_stream_dispatch.rs
  • crates/perry-runtime/src/node_stream_event_emitter.rs
  • crates/perry-runtime/src/node_stream_keys.rs
  • crates/perry-runtime/src/node_stream_readwrite.rs
  • crates/perry-runtime/src/object/class_registry/prototype_objects.rs
  • crates/perry-runtime/src/object/field_get_set/enumeration.rs
  • crates/perry-runtime/src/object/native_module.rs
  • test-files/test_gap_eventemitter_shared_prototype.ts
  • test-files/test_gap_eventemitter_shared_prototype_cjs.cts
 _____________________
< I've git the power! >
 ---------------------
  \
   \   \
        \ /\
        ( )
      .( o ).
✨ Finishing Touches
📝 Generate docstrings
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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.

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.

1 participant