Skip to content

events: optimize once() and removeListener()#64373

Open
mcollina wants to merge 1 commit into
nodejs:mainfrom
mcollina:events-optimize-once-removelistener
Open

events: optimize once() and removeListener()#64373
mcollina wants to merge 1 commit into
nodejs:mainfrom
mcollina:events-optimize-once-removelistener

Conversation

@mcollina

@mcollina mcollina commented Jul 8, 2026

Copy link
Copy Markdown
Member

Optimize the EventEmitter hot paths:

  • rewrite the once() wrapper as a closure instead of bind() + a state object
  • keep the _events object shape in removeListener(): store undefined instead of delete-ing keys or reallocating the object (eventNames() now filters, matching Stream.prototype.eventNames); removes the now-unused kShapeMode
  • outline the unhandled error path from emit() (439 → 278 bytecodes) and the max-listeners warning from _addListener() (405 → 288) so both fit within the V8 inlining budgets

benchmark/compare.js --runs 30 before/after all changes:

events
                                                                 confidence improvement accuracy (*)   (**)  (***)
events/ee-add-remove.js n=1000000 removeListener=0 newListener=0        ***     12.59 %       ±1.59% ±2.11% ±2.76%
events/ee-add-remove.js n=1000000 removeListener=0 newListener=1          *      2.37 %       ±1.92% ±2.58% ±3.40%
events/ee-add-remove.js n=1000000 removeListener=1 newListener=0        ***     15.12 %       ±1.14% ±1.52% ±1.99%
events/ee-add-remove.js n=1000000 removeListener=1 newListener=1        ***     21.82 %       ±2.24% ±3.01% ±3.97%
events/ee-emit.js listeners=1 argc=0 n=2000000                                   0.89 %       ±3.72% ±4.95% ±6.47%
events/ee-emit.js listeners=1 argc=10 n=2000000                                 -0.85 %       ±2.46% ±3.28% ±4.28%
events/ee-emit.js listeners=1 argc=2 n=2000000                                   1.25 %       ±3.73% ±4.97% ±6.50%
events/ee-emit.js listeners=1 argc=4 n=2000000                                  -0.11 %       ±2.91% ±3.87% ±5.04%
events/ee-emit.js listeners=10 argc=0 n=2000000                                  0.99 %       ±1.95% ±2.60% ±3.38%
events/ee-emit.js listeners=10 argc=10 n=2000000                                -0.24 %       ±1.64% ±2.18% ±2.84%
events/ee-emit.js listeners=10 argc=2 n=2000000                                  0.05 %       ±1.61% ±2.14% ±2.79%
events/ee-emit.js listeners=10 argc=4 n=2000000                          **      2.22 %       ±1.60% ±2.13% ±2.77%
events/ee-emit.js listeners=5 argc=0 n=2000000                                   1.82 %       ±2.04% ±2.72% ±3.54%
events/ee-emit.js listeners=5 argc=10 n=2000000                                 -0.26 %       ±1.61% ±2.14% ±2.79%
events/ee-emit.js listeners=5 argc=2 n=2000000                            *     -2.25 %       ±1.96% ±2.61% ±3.39%
events/ee-emit.js listeners=5 argc=4 n=2000000                                   0.64 %       ±2.11% ±2.81% ±3.66%
events/ee-listen-unique.js n=1000000 events=1                           ***     14.85 %       ±0.80% ±1.06% ±1.39%
events/ee-listen-unique.js n=1000000 events=10                          ***      8.46 %       ±1.28% ±1.71% ±2.23%
events/ee-listen-unique.js n=1000000 events=2                           ***     14.09 %       ±0.88% ±1.18% ±1.54%
events/ee-listen-unique.js n=1000000 events=20                          ***      7.53 %       ±1.38% ±1.84% ±2.40%
events/ee-listen-unique.js n=1000000 events=3                           ***      9.24 %       ±1.83% ±2.44% ±3.20%
events/ee-listen-unique.js n=1000000 events=5                           ***     13.22 %       ±1.39% ±1.86% ±2.45%
events/ee-listener-count-on-prototype.js n=50000000                              0.14 %       ±0.82% ±1.09% ±1.41%
events/ee-listeners.js raw='false' listeners=5 n=5000000                ***     -3.03 %       ±1.73% ±2.31% ±3.03%
events/ee-listeners.js raw='false' listeners=50 n=5000000                       -0.00 %       ±0.54% ±0.72% ±0.94%
events/ee-listeners.js raw='true' listeners=5 n=5000000                 ***      5.87 %       ±2.69% ±3.59% ±4.68%
events/ee-listeners.js raw='true' listeners=50 n=5000000                        -0.14 %       ±1.05% ±1.40% ±1.83%
events/ee-once.js argc=0 n=20000000                                     ***     25.71 %       ±1.08% ±1.43% ±1.87%
events/ee-once.js argc=1 n=20000000                                     ***     24.94 %       ±0.77% ±1.02% ±1.33%
events/ee-once.js argc=4 n=20000000                                     ***     25.14 %       ±0.92% ±1.23% ±1.61%
events/ee-once.js argc=5 n=20000000                                     ***     25.63 %       ±0.90% ±1.21% ±1.58%

