-
Notifications
You must be signed in to change notification settings - Fork 30k
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
[WIP] events: use Map for events and limit copying #17074
Conversation
I think we should put the Map under a private symbol if we do that. |
While I'm overall quite favorable to this approach, this is code that we need to be very careful with. There is a significantly non-trivial amount of userland code that can be broken by this. We'll need to do some investigation on the impact. Marking semver-major accordingly. |
lib/_stream_readable.js
Outdated
@@ -48,12 +48,15 @@ function prependListener(emitter, event, fn) { | |||
// userland ones. NEVER DO THIS. This is here only because this code needs | |||
// to continue to work with older versions of Node.js that do not include | |||
// the prependListener() method. The goal is to eventually remove this hack. | |||
if (!emitter._events || !emitter._events[event]) | |||
if (!emitter._events || !emitter._events.has(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.
Ping @mcollina @nodejs/streams ... please review this
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.
FWIW, I don't think this is correct but I need some input on what the current relationship between the npm package & node is. That original block of code seems to only be there defensively for the npm version, I don't actually know if it's relevant in the current version of Node.
lib/domain.js
Outdated
@@ -76,7 +76,6 @@ function Domain() { | |||
|
|||
Domain.prototype.members = 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.
nit: unrelated change?
I'm not 100% certain but that might present other problems in relation to accessing it from C++, which we need to do in a few cases.
We might be able to make it a proxy anyway. I'm going to look into this. |
Also, for reference: most of the |
Exposing |
5449f3d
to
a6fd6f1
Compare
Let's start with a CITGM check: https://ci.nodejs.org/view/Node.js-citgm/job/citgm-smoker/1077/ |
I'm -1 in landing this without a strategy for Since last time this issue was brought up, I have thought a lot on how to proceed to solve this problem.
@nodejs/tsc @nodejs/streams what do you think? |
I don’t see what the fuzz is. If we provide a backwards-compatible proxy for |
I'm 👍 with |
lib/events.js
Outdated
} | ||
|
||
if (type === 'error') { | ||
events._hasErrorListener = 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.
I don't think we should be adding new, underscored properties to EventEmitter
instances. Let's use symbols if we absolutely need new private properties...
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 problem is that we need to get this from C++ so the alternative is a symbol that's declared in C++ and shared with JS? I'm not too familiar with those parts so open to feedback/corrections.
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.
@apapirovski Sorry, as you know I was on mobile, I probably should have given more context.
What I had in mind was having this tracking be specific to Domain
s, not all EventEmitter
s; you could add .on('newListener');
and .on('removeListener');
checks to keep track of whether an error listener is present, then update a property directly on the Domain
object.
And, in case I wasn’t clear on it: That still means we’re doing invalid stuff in ShouldAbortOnUncaughtException()
, getting properties isn’t allowed either way – it’s just going to work as long as we don’t invoke any runnable code, I guess. (But fixing that is definitely out of scope for this PR. 😄)
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 agree.
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 is what happens when you almost lose your mind to JS not working in that chunk of C++ code and forget how to write code... 😆Thanks @addaleax, that's a much cleaner solution.
lib/events.js
Outdated
@@ -435,13 +453,27 @@ function listenerCount(type) { | |||
} | |||
|
|||
EventEmitter.prototype.eventNames = function eventNames() { | |||
return this._eventsCount > 0 ? Reflect.ownKeys(this._events) : []; | |||
return this._events !== undefined && this._events.size ? | |||
[...this._events.keys()] : []; |
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 change? Is the spread slower than manually looping and filling an array?
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 be, honestly I wasn't sure if this was performance intensive code and it works quite fast as is. Happy to dive in deeper once I get the Proxy issue sorted.
lib/events.js
Outdated
// Adding the second element, need to change to array. | ||
existing = prepend ? [listener, existing] : [existing, listener]; | ||
events.set(type, existing); | ||
} else if (existing.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.
Perhaps this check should be more specific to avoid array cloning if we're adding a listener for a different event than what is being emitted.
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.
So existing
actually refers to the specific event so it only clones if we're emitting on that exact event.
So how much of the change in performance is related to delaying array cloning vs the change to |
@mscdex |
Will have an update later tonight or tomorrow with the other bits that are needed to make this work properly (Proxy, etc.). Thanks for the reviews everyone! (Also, I'm pretty sure Proxy can account for almost 100% of use cases of |
I'm also curious as to why |
While I welcome a Proxy-based approach, as a person who has used Proxies extensively I would have to say it is more work than one might expect at first. One has to be very careful with Proxies, and it is very likely that more hooks than expected need to be implemented. With special care, the end result can be very darn close to what one would get with
|
Can you elaborate? But just guessing here, |
Not to derail this PR, but from what I understood that would have only affected userland directly using |
@mscdex Imo the main difference is that |
I’m good with a proxy here because it will be used only in very old code, and optionally created. This code is already not in use in newer versions of modules, and I think we might want to use it runtime deprecate _events access. BTW, I still think that we should force userland to upgrade. I think the time is right. Given that the major problem is readable-stream, and we control that and in May all supported Node versions have prependListener (it was introduced in 6). I think deprecating unsupported versions of readable-stream is feasible, and we can do a patch version with the fix on all the old lines. |
I'm labeling as I will continue to commit and push to this branch so everyone can review as they want to, but I will be opening a new PR once all the work is done. I'm also going to try & split up the work into individual commits before opening the new PR. |
c0a23f3
to
88f0a14
Compare
CI: https://ci.nodejs.org/job/node-test-pull-request/11561/ Just testing some of the latest proxy changes. I'm starting to write up documentation of everything that has changed and will post an updated PR when done. Based on local testing this seems to survive CitGM without any hitches and also passes test suites for |
@refack @AndreasMadsen @mscdex Could you look at these results and provide any thoughts? My local benchmark results on a Mac (100 runs for each node version, both built today on top of master):
Benchmark CI (200 runs):
The biggest difference is in the I'm guessing this might be related to some underlying differences in the exact compiler used & GC on the different systems? Or am I missing something else? Also, this makes me wonder about whether we should generally have more than one benchmark machine to account for potential differences between Linux, OS X & Windows... |
AFAICT the results on your local machine for
|
@refack yeah the results for many arguments get noisy but
vs
But also, I've never seen that result fall bellow like 2.5% on my machine. In any of the tries with 100+ runs. Edit: Here's a quick check with 200 runs on my machine |
The p-value only means that the chances for finding the observed difference by chance (by getting only "extreme" results) is negligibly small. If the actual measurement is very small (which is what I saw locally for One of the reasons the benchmark machine was created, so that we have a stable, independent, and reproducible environment. It doesn't mean it's the one and only truth, just something we can all compare to. It's not as if real code runs tl;dr eliminating FUD from benchmarks is an art not a science 🤷♂️ |
@refack Makes sense. 👍 |
0faedf5
to
4f0465c
Compare
@apapirovski trust the CI results. It is almost impossible to not get inference from other processes on your own machine. |
@AndreasMadsen The CI results seem less trustworthy in this case though. In particular, ee-emit is coloured by 1 in 10 runs or so being about 1.75x faster on the old version (where the rest are equivalent). I've also spun up a separate instance on Joyent with SmartOS that was reporting similar results to what I'm seeing locally. |
Glad there were some disparities between my machine and the CI as it led me to start experimenting with our events benchmarks and tweaking them for further coverage. That led to the discovery that Map performance is significantly worse when dealing with more keys being added. So back to change this to work without a Map. I've made a copy of the current code to revisit it later once newer V8 versions are added — perhaps something changes then. |
a588f3e
to
f7ad115
Compare
We should have a rule-of-thumb that |
74de637
to
cc0f1de
Compare
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.
Refactor lib & src code to eliminate all deep reaches into the internal _events dictionary object, instead use available APIs and add an extra argument unwrap to listeners.
cc0f1de
to
7ec336b
Compare
I think I just need to find the time and integrate some calculations similar to those I made in #16888 (comment), such the benchmark tools report what improvement is measurable with one |
Instead of creating a dictionary object, use Map to store all the event handlers.
Instead of copying on each emit, set a flag that indicates an emit is in progress on that particular event name and copy only if events are added or removed.
Some other assorted changes to make this work. The one question mark is the changes to
_stream_readable.js
as I know there are some other issues around that (given that it's an npm package).(Also thanks to @addaleax for the help with the domains part after I spent half a day trying to make v8::Map work... life saver!)
Checklist
make -j4 test
(UNIX), orvcbuild test
(Windows) passesAffected core subsystem(s)
events, src, test