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: improve performance for emit(), on(), and listeners() #13155

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 10 additions & 8 deletions benchmark/events/ee-add-remove.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,26 @@
var common = require('../common.js');
var events = require('events');

var bench = common.createBenchmark(main, {n: [25e4]});
var bench = common.createBenchmark(main, {n: [25e4], listeners: [10]});

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

var ee = new events.EventEmitter();
var listeners = [];
ee.setMaxListeners(listeners + 1);
var fns = [];

var k;
for (k = 0; k < 10; k += 1)
listeners.push(function() {});
for (k = 0; k < listeners; k += 1)
fns.push(function() { return 0; });

bench.start();
for (var i = 0; i < n; i += 1) {
for (k = listeners.length; --k >= 0; /* empty */)
ee.on('dummy', listeners[k]);
for (k = listeners.length; --k >= 0; /* empty */)
ee.removeListener('dummy', listeners[k]);
for (k = listeners; --k >= 0; /* empty */)
ee.on('dummy', fns[k]);
for (k = listeners; --k >= 0; /* empty */)
ee.removeListener('dummy', fns[k]);
}
bench.end(n);
}
8 changes: 5 additions & 3 deletions benchmark/events/ee-emit.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,17 @@
var common = require('../common.js');
var EventEmitter = require('events').EventEmitter;

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

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

var ee = new EventEmitter();
ee.setMaxListeners(listeners + 1);

for (var k = 0; k < 10; k += 1)
ee.on('dummy', function() {});
for (var k = 0; k < listeners; k += 1)
ee.on('dummy', function() { return 0; });

bench.start();
for (var i = 0; i < n; i += 1) {
Expand Down
21 changes: 0 additions & 21 deletions benchmark/events/ee-listeners-many.js

This file was deleted.

8 changes: 5 additions & 3 deletions benchmark/events/ee-listeners.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,17 @@
var common = require('../common.js');
var EventEmitter = require('events').EventEmitter;

var bench = common.createBenchmark(main, {n: [5e6]});
var bench = common.createBenchmark(main, {n: [5e6], listeners: [10, 100]});

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

var ee = new EventEmitter();
ee.setMaxListeners(listeners + 1);

for (var k = 0; k < 10; k += 1)
ee.on('dummy', function() {});
for (var k = 0; k < listeners; k += 1)
ee.on('dummy', function() { return 0; });

bench.start();
for (var i = 0; i < n; i += 1) {
Expand Down
205 changes: 114 additions & 91 deletions lib/events.js
Original file line number Diff line number Diff line change
Expand Up @@ -86,74 +86,18 @@ EventEmitter.prototype.setMaxListeners = function setMaxListeners(n) {
};

function $getMaxListeners(that) {
if (that._maxListeners === undefined)
const maxListeners = that._maxListeners;
if (maxListeners === undefined)
return EventEmitter.defaultMaxListeners;
return that._maxListeners;
return maxListeners;
}

EventEmitter.prototype.getMaxListeners = function getMaxListeners() {
return $getMaxListeners(this);
};

// These standalone emit* functions are used to optimize calling of event
// handlers for fast cases because emit() itself often has a variable number of
// arguments and can be deoptimized because of that. These functions always have
// the same number of arguments and thus do not get deoptimized, so the code
// inside them can execute faster.
function emitNone(handler, isFn, self) {
if (isFn)
handler.call(self);
else {
var len = handler.length;
var listeners = arrayClone(handler, len);
for (var i = 0; i < len; ++i)
listeners[i].call(self);
}
}
function emitOne(handler, isFn, self, arg1) {
if (isFn)
handler.call(self, arg1);
else {
var len = handler.length;
var listeners = arrayClone(handler, len);
for (var i = 0; i < len; ++i)
listeners[i].call(self, arg1);
}
}
function emitTwo(handler, isFn, self, arg1, arg2) {
if (isFn)
handler.call(self, arg1, arg2);
else {
var len = handler.length;
var listeners = arrayClone(handler, len);
for (var i = 0; i < len; ++i)
listeners[i].call(self, arg1, arg2);
}
}
function emitThree(handler, isFn, self, arg1, arg2, arg3) {
if (isFn)
handler.call(self, arg1, arg2, arg3);
else {
var len = handler.length;
var listeners = arrayClone(handler, len);
for (var i = 0; i < len; ++i)
listeners[i].call(self, arg1, arg2, arg3);
}
}

function emitMany(handler, isFn, self, args) {
if (isFn)
handler.apply(self, args);
else {
var len = handler.length;
var listeners = arrayClone(handler, len);
for (var i = 0; i < len; ++i)
listeners[i].apply(self, args);
}
}

EventEmitter.prototype.emit = function emit(type) {
var er, handler, len, args, i, events, domain;
var er, fn, i, events, domain;
var needDomainExit = false;
var doError = (type === 'error');

Expand Down Expand Up @@ -189,38 +133,103 @@ EventEmitter.prototype.emit = function emit(type) {
return false;
}

handler = events[type];
fn = events[type];

if (!handler)
if (!fn)
return false;

if (domain && this !== process) {
domain.enter();
needDomainExit = true;
}

var isFn = typeof handler === 'function';
len = arguments.length;
switch (len) {
// fast cases
case 1:
emitNone(handler, isFn, this);
break;
case 2:
emitOne(handler, isFn, this, arguments[1]);
break;
case 3:
emitTwo(handler, isFn, this, arguments[1], arguments[2]);
break;
case 4:
emitThree(handler, isFn, this, arguments[1], arguments[2], arguments[3]);
break;
// slower
default:
args = new Array(len - 1);
for (i = 1; i < len; i++)
args[i - 1] = arguments[i];
emitMany(handler, isFn, this, args);
var len = arguments.length;
var args;
if (typeof fn === 'function') {
switch (len) {
Copy link

Choose a reason for hiding this comment

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

Maybe if simplified to will be faster?

EventEmitter.prototype.emit = function emit(type, ...args) {
  /* code */
  fn.call(this, ...args);
  /* code */
}

case 1: fn.call(this); break;
case 2: fn.call(this, arguments[1]); break;
case 3: fn.call(this, arguments[1], arguments[2]); break;
case 4: fn.call(this, arguments[1], arguments[2], arguments[3]);
break;
default:
args = new Array(len - 1);
for (i = 1; i < len; i++)
args[i - 1] = arguments[i];
fn.apply(this, args);
}
} else {
var fnlen = fn.length;
fn = arrayClone(fn, fnlen);
switch (len) {
case 1:
fn[0].call(this);
fn[1].call(this);
if (fnlen === 2) break;
fn[2].call(this);
if (fnlen === 3) break;
fn[3].call(this);
if (fnlen === 4) break;
fn[4].call(this);
if (fnlen === 5) break;
Copy link
Member

Choose a reason for hiding this comment

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

if (fnlen === 5) [](start = 7, length = 17)

Is there any significance of unrolling the loop for 5 ? Or is it that in most common scenario there are not more than 5 event handlers to trigger? Either case, could you write a comment about it?

for (i = 5; i < fnlen; ++i)
fn[i].call(this);
break;
case 2:
fn[0].call(this, arguments[1]);
fn[1].call(this, arguments[1]);
if (fnlen === 2) break;
fn[2].call(this, arguments[1]);
if (fnlen === 3) break;
fn[3].call(this, arguments[1]);
if (fnlen === 4) break;
fn[4].call(this, arguments[1]);
if (fnlen === 5) break;
for (i = 5; i < fnlen; ++i)
fn[i].call(this, arguments[1]);
break;
case 3:
fn[0].call(this, arguments[1], arguments[2]);
fn[1].call(this, arguments[1], arguments[2]);
if (fnlen === 2) break;
fn[2].call(this, arguments[1], arguments[2]);
if (fnlen === 3) break;
fn[3].call(this, arguments[1], arguments[2]);
if (fnlen === 4) break;
fn[4].call(this, arguments[1], arguments[2]);
if (fnlen === 5) break;
for (i = 5; i < fnlen; ++i)
fn[i].call(this, arguments[1], arguments[2]);
break;
case 4:
fn[0].call(this, arguments[1], arguments[2], arguments[3]);
fn[1].call(this, arguments[1], arguments[2], arguments[3]);
if (fnlen === 2) break;
fn[2].call(this, arguments[1], arguments[2], arguments[3]);
if (fnlen === 3) break;
fn[3].call(this, arguments[1], arguments[2], arguments[3]);
if (fnlen === 4) break;
fn[4].call(this, arguments[1], arguments[2], arguments[3]);
if (fnlen === 5) break;
for (i = 5; i < fnlen; ++i)
fn[i].call(this, arguments[1], arguments[2], arguments[3]);
break;
default:
args = new Array(len - 1);
for (i = 1; i < len; i++)
args[i - 1] = arguments[i];
fn[0].apply(this, args);
fn[1].apply(this, args);
if (fnlen === 2) break;
fn[2].apply(this, args);
if (fnlen === 3) break;
fn[3].apply(this, args);
if (fnlen === 4) break;
fn[4].apply(this, args);
if (fnlen === 5) break;
for (i = 5; i < fnlen; ++i)
fn[i].apply(this, args);
}
}

if (needDomainExit)
Expand All @@ -230,7 +239,6 @@ EventEmitter.prototype.emit = function emit(type) {
};

function _addListener(target, type, listener, prepend) {
var m;
var events;
var existing;

Expand Down Expand Up @@ -275,7 +283,7 @@ function _addListener(target, type, listener, prepend) {

// Check for listener leak
if (!existing.warned) {
m = $getMaxListeners(target);
const m = $getMaxListeners(target);
if (m && m > 0 && existing.length > m) {
existing.warned = true;
const w = new Error('Possible EventEmitter memory leak detected. ' +
Expand Down Expand Up @@ -497,16 +505,31 @@ function spliceOne(list, index) {
}

function arrayClone(arr, n) {
var copy = new Array(n);
switch (n) {
case 2: return [arr[0], arr[1]];
case 3: return [arr[0], arr[1], arr[2]];
case 4: return [arr[0], arr[1], arr[2], arr[3]];
case 5: return [arr[0], arr[1], arr[2], arr[3], arr[4]];
}
const copy = new Array(n);
for (var i = 0; i < n; ++i)
copy[i] = arr[i];
return copy;
}

function unwrapListeners(arr) {
const ret = new Array(arr.length);
for (var i = 0; i < ret.length; ++i) {
ret[i] = arr[i].listener || arr[i];
const first = arr[0].listener || arr[0];
const second = arr[1].listener || arr[1];
const n = arr.length;
switch (n) {
case 2: return [first, second];
case 3: return [first, second, arr[2].listener || arr[2]];
case 4: return [first, second, arr[2].listener || arr[2],
arr[3].listener || arr[3]];
default:
const copy = new Array(n);
for (var i = 0; i < n; ++i)
copy[i] = arr[i].listener || arr[i];
return copy;
}
return ret;
}
Loading