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: optimize for performance, remove _events use outside of events #17324

Closed
wants to merge 5 commits into from

Conversation

apapirovski
Copy link
Member

@apapirovski apapirovski commented Nov 26, 2017

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:

  • No longer copy array of listeners on emit but rather set a flag and only copy if that same array is modified while the emit is happening.
  • No longer use delete or Object.create(null) to reset the dictionary object, instead just set to undefined and modify the rest of the codebase to account for this change.
  • A few other assorted changes to improve performance.

2nd commit:

  • Introduce new wrappedListeners method which allows the user to retrieve the actual functions stored within _events (such as the once 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.
  • Remove remaining reaches into _events from C++

CitGM came back clean for these changes. Also, here are the results from the Benchmark CI:

events/ee-add-remove.js listeners=1 events=0 n=5000000     88.44 %        *** 3.644977e-199
events/ee-add-remove.js listeners=1 events=5 n=5000000    121.70 %        *** 9.927899e-110
events/ee-add-remove.js listeners=5 events=0 n=5000000     14.42 %        ***  6.703542e-82
events/ee-add-remove.js listeners=5 events=5 n=5000000     22.55 %        *** 8.566324e-104
events/ee-emit.js listeners=10 argc=0 n=20000000           36.31 %        *** 1.895656e-133
events/ee-emit.js listeners=10 argc=10 n=20000000          18.74 %        *** 1.108759e-125
events/ee-emit.js listeners=10 argc=2 n=20000000           20.78 %        *** 1.598679e-116
events/ee-emit.js listeners=10 argc=4 n=20000000           23.10 %        *** 3.275036e-110
events/ee-emit.js listeners=1 argc=0 n=20000000             3.46 %        ***  3.607081e-44
events/ee-emit.js listeners=1 argc=10 n=20000000            2.31 %        ***  1.616438e-12
events/ee-emit.js listeners=1 argc=2 n=20000000             0.63 %             9.366437e-02
events/ee-emit.js listeners=1 argc=4 n=20000000             1.04 %        ***  1.090454e-05
events/ee-emit.js listeners=5 argc=0 n=20000000            36.88 %        *** 5.504055e-157
events/ee-emit.js listeners=5 argc=10 n=20000000           19.54 %        *** 1.898596e-105
events/ee-emit.js listeners=5 argc=2 n=20000000            22.44 %        *** 3.811312e-108
events/ee-emit.js listeners=5 argc=4 n=20000000            25.42 %        *** 6.045287e-119
events/ee-emit-multi.js listeners=10 n=20000000            19.98 %        *** 1.808712e-160
events/ee-emit-multi.js listeners=1 n=20000000              1.46 %        ***  8.833914e-07
events/ee-emit-multi.js listeners=5 n=20000000             22.94 %        *** 1.293491e-115
events/ee-event-names.js n=1000000                        -29.22 %        *** 2.200089e-125
events/ee-listener-count-on-prototype.js n=50000000        -0.09 %             9.372100e-01
events/ee-listeners.js n=50000000                           0.22 %             5.623445e-01
events/ee-listeners-many.js n=10000000                     -0.37 %             4.721418e-01
events/ee-once.js listeners=1 n=5000000                    53.51 %        *** 3.023736e-140
events/ee-once.js listeners=5 n=5000000                     2.83 %        ***  1.692646e-08

Refs: #17074

Checklist
  • make -j4 test (UNIX), or vcbuild test (Windows) passes
  • tests and/or benchmarks are included
  • documentation is changed or added
  • commit message follows commit guidelines
Affected core subsystem(s)

domain, events, process

@apapirovski apapirovski added domain Issues and PRs related to the domain subsystem. events Issues and PRs related to the events subsystem / EventEmitter. lib / src Issues and PRs related to general changes in the lib or src directory. process Issues and PRs related to the process subsystem. semver-minor PRs that contain new features and should be released in the next minor version. labels Nov 26, 2017
@nodejs-github-bot nodejs-github-bot added the lib / src Issues and PRs related to general changes in the lib or src directory. label Nov 26, 2017
@apapirovski
Copy link
Member Author

Copy link
Member

@jasnell jasnell left a 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

-->
- `eventName` {any}
* `eventName` {any} The name of the event.
* `unwrap` {boolean} When set to true, will return the original listeners
Copy link
Member

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?

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 think it would need to be wrappedListeners or something similar. Will think on it.

Copy link
Member

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.

Copy link
Member Author

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;
Copy link
Member

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

Copy link
Member Author

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)?