Be aware that when doing many comparisons the risk of a false-positive result increases.
In this case, there are 31 comparisons, you can thus expect the following amount of false-positive results:
  1.55 false positives, when considering a   5% risk acceptance (*, **, ***),
  0.31 false positives, when considering a   1% risk acceptance (**, ***),
  0.03 false positives, when considering a 0.1% risk acceptance (***)
streams
                                                                                         confidence improvement accuracy (*)    (**)   (***)
streams/creation.js kind='duplex' n=50000000                                                    ***      1.32 %       ±0.61%  ±0.81%  ±1.05%
streams/creation.js kind='readable' n=50000000                                                          -0.37 %       ±0.61%  ±0.81%  ±1.06%
streams/creation.js kind='transform' n=50000000                                                   *      3.83 %       ±3.24%  ±4.37%  ±5.78%
streams/creation.js kind='writable' n=50000000                                                  ***      2.83 %       ±0.55%  ±0.73%  ±0.95%
streams/destroy.js kind='duplex' n=1000000                                                              -0.08 %       ±0.64%  ±0.85%  ±1.11%
streams/destroy.js kind='readable' n=1000000                                                    ***      3.98 %       ±1.82%  ±2.42%  ±3.16%
streams/destroy.js kind='transform' n=1000000                                                            0.32 %       ±0.54%  ±0.72%  ±0.94%
streams/destroy.js kind='writable' n=1000000                                                    ***      2.57 %       ±0.60%  ±0.79%  ±1.03%
streams/iter-creation.js n=100000 type='pair' api='classic'                                              1.49 %       ±4.62%  ±6.15%  ±8.00%
streams/iter-creation.js n=100000 type='pair' api='iter'                                                 0.02 %       ±0.78%  ±1.04%  ±1.35%
streams/iter-creation.js n=100000 type='pair' api='webstream'                                           -0.31 %       ±1.61%  ±2.14%  ±2.78%
streams/iter-creation.js n=100000 type='readable' api='classic'                                         -0.15 %       ±5.01%  ±6.67%  ±8.69%
streams/iter-creation.js n=100000 type='readable' api='iter'                                             0.48 %       ±2.18%  ±2.90%  ±3.77%
streams/iter-creation.js n=100000 type='readable' api='webstream'                                       -0.29 %       ±1.71%  ±2.27%  ±2.96%
streams/iter-creation.js n=100000 type='transform' api='classic'                                         1.97 %       ±2.41%  ±3.21%  ±4.18%
streams/iter-creation.js n=100000 type='transform' api='webstream'                                       0.19 %       ±0.86%  ±1.14%  ±1.49%
streams/iter-creation.js n=100000 type='writable' api='classic'                                          1.07 %       ±2.10%  ±2.80%  ±3.64%
streams/iter-creation.js n=100000 type='writable' api='iter'                                             0.60 %       ±1.89%  ±2.51%  ±3.27%
streams/iter-creation.js n=100000 type='writable' api='webstream'                                       -0.18 %       ±1.23%  ±1.64%  ±2.13%
streams/pipe.js n=5000000                                                                       ***     13.92 %       ±1.41%  ±1.88%  ±2.45%
streams/readable-async-iterator.js sync='no' n=100000                                             *      2.45 %       ±1.86%  ±2.48%  ±3.24%
streams/readable-async-iterator.js sync='yes' n=100000                                                  -3.12 %       ±3.85%  ±5.15%  ±6.76%
streams/readable-bigread.js n=1000                                                                       0.14 %       ±2.98%  ±3.96%  ±5.16%
streams/readable-readall.js n=5000                                                                      -0.24 %       ±2.49%  ±3.32%  ±4.33%
streams/writable-manywrites.js len=1024 callback='no' writev='no' sync='no' n=100000                    -0.87 %       ±2.73%  ±3.64%  ±4.75%
streams/writable-manywrites.js len=1024 callback='no' writev='no' sync='yes' n=100000                    1.20 %       ±4.18%  ±5.57%  ±7.29%
streams/writable-manywrites.js len=1024 callback='no' writev='yes' sync='no' n=100000                    2.47 %       ±5.23%  ±6.98%  ±9.13%
streams/writable-manywrites.js len=1024 callback='no' writev='yes' sync='yes' n=100000                  -1.24 %       ±7.14%  ±9.50% ±12.36%
streams/writable-manywrites.js len=1024 callback='yes' writev='no' sync='no' n=100000             *      2.65 %       ±2.43%  ±3.23%  ±4.22%
streams/writable-manywrites.js len=1024 callback='yes' writev='no' sync='yes' n=100000           **      8.29 %       ±5.26%  ±7.00%  ±9.11%
streams/writable-manywrites.js len=1024 callback='yes' writev='yes' sync='no' n=100000                   2.01 %       ±4.22%  ±5.62%  ±7.31%
streams/writable-manywrites.js len=1024 callback='yes' writev='yes' sync='yes' n=100000           *      5.16 %       ±5.15%  ±6.85%  ±8.92%
streams/writable-manywrites.js len=32768 callback='no' writev='no' sync='no' n=100000                    2.67 %       ±3.56%  ±4.73%  ±6.16%
streams/writable-manywrites.js len=32768 callback='no' writev='no' sync='yes' n=100000            *      5.65 %       ±4.82%  ±6.42%  ±8.37%
streams/writable-manywrites.js len=32768 callback='no' writev='yes' sync='no' n=100000                  -0.76 %       ±1.95%  ±2.59%  ±3.37%
streams/writable-manywrites.js len=32768 callback='no' writev='yes' sync='yes' n=100000                  4.26 %       ±8.13% ±10.81% ±14.07%
streams/writable-manywrites.js len=32768 callback='yes' writev='no' sync='no' n=100000                  -1.57 %       ±3.42%  ±4.56%  ±5.94%
streams/writable-manywrites.js len=32768 callback='yes' writev='no' sync='yes' n=100000                  2.83 %       ±4.44%  ±5.91%  ±7.69%
streams/writable-manywrites.js len=32768 callback='yes' writev='yes' sync='no' n=100000                 -0.87 %       ±2.98%  ±3.96%  ±5.16%
streams/writable-manywrites.js len=32768 callback='yes' writev='yes' sync='yes' n=100000                 2.03 %       ±5.96%  ±7.93% ±10.32%

Be aware that when doing many comparisons the risk of a false-positive result increases.
In this case, there are 40 comparisons, you can thus expect the following amount of false-positive results:
  2.00 false positives, when considering a   5% risk acceptance (*, **, ***),
  0.40 false positives, when considering a   1% risk acceptance (**, ***),
  0.04 false positives, when considering a 0.1% risk acceptance (***)

Use a closure instead of a bound state object for the once() wrapper,
keep the events object shape in removeListener() by storing undefined
instead of deleting keys or reallocating the object, and outline the
cold unhandled 'error' path from emit() and the max-listeners warning
from _addListener() so both fit within V8 inlining budgets.

ee-once +25%, ee-add-remove +12-22%, ee-listen-unique +7-15%,
streams/pipe +13.9%.

Signed-off-by: Matteo Collina <hello@matteocollina.com>
@nodejs-github-bot nodejs-github-bot added events Issues and PRs related to the events subsystem / EventEmitter. needs-ci PRs that need a full CI run. labels Jul 8, 2026
@codecov

codecov Bot commented Jul 8, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 90.24%. Comparing base (1d87a24) to head (ce6c9ad).
⚠️ Report is 13 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main   #64373      +/-   ##
==========================================
- Coverage   90.25%   90.24%   -0.01%     
==========================================
  Files         741      741              
  Lines      240990   241196     +206     
  Branches    45402    45431      +29     
==========================================
+ Hits       217502   217666     +164     
- Misses      15066    15086      +20     
- Partials     8422     8444      +22     
Files with missing lines Coverage Δ
lib/events.js 99.59% <100.00%> (+<0.01%) ⬆️

... and 48 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

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

Labels

events Issues and PRs related to the events subsystem / EventEmitter. needs-ci PRs that need a full CI run.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants