Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

events: add fast and slow path #52733

Draft
wants to merge 67 commits into
base: main
Choose a base branch
from

Conversation

rluvaton
Copy link
Member

@rluvaton rluvaton commented Apr 28, 2024

Thank you @benjamingr for the idea

WIP!!!, fixed most of the tests

TODO

Benchmarks

events

events benchmark URL

                                                                 confidence improvement accuracy (*)   (**)   (***)
events/ee-add-remove.js n=1000000 removeListener=0 newListener=0                -0.25 %       ±0.71% ±0.95%  ±1.24%
events/ee-add-remove.js n=1000000 removeListener=0 newListener=1        ***     10.22 %       ±0.63% ±0.84%  ±1.10%
events/ee-add-remove.js n=1000000 removeListener=1 newListener=0                 0.23 %       ±0.71% ±0.95%  ±1.24%
events/ee-add-remove.js n=1000000 removeListener=1 newListener=1        ***      7.33 %       ±0.85% ±1.13%  ±1.48%
events/ee-emit.js listeners=1 argc=0 n=2000000                          ***    111.60 %       ±6.32% ±8.47% ±11.14%
events/ee-emit.js listeners=1 argc=10 n=2000000                         ***    -21.98 %       ±1.40% ±1.86%  ±2.42%
events/ee-emit.js listeners=1 argc=2 n=2000000                          ***    110.07 %       ±4.38% ±5.83%  ±7.59%
events/ee-emit.js listeners=1 argc=4 n=2000000                          ***     99.87 %       ±5.63% ±7.49%  ±9.75%
events/ee-emit.js listeners=10 argc=0 n=2000000                         ***      7.45 %       ±0.80% ±1.08%  ±1.42%
events/ee-emit.js listeners=10 argc=10 n=2000000                        ***      6.16 %       ±0.44% ±0.59%  ±0.77%
events/ee-emit.js listeners=10 argc=2 n=2000000                         ***      7.60 %       ±0.79% ±1.05%  ±1.36%
events/ee-emit.js listeners=10 argc=4 n=2000000                         ***      7.78 %       ±1.17% ±1.57%  ±2.07%
events/ee-emit.js listeners=5 argc=0 n=2000000                          ***     17.87 %       ±1.36% ±1.81%  ±2.36%
events/ee-emit.js listeners=5 argc=10 n=2000000                         ***     13.65 %       ±1.13% ±1.50%  ±1.96%
events/ee-emit.js listeners=5 argc=2 n=2000000                          ***      7.37 %       ±1.26% ±1.69%  ±2.22%
events/ee-emit.js listeners=5 argc=4 n=2000000                          ***     18.39 %       ±1.30% ±1.74%  ±2.27%
events/ee-listen-unique.js n=1000000 events=1                                    0.13 %       ±0.47% ±0.63%  ±0.82%
events/ee-listen-unique.js n=1000000 events=10                            *      1.20 %       ±1.12% ±1.49%  ±1.94%
events/ee-listen-unique.js n=1000000 events=2                             *     -0.75 %       ±0.71% ±0.94%  ±1.23%
events/ee-listen-unique.js n=1000000 events=20                                   0.35 %       ±1.39% ±1.85%  ±2.41%
events/ee-listen-unique.js n=1000000 events=3                            **     -1.68 %       ±1.11% ±1.48%  ±1.94%
events/ee-listen-unique.js n=1000000 events=5                                    0.10 %       ±0.96% ±1.28%  ±1.67%
events/ee-listener-count-on-prototype.js n=50000000                     ***    -16.68 %       ±1.27% ±1.70%  ±2.24%
events/ee-listeners.js raw='false' listeners=5 n=5000000                ***     -6.19 %       ±0.83% ±1.11%  ±1.44%
events/ee-listeners.js raw='false' listeners=50 n=5000000               ***     -2.50 %       ±0.48% ±0.64%  ±0.83%
events/ee-listeners.js raw='true' listeners=5 n=5000000                 ***     -9.56 %       ±0.95% ±1.27%  ±1.65%
events/ee-listeners.js raw='true' listeners=50 n=5000000                ***     -1.63 %       ±0.64% ±0.86%  ±1.12%
events/ee-once.js argc=0 n=20000000                                     ***     17.17 %       ±1.12% ±1.49%  ±1.96%
events/ee-once.js argc=1 n=20000000                                     ***     14.88 %       ±1.57% ±2.11%  ±2.78%
events/ee-once.js argc=4 n=20000000                                     ***     11.77 %       ±0.71% ±0.95%  ±1.24%
events/ee-once.js argc=5 n=20000000                                     ***     12.95 %       ±1.20% ±1.60%  ±2.08%

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 (***)
outdated benchmark result