Copy link
Member

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 :)

@mscdex
Copy link
Contributor

mscdex commented Nov 26, 2017

Why the large eventNames() regression?

@apapirovski
Copy link
Member Author

apapirovski commented Nov 26, 2017

@mscdex We have to iterate over _events now to find where listeners exist as opposed to just undefined. In practice, this seems less of an issue than the performance of on, removeListener and emit. Even packages that use eventNames (such as ultron) seem to benefit since they usually couple it with removeListener.

@apapirovski apapirovski force-pushed the patch-ee-perf branch 2 times, most recently from 6aeb42a to 73e0d13 Compare November 26, 2017 18:49
@apapirovski
Copy link
Member Author

@mscdex Refactored that method and now it iterates over event names without Reflect.ownKeys (using for..in & Object.getOwnPropertySymbols) which is mostly faster than it was previously, with the tiny exception being a really small list of events (less than 10) that includes Symbols. For most use cases there's now no regression.

 events/ee-event-names.js symbols="false" listeners=10 n=1000000      9.47 %        *** 2.556083e-48
 events/ee-event-names.js symbols="false" listeners=1 n=1000000      22.61 %        *** 3.692565e-20
 events/ee-event-names.js symbols="true" listeners=10 n=1000000      10.37 %        *** 2.488306e-95
 events/ee-event-names.js symbols="true" listeners=1 n=1000000      -25.36 %        *** 2.084307e-58

-->
- `eventName` {any}
* `eventName` {any} The name of the event.
* `unwrap` {boolean} When set to true, will return the original listeners
Copy link
Member

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;
Copy link
Member

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 :)

caught = process.domain._errorHandler(er);

if (!caught)
if (shouldCatch && !caught)
Copy link
Member

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?

Copy link
Member Author

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.

addaleax added a commit to addaleax/node that referenced this pull request Nov 27, 2017
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
@apapirovski
Copy link
Member Author

This will need #17333 and #17159 to land, at which point a significant portion of the 2nd commit will go away.

addaleax added a commit that referenced this pull request Nov 29, 2017
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>
@addaleax
Copy link
Member

@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.
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.
@apapirovski
Copy link
Member Author

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;
Copy link
Contributor

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.

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 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.

Copy link
Contributor

@mscdex mscdex Nov 29, 2017

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.

- `eventName` {any}

Returns a copy of the array of listeners for the event named `eventName`,
including any wrappers (such as those created by `.once`.
Copy link
Contributor

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`,
Copy link
Contributor

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'?

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 can change this but we have another 9 instances of this exact phrasing in this doc.

Copy link
Member

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;
Copy link
Contributor

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.

Copy link
Member Author

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);
Copy link
Contributor

@mscdex mscdex Nov 29, 2017

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?

Copy link
Member Author

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.)

@apapirovski
Copy link
Member Author

@addaleax & @TimothyGu would you consider reviewing this please? You had some feedback for the previous iteration of this in #17074

Thanks!

Copy link
Member

@addaleax addaleax left a 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 :)

@@ -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)
Copy link
Member

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? 😄 )

Copy link
Member

@addaleax addaleax left a 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`,
Copy link
Member

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) {
Copy link
Member

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) {
Copy link
Member

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);

Copy link
Member Author

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?

Copy link
Member

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) {
Copy link
Member

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?

@apapirovski apapirovski removed the domain Issues and PRs related to the domain subsystem. label Dec 2, 2017
}
} else if (typeof list !== 'function') {
position = -1;
events[type] = undefined;
Copy link
Contributor

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.

Copy link
Member Author

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;
Copy link
Contributor

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.

@apapirovski apapirovski added the blocked PRs that are blocked by other issues or PRs. label Dec 3, 2017
MylesBorins pushed a commit that referenced this pull request Dec 12, 2017
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>
MylesBorins pushed a commit that referenced this pull request Dec 12, 2017
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
blocked PRs that are blocked by other issues or PRs. events Issues and PRs related to the events subsystem / EventEmitter. lib / src Issues and PRs related to general changes in the lib or src directory. process Issues and PRs related to the process subsystem. semver-minor PRs that contain new features and should be released in the next minor version.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants