-
Notifications
You must be signed in to change notification settings - Fork 30.2k
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: optimize for performance, remove _events use outside of events #17324
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Generally looks good. Couple of nits
doc/api/events.md
Outdated
--> | ||
- `eventName` {any} | ||
* `eventName` {any} The name of the event. | ||
* `unwrap` {boolean} When set to true, will return the original listeners |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not generally a fan of Boolean args in public APIs. Perhaps having a separate unwrappedListeners()
method would work?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it would need to be wrappedListeners
or something similar. Will think on it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you want an option argument, I'd prefer to be explicit and call it unwrapOnceListeners
(which could also be a string or object instead of a boolean).
However, from a performance point of view new methods for separate input types should always be a good idea.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should be addressed now, instead we have a wrappedListeners
method on EventEmitter.
} else if (typeof list !== 'function') { | ||
position = -1; | ||
events[type] = undefined; | ||
--this._eventsCount; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Having the --
at the start here just looks odd. Having it at the end may be more readable
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I'm not sure which way to go here. It matches the rest of the events
. Feels more confusing if we go back and forth throughout this code. Maybe just convert all the usage within this file to be postfix (where the result isn't being used)?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm okay with having consistency in this file.
It probably looks a lot less weird to you if you're used to writing C++ code :)
Why the large |
@mscdex We have to iterate over |
6aeb42a
to
73e0d13
Compare
@mscdex Refactored that method and now it iterates over event names without
|
doc/api/events.md
Outdated
--> | ||
- `eventName` {any} | ||
* `eventName` {any} The name of the event. | ||
* `unwrap` {boolean} When set to true, will return the original listeners |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you want an option argument, I'd prefer to be explicit and call it unwrapOnceListeners
(which could also be a string or object instead of a boolean).
However, from a performance point of view new methods for separate input types should always be a good idea.
} else if (typeof list !== 'function') { | ||
position = -1; | ||
events[type] = undefined; | ||
--this._eventsCount; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm okay with having consistency in this file.
It probably looks a lot less weird to you if you're used to writing C++ code :)
lib/internal/bootstrap_node.js
Outdated
caught = process.domain._errorHandler(er); | ||
|
||
if (!caught) | ||
if (shouldCatch && !caught) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm still not sure what exactly shouldCatch
entails. I'd like to get #17159 done at some point... maybe we can try to do that before this lands?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This part is slightly different than the work within that PR. It's so we don't need ClearFatalExceptionHandlers
which deep reaches into a bunch of JS code (including _events
).
The part of this PR that would be made obsolete by #17159 is within lib/domain.js (& a bit within node.cc). I'm guessing this could land and then we can refactor later? Not sure where that work is at though so I'm happy to wait if it's close.
Edit: The main reason I say 'land this' is just because it's strictly better than what's there currently which requires actually inspecting the _events
object and looking for error handlers, whereas now that responsibility is mostly in JS land and all we check for is a single flag. Far from ideal but a lot better.
73e0d13
to
f0483bd
Compare
At its call sites, `ClearFatalExceptionHandlers()` was used to make the process crash as soon as possible once an exception occurred, without giving JS land a chance to interfere. `ClearFatalExceptionHandlers()` awkwardly removed the current domain and any `uncaughtException` handlers, whereas a clearer way is to execute the relevant reporting (and `exit()`) code directly. Refs: nodejs#17159 Refs: nodejs#17324
At its call sites, `ClearFatalExceptionHandlers()` was used to make the process crash as soon as possible once an exception occurred, without giving JS land a chance to interfere. `ClearFatalExceptionHandlers()` awkwardly removed the current domain and any `uncaughtException` handlers, whereas a clearer way is to execute the relevant reporting (and `exit()`) code directly. PR-URL: #17333 Refs: #17159 Refs: #17324 Reviewed-By: Anatoli Papirovski <apapirovski@mac.com> Reviewed-By: Timothy Gu <timothygu99@gmail.com> Reviewed-By: Daniel Bevenius <daniel.bevenius@gmail.com> Reviewed-By: Andreas Madsen <amwebdk@gmail.com> Reviewed-By: Ben Noordhuis <info@bnoordhuis.nl> Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Colin Ihrig <cjihrig@gmail.com>
@apapirovski Just merged the other 2 PRs :) |
If more than one handler is bound to an event type, avoid copying it on emit and instead set a flag that regulates whether to make a copy when a new event is added or removed. Do not use delete or Object.create(null) when removing listeners, instead set to undefined and modify other methods to account for this.
f0483bd
to
6d230e3
Compare
Refactor lib & src code to eliminate all deep reaches into the internal _events dictionary object, instead use available APIs and add an extra method to EventEmitter: wrappedListeners.
6d230e3
to
9534a22
Compare
Updated. PTAL everyone. CI: https://ci.nodejs.org/job/node-test-pull-request/11804/ |
const argc = conf.argc | 0; | ||
const listeners = Math.max(conf.listeners | 0, 1); | ||
|
||
const ee = new EventEmitter(); | ||
|
||
for (var k = 0; k < listeners; k += 1) | ||
if (listeners === 1) | ||
n *= 5; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think automagically adjusting the n
value is a good idea.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I mean, we don't have a ton of options here. The benchmark doesn't run long enough with 1 listener whereas setting a higher default n
makes the other ones run way too long.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's just a general benchmarking problem, just like any particular n
value might cause things to run too fast or too slow on someone else's machine. At some point you will just need to supply your own set of values for each parameter if you have combinations that vary in speed that much.
doc/api/events.md
Outdated
- `eventName` {any} | ||
|
||
Returns a copy of the array of listeners for the event named `eventName`, | ||
including any wrappers (such as those created by `.once`. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing end parenthesis. Also, maybe link once()
?
--> | ||
- `eventName` {any} | ||
|
||
Returns a copy of the array of listeners for the event named `eventName`, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think saying 'event named' sounds a little confusing. Maybe something like 'event in' instead? Or maybe just 'event'?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can change this but we have another 9 instances of this exact phrasing in this doc.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the current wording is fine here and if we do change it, I'd rather do that throughout the doc and in a separate PR for consistency
@@ -36,6 +36,7 @@ EventEmitter.usingDomains = false; | |||
|
|||
EventEmitter.prototype.domain = undefined; | |||
EventEmitter.prototype._events = undefined; | |||
EventEmitter.prototype._eventsCount = 0; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You might check the performance on this, but I remember at least back with Crankshaft, putting properties like this (that are likely to change in every EventEmitter
instance) on the prototype could slow things down.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's definitely fine now from the testing I did. (I ran benchmarks with & without.)
} | ||
|
||
// Check for listener leak | ||
const m = $getMaxListeners(target); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Did you benchmark this particular change? I would think that checking list.warned
is less expensive than calling out to $getMaxListeners()
every time, when .warned === true
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Accessing .warned
or any other named key on an Array is really expensive from the testing I've done. Also, even if it wasn't, it's better to optimize for the case where warned === false
since that's going to be more common.
(It's been a while but as I can recall, this change alone made roughly 2% difference for cases where warned === false
. This was before I added all the major changes that account for the 20-120% difference.)
@addaleax & @TimothyGu would you consider reviewing this please? You had some feedback for the previous iteration of this in #17074 Thanks! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It might take a bit to review this but I’ll try to get to it :)
doc/api/events.md
Outdated
@@ -574,6 +574,15 @@ to indicate an unlimited number of listeners. | |||
|
|||
Returns a reference to the `EventEmitter`, so that calls can be chained. | |||
|
|||
### emitter.wrappedListeners(eventName) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I kind of feel like something like rawListeners()
might be a better name, but this should be okay.
(i.e. I’m not saying it is a better name, just that wrappedListeners()
does not immediately tell you what this does … neither does rawListeners()
, obviously, but I think it comes closer? 😄 )
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changes to lib, src, test LGTM
--> | ||
- `eventName` {any} | ||
|
||
Returns a copy of the array of listeners for the event named `eventName`, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the current wording is fine here and if we do change it, I'd rather do that throughout the doc and in a separate PR for consistency
lib/events.js
Outdated
if (position === 0) | ||
if (list.length === 2) | ||
events[type] = list[position ? 0 : 1]; | ||
else if (list.emitting) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Tiny nit: Can you add {}
for the if
as well if there is one for the else
part as well?
this.removeListener(type, listeners[i]); | ||
} | ||
} | ||
|
||
return this; | ||
}; | ||
|
||
EventEmitter.prototype.listeners = function listeners(type) { | ||
const events = this._events; | ||
function _listeners(target, type, unwrap) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder if it would make sense to turn this into a closure where unwrap
can be constant-folded, like:
function makeListeners(unwrap) {
return function listeners(type) {
// Code of _listeners with `this` as `target`
}
}
EventEmitter.prototype.listeners = makeListeners(true);
EventEmitter.prototype.rawListeners = makeListeners(false);
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This was just to mimic how we do it elsewhere in this file, namely addListener
and prependListener
but yeah, this could also work. I suppose the suggested version does close over one variable whereas what I'm guessing happens with the current implementation is that V8 inlines _listeners
?
No clue what's preferable. Maybe just stick with consistency for now?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If there is no perf difference, then yes, just stick with this :)
lib/events.js
Outdated
} | ||
|
||
// We must have Symbols to fill in | ||
if (j < count) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Tiny nit: It would make sense to me if the comment was inside the if
since it refers to what must be true if the condition is fulfilled... does that make sense?
} | ||
} else if (typeof list !== 'function') { | ||
position = -1; | ||
events[type] = undefined; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This will cause memory leaks.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep, I originally ran a modified version of our ee-add-remove
benchmark but the 25e4 iterations are not very helpful in testing this. It also regresses the performance once the size of the dictionary object grows (although that seems to not be an issue in later versions of V8, the memory still is).
Mostly I think I was just too optimistic re: user input into the EventEmitter being mostly constant. Probably not a safe assumption.
this._events = Object.create(null); | ||
else | ||
delete events[type]; | ||
events[type] = undefined; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This will also cause memory leaks.
At its call sites, `ClearFatalExceptionHandlers()` was used to make the process crash as soon as possible once an exception occurred, without giving JS land a chance to interfere. `ClearFatalExceptionHandlers()` awkwardly removed the current domain and any `uncaughtException` handlers, whereas a clearer way is to execute the relevant reporting (and `exit()`) code directly. PR-URL: #17333 Refs: #17159 Refs: #17324 Reviewed-By: Anatoli Papirovski <apapirovski@mac.com> Reviewed-By: Timothy Gu <timothygu99@gmail.com> Reviewed-By: Daniel Bevenius <daniel.bevenius@gmail.com> Reviewed-By: Andreas Madsen <amwebdk@gmail.com> Reviewed-By: Ben Noordhuis <info@bnoordhuis.nl> Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Colin Ihrig <cjihrig@gmail.com>
At its call sites, `ClearFatalExceptionHandlers()` was used to make the process crash as soon as possible once an exception occurred, without giving JS land a chance to interfere. `ClearFatalExceptionHandlers()` awkwardly removed the current domain and any `uncaughtException` handlers, whereas a clearer way is to execute the relevant reporting (and `exit()`) code directly. PR-URL: #17333 Refs: #17159 Refs: #17324 Reviewed-By: Anatoli Papirovski <apapirovski@mac.com> Reviewed-By: Timothy Gu <timothygu99@gmail.com> Reviewed-By: Daniel Bevenius <daniel.bevenius@gmail.com> Reviewed-By: Andreas Madsen <amwebdk@gmail.com> Reviewed-By: Ben Noordhuis <info@bnoordhuis.nl> Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Colin Ihrig <cjihrig@gmail.com>
This is a bit broad in scope but all related so hard to separate into independent PRs. There are two commits though to make reviewing easier.
1st commit:
delete
orObject.create(null)
to reset the dictionary object, instead just set toundefined
and modify the rest of the codebase to account for this change.2nd commit:
wrappedListeners
method which allows the user to retrieve the actual functions stored within_events
(such as theonce
wrappers). We were reaching into_events
for this within our own code and it's also one of the most common reasons to reach into_events
by user code._events
from C++CitGM came back clean for these changes. Also, here are the results from the Benchmark CI:
Refs: #17074
Checklist
make -j4 test
(UNIX), orvcbuild test
(Windows) passesAffected core subsystem(s)
domain, events, process