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: don't keep arrays with a single listener #12043

Closed
wants to merge 1 commit into from

Conversation

lpinca
Copy link
Member

@lpinca lpinca commented Mar 25, 2017

Use the remaining listener directly if the array of listeners has only one element after running EventEmitter.prototype.removeListener().

Advantages:

  • Better memory usage and better performance if no new listeners are added for the same event.

Disadvantages:

  • A new array must be created if new listeners are added for the same event.
Checklist
  • make -j4 test (UNIX), or vcbuild test (Windows) passes
  • tests and/or benchmarks are included
  • commit message follows commit guidelines
Affected core subsystem(s)

events

@nodejs-github-bot nodejs-github-bot added the events Issues and PRs related to the events subsystem / EventEmitter. label Mar 25, 2017
@mscdex
Copy link
Contributor

mscdex commented Mar 26, 2017

I fear this could cause userland issues like when I tried to change the internal storage structure when optimizing .once() a long while back. There's quite a few people that access _events externally.

@lpinca
Copy link
Member Author

lpinca commented Mar 26, 2017

@mscdex care to elaborate?

This doesn't change internal structures, it keeps them consistent (1 listener - no array, 2+ listeners - array). If people use ee._events they have to check if ee._events.whatever is an array or not regardless of this change.

lib/events.js Outdated
do {
this.removeListener(type, listeners[listeners.length - 1]);
} while (listeners[0]);
for (i = listeners.length; i-- > 0;) {
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not a big fan of this notation tbh. Why not move the -- operation to the 3rd chunk of the for-structure?

Copy link
Member Author

@lpinca lpinca Mar 28, 2017

Choose a reason for hiding this comment

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

I've used that because the same is done few lines above: https://github.com/nodejs/node/pull/12043/files#diff-71dcd327d0ca067b490b22d677f81966R366.

Copy link
Contributor

@ronkorving ronkorving Mar 28, 2017

Choose a reason for hiding this comment

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

Oh wow. Would love to see some feedback from one or two other @nodejs/collaborators on this. You're re-using this pattern here (where it wasn't here before). Do we really want to embrace that pattern or move away from it?

Copy link
Contributor

@seishun seishun Mar 28, 2017

Choose a reason for hiding this comment

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

I agree, this pattern is hard to understand. Let's change it in both places to something simpler.

Copy link
Member

Choose a reason for hiding this comment

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

Looks like this is the only place in core we use this pattern (although it appears a few times in v8 c++ code. It was added in 3e64b56. I think we should remove this and just use the standard for loop style.

➜ rg "i[-|+]+ [<|>]" | grep for
lib/events.js:        for (i = list.length; i-- > 0;) {
deps/v8/src/deoptimizer.cc:    for (size_t i = count; i-- > 0;) {
deps/v8/src/ast/ast-types.cc:  for (size_t i = BoundariesSize() - 1; i-- > 0;) {
deps/v8/src/full-codegen/full-codegen.cc:    for (int i = last; i-- > 0;) {
deps/v8/src/compiler/types.cc:  for (size_t i = BoundariesSize() - 1; i-- > 0;) {
deps/v8/src/parsing/parser.cc:    for (int i = labels->length(); i-- > 0;) {
deps/openssl/openssl/crypto/store/str_lib.c:        for (i = 0; i++ < STORE_ATTR_TYPE_NUM;)

Copy link
Member Author

Choose a reason for hiding this comment

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

🔧 in ca87422.

Copy link
Contributor

Choose a reason for hiding this comment

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

Thanks guys 👍

Copy link
Member Author

Choose a reason for hiding this comment

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

FWIW the same pattern is also used here

for (k = listeners.length; --k >= 0; /* empty */)

@mcollina
Copy link
Member

Does this affect performance? Can you please run https://github.com/nodejs/node/tree/master/benchmark/events?

@lpinca
Copy link
Member Author

lpinca commented Mar 28, 2017

@mcollina yes will do as soon as my internet connection will start working again.

@lpinca
Copy link
Member Author

lpinca commented Mar 28, 2017

$ node benchmark/compare.js --old ./node-master --new ./node-pr-12043 events > compare-pr-12043.csv
[00:13:25|% 100| 7/7 files | 60/60 runs | 1/1 configs]: Done
$ cat compare-pr-12043.csv | Rscript benchmark/compare.R

                                                     improvement confidence      p.value
 events/ee-add-remove.js n=250000                         0.42 %            4.469925e-01
 events/ee-emit-multi-args.js n=2000000                   0.13 %            8.361339e-01
 events/ee-emit.js n=2000000                              1.76 %          * 2.385787e-02
 events/ee-listener-count-on-prototype.js n=50000000     -0.75 %          * 1.356947e-02
 events/ee-listeners-many.js n=5000000                    0.21 %            3.401062e-01
 events/ee-listeners.js n=5000000                         2.10 %        *** 4.978391e-08
 events/ee-once.js n=20000000                             4.05 %        *** 1.101861e-41

Copy link
Member

@mcollina mcollina left a comment

Choose a reason for hiding this comment

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

LGTM, can you add that we get some speedup in the commit message?

@lpinca lpinca force-pushed the avoid/single-listener-arrays branch from ca87422 to 8979815 Compare March 28, 2017 19:22
@lpinca
Copy link
Member Author

lpinca commented Mar 28, 2017

@mcollina done, I've only mentioned the events/ee-once.js benchmark as I can't really see how this change could make a difference in events/ee-listeners.js or events/ee-emit.js.

@mscdex
Copy link
Contributor

mscdex commented Mar 28, 2017

It might be better to add some new benchmarks and refer to the results of those since the current benchmarks should not be affected by this PR's changes (except maybe ee-add-remove).

@lpinca
Copy link
Member Author

lpinca commented Mar 28, 2017

Agreed. ee-add-remove is actually a good benchmark for these changes. I'll remove the misleading info from commit message.

@lpinca lpinca force-pushed the avoid/single-listener-arrays branch from 8979815 to ac61a0a Compare March 28, 2017 20:33
Use the remaining listener directly if the array of listeners has only
one element after running `EventEmitter.prototype.removeListener()`.

Advantages:

- Better memory usage and better performance if no new listeners are
  added for the same event.

Disadvantages:

- A new array must be created if new listeners are added for the same
  event.
@lpinca lpinca force-pushed the avoid/single-listener-arrays branch from ac61a0a to 1b027c1 Compare April 3, 2017 19:04
@lpinca
Copy link
Member Author

lpinca commented Apr 3, 2017

This ad-hoc benchmark shows no difference in performance between master and this PR.

'use strict';
var common = require('../common.js');
var events = require('events');

var bench = common.createBenchmark(main, {n: [1e6]});

function main(conf) {
  var n = conf.n | 0;

  var ee = new events.EventEmitter();

  function listener1() {}
  function listener2() {}

  ee.on('removeListener', function() {});

  bench.start();
  for (var i = 0; i < n; i++) {
    ee.on('dummy', listener1);
    ee.on('dummy', listener2);
    ee.removeListener('dummy', listener2);
    ee.emit('dummy', 'arg');
    ee.on('dummy', listener2);
    ee.removeAllListeners('dummy');
  }
  bench.end(n);
}

Taking the best result over multiple runs I get

events/pr-12043.js n=1000000: 1,189,260.6881846066

for master and

events/pr-12043.js n=1000000: 1,186,814.6173030164

for this PR.

@lpinca
Copy link
Member Author

lpinca commented Apr 3, 2017

@ronkorving
Copy link
Contributor

ronkorving commented Apr 4, 2017

@lpinca Then I guess one of the advantages mentioned in the description of this PR (performance) is not really valid. I think the consistency argument is valid though.

@lpinca
Copy link
Member Author

lpinca commented Apr 4, 2017

@ronkorving well if I craft the benchmark to only remove listener2 and emit the event there is indeed a minimal performance gain and better memory usage (I made this PR for this particular case, see #11868 (comment)).

The benchmark also readd listener2 which is the "disadvantage" mentioned in commit message and then remove all listeners to see the overall impact.

@ronkorving
Copy link
Contributor

@lpinca Oh wow, interesting 👍

@lpinca
Copy link
Member Author

lpinca commented Apr 4, 2017

If there are no objections I'll land this tomorrow.

cc: @nodejs/collaborators

@ronkorving
Copy link
Contributor

LGTM 👍

jasnell pushed a commit that referenced this pull request Apr 4, 2017
Use the remaining listener directly if the array of listeners has only
one element after running `EventEmitter.prototype.removeListener()`.

Advantages:

- Better memory usage and better performance if no new listeners are
  added for the same event.

Disadvantages:

- A new array must be created if new listeners are added for the same
  event.

PR-URL: #12043
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Daijiro Wachi <daijiro.wachi@gmail.com>
Reviewed-By: Ron Korving <ron@ronkorving.nl>
@jasnell
Copy link
Member

jasnell commented Apr 4, 2017

Landed in 8d386ed

@jasnell jasnell closed this Apr 4, 2017
@lpinca lpinca deleted the avoid/single-listener-arrays branch April 4, 2017 18:32
@jasnell jasnell mentioned this pull request Apr 4, 2017
italoacasas pushed a commit to italoacasas/node that referenced this pull request Apr 10, 2017
Use the remaining listener directly if the array of listeners has only
one element after running `EventEmitter.prototype.removeListener()`.

Advantages:

- Better memory usage and better performance if no new listeners are
  added for the same event.

Disadvantages:

- A new array must be created if new listeners are added for the same
  event.

PR-URL: nodejs#12043
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Daijiro Wachi <daijiro.wachi@gmail.com>
Reviewed-By: Ron Korving <ron@ronkorving.nl>
@MylesBorins
Copy link
Contributor

MylesBorins commented Apr 18, 2017

looks like something we may want on v6.x, want to give it a bit more time to bake first

@cjihrig cjihrig mentioned this pull request Apr 19, 2017
2 tasks
cjihrig added a commit to cjihrig/node that referenced this pull request Apr 21, 2017
Commit 8d386ed stopped the
Event Emitter implementation from storing arrays containing a
single listener. This change left a section of code in
removeListener() as unreachable. This commit removes the
unreachable code.

Refs: nodejs#12043
PR-URL: nodejs#12501
Reviewed-By: Luigi Pinca <luigipinca@gmail.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
@MylesBorins MylesBorins added the baking-for-lts PRs that need to wait before landing in a LTS release. label May 15, 2017
@gibfahn gibfahn added backport-requested-v6.x and removed baking-for-lts PRs that need to wait before landing in a LTS release. labels Jun 18, 2017
@gibfahn
Copy link
Member

gibfahn commented Jun 18, 2017

@lpinca Should this be backported to v6.x-staging? If yes please follow the guide and raise a backport PR, if no let me know or add the dont-land-on label.

#12501 should probably be backported in the same PR.

lpinca pushed a commit to lpinca/node that referenced this pull request Jun 19, 2017
Commit 8d386ed stopped the
Event Emitter implementation from storing arrays containing a
single listener. This change left a section of code in
removeListener() as unreachable. This commit removes the
unreachable code.

Refs: nodejs#12043
PR-URL: nodejs#12501
Reviewed-By: Luigi Pinca <luigipinca@gmail.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
gibfahn pushed a commit that referenced this pull request Jun 20, 2017
Use the remaining listener directly if the array of listeners has only
one element after running `EventEmitter.prototype.removeListener()`.

Advantages:

- Better memory usage and better performance if no new listeners are
  added for the same event.

Disadvantages:

- A new array must be created if new listeners are added for the same
  event.

PR-URL: #12043
Backport-PR-URL: #13796
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Daijiro Wachi <daijiro.wachi@gmail.com>
Reviewed-By: Ron Korving <ron@ronkorving.nl>
gibfahn pushed a commit that referenced this pull request Jun 20, 2017
Commit 8d386ed stopped the
Event Emitter implementation from storing arrays containing a
single listener. This change left a section of code in
removeListener() as unreachable. This commit removes the
unreachable code.

Refs: #12043
PR-URL: #12501
Backport-PR-URL: #13796
Reviewed-By: Luigi Pinca <luigipinca@gmail.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
MylesBorins pushed a commit that referenced this pull request Jul 11, 2017
Use the remaining listener directly if the array of listeners has only
one element after running `EventEmitter.prototype.removeListener()`.

Advantages:

- Better memory usage and better performance if no new listeners are
  added for the same event.

Disadvantages:

- A new array must be created if new listeners are added for the same
  event.

PR-URL: #12043
Backport-PR-URL: #13796
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Daijiro Wachi <daijiro.wachi@gmail.com>
Reviewed-By: Ron Korving <ron@ronkorving.nl>
MylesBorins pushed a commit that referenced this pull request Jul 11, 2017
Commit 8d386ed stopped the
Event Emitter implementation from storing arrays containing a
single listener. This change left a section of code in
removeListener() as unreachable. This commit removes the
unreachable code.

Refs: #12043
PR-URL: #12501
Backport-PR-URL: #13796
Reviewed-By: Luigi Pinca <luigipinca@gmail.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
@MylesBorins MylesBorins mentioned this pull request Jul 18, 2017
abhishekumar-tyagi pushed a commit to abhishekumar-tyagi/node that referenced this pull request May 5, 2024
Use the remaining listener directly if the array of listeners has only
one element after running `EventEmitter.prototype.removeListener()`.

Advantages:

- Better memory usage and better performance if no new listeners are
  added for the same event.

Disadvantages:

- A new array must be created if new listeners are added for the same
  event.

PR-URL: nodejs/node#12043
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Daijiro Wachi <daijiro.wachi@gmail.com>
Reviewed-By: Ron Korving <ron@ronkorving.nl>
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.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

10 participants