events benchmark URL

                                                                 confidence improvement accuracy (*)   (**)   (***)
events/ee-add-remove.js n=1000000 removeListener=0 newListener=0        ***      1.08 %       ±0.57% ±0.76%  ±0.99%
events/ee-add-remove.js n=1000000 removeListener=0 newListener=1        ***     11.93 %       ±0.69% ±0.92%  ±1.20%
events/ee-add-remove.js n=1000000 removeListener=1 newListener=0         **      1.10 %       ±0.75% ±0.99%  ±1.29%
events/ee-add-remove.js n=1000000 removeListener=1 newListener=1        ***      7.78 %       ±0.71% ±0.95%  ±1.24%
events/ee-emit.js listeners=1 argc=0 n=2000000                          ***    110.02 %       ±2.90% ±3.86%  ±5.03%
events/ee-emit.js listeners=1 argc=10 n=2000000                         ***    -21.73 %       ±3.21% ±4.28%  ±5.57%
events/ee-emit.js listeners=1 argc=2 n=2000000                          ***    102.72 %       ±5.43% ±7.24%  ±9.48%
events/ee-emit.js listeners=1 argc=4 n=2000000                          ***     98.91 %       ±6.48% ±8.66% ±11.35%
events/ee-emit.js listeners=10 argc=0 n=2000000                         ***      8.77 %       ±0.73% ±0.97%  ±1.26%
events/ee-emit.js listeners=10 argc=10 n=2000000                        ***      6.19 %       ±0.39% ±0.52%  ±0.68%
events/ee-emit.js listeners=10 argc=2 n=2000000                         ***      6.92 %       ±0.88% ±1.17%  ±1.54%
events/ee-emit.js listeners=10 argc=4 n=2000000                         ***      6.71 %       ±0.76% ±1.01%  ±1.32%
events/ee-emit.js listeners=5 argc=0 n=2000000                          ***     17.82 %       ±0.90% ±1.20%  ±1.56%
events/ee-emit.js listeners=5 argc=10 n=2000000                         ***     14.79 %       ±0.75% ±0.99%  ±1.29%
events/ee-emit.js listeners=5 argc=2 n=2000000                          ***      7.11 %       ±0.94% ±1.25%  ±1.63%
events/ee-emit.js listeners=5 argc=4 n=2000000                          ***     17.21 %       ±1.11% ±1.48%  ±1.94%
events/ee-listen-unique.js n=1000000 events=1                                    0.27 %       ±0.53% ±0.70%  ±0.91%
events/ee-listen-unique.js n=1000000 events=10                           **      1.72 %       ±1.06% ±1.41%  ±1.83%
events/ee-listen-unique.js n=1000000 events=2                                    0.05 %       ±0.82% ±1.09%  ±1.41%
events/ee-listen-unique.js n=1000000 events=20                                   0.96 %       ±1.36% ±1.81%  ±2.36%
events/ee-listen-unique.js n=1000000 events=3                            **      1.51 %       ±1.10% ±1.46%  ±1.90%
events/ee-listen-unique.js n=1000000 events=5                            **      1.65 %       ±0.98% ±1.31%  ±1.70%
events/ee-listener-count-on-prototype.js n=50000000                     ***    -16.19 %       ±1.44% ±1.93%  ±2.54%
events/ee-listeners.js raw='false' listeners=5 n=5000000                ***     -5.07 %       ±0.57% ±0.76%  ±0.99%
events/ee-listeners.js raw='false' listeners=50 n=5000000               ***     -1.68 %       ±0.46% ±0.61%  ±0.79%
events/ee-listeners.js raw='true' listeners=5 n=5000000                 ***    -10.91 %       ±1.03% ±1.37%  ±1.78%
events/ee-listeners.js raw='true' listeners=50 n=5000000                 **      0.93 %       ±0.61% ±0.81%  ±1.06%
events/ee-once.js argc=0 n=20000000                                     ***     15.84 %       ±1.81% ±2.41%  ±3.14%
events/ee-once.js argc=1 n=20000000                                     ***     13.77 %       ±1.16% ±1.56%  ±2.07%
events/ee-once.js argc=4 n=20000000                                     ***     11.74 %       ±0.63% ±0.84%  ±1.09%
events/ee-once.js argc=5 n=20000000                                     ***     13.90 %       ±1.14% ±1.53%  ±2.01%

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

streams benchmark URL

outdated benchmark result

streams benchmark URL

                                                                                         confidence improvement accuracy (*)   (**)  (***)
streams/creation.js kind='duplex' n=50000000                                                    ***    -12.91 %       ±0.56% ±0.75% ±0.99%
streams/creation.js kind='readable' n=50000000                                                  ***    -34.50 %       ±0.61% ±0.81% ±1.06%
streams/creation.js kind='transform' n=50000000                                                 ***    -11.56 %       ±2.02% ±2.69% ±3.50%
streams/creation.js kind='writable' n=50000000                                                  ***    -19.04 %       ±0.77% ±1.02% ±1.33%
streams/destroy.js kind='duplex' n=1000000                                                      ***    -12.93 %       ±0.39% ±0.52% ±0.68%
streams/destroy.js kind='readable' n=1000000                                                    ***    -26.69 %       ±2.16% ±2.90% ±3.84%
streams/destroy.js kind='transform' n=1000000                                                   ***    -16.18 %       ±0.49% ±0.65% ±0.84%
streams/destroy.js kind='writable' n=1000000                                                    ***    -18.73 %       ±0.90% ±1.20% ±1.57%
streams/pipe-object-mode.js n=5000000                                                           ***      5.55 %       ±0.37% ±0.50% ±0.65%
streams/pipe.js n=5000000                                                                       ***      2.28 %       ±0.35% ±0.46% ±0.60%
streams/readable-async-iterator.js sync='no' n=100000                                                   -1.17 %       ±1.29% ±1.71% ±2.24%
streams/readable-async-iterator.js sync='yes' n=100000                                          ***     -3.41 %       ±1.63% ±2.16% ±2.82%
streams/readable-bigread.js n=1000                                                              ***     -1.68 %       ±0.67% ±0.90% ±1.17%
streams/readable-bigunevenread.js n=1000                                                         **     -0.89 %       ±0.62% ±0.83% ±1.08%
streams/readable-boundaryread.js type='buffer' n=2000                                           ***     -3.05 %       ±0.65% ±0.87% ±1.13%
streams/readable-boundaryread.js type='string' n=2000                                                    0.27 %       ±1.33% ±1.77% ±2.30%
streams/readable-from.js type='array' n=10000000                                                 **     -6.09 %       ±3.66% ±4.92% ±6.51%
streams/readable-from.js type='async-generator' n=10000000                                              -0.74 %       ±1.44% ±1.93% ±2.53%
streams/readable-from.js type='sync-generator-with-async-values' n=10000000                     ***     -3.27 %       ±1.59% ±2.14% ±2.83%
streams/readable-from.js type='sync-generator-with-sync-values' n=10000000                      ***     -3.00 %       ±0.22% ±0.30% ±0.38%
streams/readable-readall.js n=5000                                                                       0.51 %       ±1.99% ±2.65% ±3.45%
streams/readable-uint8array.js kind='encoding' n=1000000                                          *      0.93 %       ±0.91% ±1.21% ±1.58%
streams/readable-uint8array.js kind='read' n=1000000                                                     0.54 %       ±1.30% ±1.73% ±2.26%
streams/readable-unevenread.js n=1000                                                                   -0.16 %       ±0.42% ±0.56% ±0.72%
streams/writable-manywrites.js len=1024 callback='no' writev='no' sync='no' n=100000              *     -1.90 %       ±1.90% ±2.52% ±3.28%
streams/writable-manywrites.js len=1024 callback='no' writev='no' sync='yes' n=100000           ***     -1.36 %       ±0.59% ±0.78% ±1.02%
streams/writable-manywrites.js len=1024 callback='no' writev='yes' sync='no' n=100000           ***     -2.15 %       ±1.13% ±1.51% ±1.97%
streams/writable-manywrites.js len=1024 callback='no' writev='yes' sync='yes' n=100000           **     -0.97 %       ±0.63% ±0.84% ±1.10%
streams/writable-manywrites.js len=1024 callback='yes' writev='no' sync='no' n=100000                   -1.14 %       ±2.40% ±3.20% ±4.17%
streams/writable-manywrites.js len=1024 callback='yes' writev='no' sync='yes' n=100000                  -0.19 %       ±1.46% ±1.94% ±2.53%
streams/writable-manywrites.js len=1024 callback='yes' writev='yes' sync='no' n=100000          ***     -3.61 %       ±1.27% ±1.70% ±2.23%
streams/writable-manywrites.js len=1024 callback='yes' writev='yes' sync='yes' n=100000                  0.18 %       ±1.76% ±2.35% ±3.08%
streams/writable-manywrites.js len=32768 callback='no' writev='no' sync='no' n=100000                    1.39 %       ±2.35% ±3.13% ±4.09%
streams/writable-manywrites.js len=32768 callback='no' writev='no' sync='yes' n=100000                  -0.08 %       ±1.99% ±2.67% ±3.52%
streams/writable-manywrites.js len=32768 callback='no' writev='yes' sync='no' n=100000            *     -3.00 %       ±2.33% ±3.10% ±4.03%
streams/writable-manywrites.js len=32768 callback='no' writev='yes' sync='yes' n=100000                  0.25 %       ±1.96% ±2.63% ±3.47%
streams/writable-manywrites.js len=32768 callback='yes' writev='no' sync='no' n=100000            *     -3.12 %       ±2.43% ±3.24% ±4.23%
streams/writable-manywrites.js len=32768 callback='yes' writev='no' sync='yes' n=100000         ***     -0.87 %       ±0.42% ±0.56% ±0.73%
streams/writable-manywrites.js len=32768 callback='yes' writev='yes' sync='no' n=100000         ***     -3.44 %       ±1.65% ±2.20% ±2.87%
streams/writable-manywrites.js len=32768 callback='yes' writev='yes' sync='yes' n=100000        ***     -1.91 %       ±1.01% ±1.34% ±1.75%
streams/writable-uint8array.js kind='object-mode' n=50000000                                     **      1.12 %       ±0.83% ±1.11% ±1.48%
streams/writable-uint8array.js kind='write' n=50000000                                           **     -0.73 %       ±0.53% ±0.71% ±0.94%
streams/writable-uint8array.js kind='writev' n=50000000                                                 -0.26 %       ±0.29% ±0.38% ±0.50%

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

@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 Apr 28, 2024
@rluvaton rluvaton added the performance Issues and PRs related to the performance of Node.js. label Apr 28, 2024
@nodejs-github-bot
Copy link
Collaborator

@nodejs-github-bot
Copy link
Collaborator

@nodejs-github-bot
Copy link
Collaborator

Copy link
Member

@benjamingr benjamingr left a comment

Choose a reason for hiding this comment

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

Really nice, happy the idea works and awesome benchmrks. Still lots of work to do here and stuff to optimize.

I imgined something even faster (e.g. optimize for single event + single listener (+error listener) and not just single listener for the event).

In many cases (like events.once and .on) we can do even faster. Lots of low hanging fruit once we have two implementations.

I also envisioned something more akin to shapes (rather than just two representations), though seeing the benchmarks even just this iss already a huge win given how omnipresent events and emitters are in the ecoystem.

Would also be cool to apply this later to EventTarget and a few others

lib/events.js Outdated
// 3. kErrorMonitor - undefined by default
// TODO - add comment for what this is optimized for
/**
* This class is optimized for the case where there is only a single listener for each event.
Copy link
Member

Choose a reason for hiding this comment

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

I would also optimize for a few specific events.

@rluvaton
Copy link
Member Author

If I use 1 class I will need to add an if in the hot path (emit)

@rluvaton
Copy link
Member Author

I need help with reducing the creation time for streams

Cc @nodejs/performance

lib/events.js Outdated Show resolved Hide resolved
lib/events.js Outdated
// TODO(rluvaton): if have newListener and removeListener events, switch to slow path

const shouldBeFastPath = arg === undefined ||
ObjectEntries(arg).some(({ 0: key, 1: value }) =>
Copy link
Member

Choose a reason for hiding this comment

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

Can you avoid Object.entries in here?

Copy link
Member Author

Choose a reason for hiding this comment

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

Instead changed to a slow path by default to avoid potential bugs

lib/events.js Outdated Show resolved Hide resolved
@mcollina
Copy link
Member

As a side note, I think we might want to drop captureRejections. It was a nice experiment without major usage.

It's code that sits into a hot path (even if not triggered), so maybe it would help speeding this up a bit.

@@ -47,7 +47,6 @@ assert.throws(function() {
}, /blerg/);

process.on('exit', function() {
assert(!(myee._events instanceof Object));
Copy link
Member Author

@rluvaton rluvaton May 1, 2024

Choose a reason for hiding this comment

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

Removed this as in fast path we don't have __proto__: null on the events and therefor it is an object

@rluvaton

This comment was marked as outdated.

// Users can call emit in the constructor before even calling super causing this[kImpl] to be undefined
const impl = this[kImpl];

// The order here is important as impl === undefined is slower than type === 'error'
Copy link
Member Author

Choose a reason for hiding this comment

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

I have no idea why checking type === 'error' first is faster when running ee-emit.js benchmark and slower is impl === undefined first

@nodejs-github-bot
Copy link
Collaborator

@nodejs-github-bot
Copy link
Collaborator

@nodejs-github-bot
Copy link
Collaborator

@nodejs-github-bot
Copy link
Collaborator

Comment on lines -12 to -13
assert.strictEqual(ee._events.hasOwnProperty, undefined);
assert.strictEqual(ee._events.toString, undefined);
Copy link
Member Author

@rluvaton rluvaton May 2, 2024

Choose a reason for hiding this comment

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

Removed this as in fast path we don't have __proto__: null on the events and therefor it has those properties

@@ -26,7 +26,6 @@ const events = require('events');

const e = new events.EventEmitter();

assert(!(e._events instanceof Object));
Copy link
Member Author

Choose a reason for hiding this comment

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

Removed this as in fast path we don't have __proto__: null on the events and therefor it is an object

@@ -32,7 +32,6 @@ let fl; // foo listeners
fl = e.listeners('foo');
assert(Array.isArray(fl));
assert.strictEqual(fl.length, 0);
assert(!(e._events instanceof Object));
Copy link
Member Author

Choose a reason for hiding this comment

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

Removed this as in fast path we don't have __proto__: null on the events and therefor it is an object

@nodejs-github-bot
Copy link
Collaborator

@rluvaton rluvaton added the needs-citgm PRs that need a CITGM CI run. label May 2, 2024
@nodejs-github-bot
Copy link
Collaborator

@nodejs-github-bot
Copy link
Collaborator

@RedYetiDev RedYetiDev added the needs-benchmark-ci PR that need a benchmark CI run. label May 6, 2024
@rluvaton rluvaton force-pushed the add-fast-and-slow-path-for-event-emitter branch from 5f26e0d to acf96af Compare May 12, 2024 20:16
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-benchmark-ci PR that need a benchmark CI run. needs-ci PRs that need a full CI run. needs-citgm PRs that need a CITGM CI run. performance Issues and PRs related to the performance of Node.js.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

6 participants