events: optimize various functions
authorBrian White <mscdex@mscdex.net>
Thu, 5 Feb 2015 20:35:33 +0000 (15:35 -0500)
committerBen Noordhuis <info@bnoordhuis.nl>
Mon, 9 Feb 2015 16:47:49 +0000 (17:47 +0100)
Cache events and listeners objects where possible and loop over
Object.keys() instead of using for..in. These changes alone give
~60-65% improvement in the ee-add-remove benchmark.

The changes to EventEmitter.listenerCount() gives ~14%
improvement and changes to emitter.listeners() gives
significant improvements for <50 listeners
(~195% improvement for 10 listeners).

The changes to emitter.emit() gives 3x speedup for the fast
cases with multiple handlers and a minor speedup for the slow
case with multiple handlers.

The swapping out of the util.is* type checking functions with inline
checks gives another ~5-10% improvement.

PR-URL: https://github.com/iojs/io.js/pull/601
Reviewed-By: Ben Noordhuis <info@bnoordhuis.nl>
Reviewed-By: Evan Lucas <evanlucas@me.com>
lib/events.js
test/message/stdin_messages.out

index cece093..3e6f1af 100644 (file)
@@ -40,7 +40,7 @@ EventEmitter.init = function() {
 // that to be increased. Set to zero for unlimited.
 EventEmitter.prototype.setMaxListeners = function setMaxListeners(n) {
   if (typeof n !== 'number' || n < 0 || isNaN(n))
-    throw TypeError('n must be a positive number');
+    throw new TypeError('n must be a positive number');
   this._maxListeners = n;
   return this;
 };
@@ -55,22 +55,72 @@ 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);
+  }
+}
+
 EventEmitter.prototype.emit = function emit(type) {
-  var er, handler, len, args, i, listeners;
+  var er, handler, len, args, i, listeners, events, domain;
+  var needDomainExit = false;
 
-  if (!this._events)
-    this._events = {};
+  events = this._events;
+  if (!events)
+    events = this._events = {};
+
+  domain = this.domain;
 
   // If there is no 'error' event listener then throw.
-  if (type === 'error' && !this._events.error) {
+  if (type === 'error' && !events.error) {
     er = arguments[1];
-    if (this.domain) {
+    if (domain) {
       if (!er)
         er = new Error('Uncaught, unspecified "error" event.');
       er.domainEmitter = this;
-      er.domain = this.domain;
+      er.domain = domain;
       er.domainThrown = false;
-      this.domain.emit('error', er);
+      domain.emit('error', er);
     } else if (er instanceof Error) {
       throw er; // Unhandled 'error' event
     } else {
@@ -79,88 +129,94 @@ EventEmitter.prototype.emit = function emit(type) {
     return false;
   }
 
-  handler = this._events[type];
+  handler = events[type];
 
-  if (handler === undefined)
+  if (!handler)
     return false;
 
-  if (this.domain && this !== process)
-    this.domain.enter();
-
-  if (typeof handler === 'function') {
-    switch (arguments.length) {
-      // fast cases
-      case 1:
-        handler.call(this);
-        break;
-      case 2:
-        handler.call(this, arguments[1]);
-        break;
-      case 3:
-        handler.call(this, arguments[1], arguments[2]);
-        break;
-      // slower
-      default:
-        len = arguments.length;
-        args = new Array(len - 1);
-        for (i = 1; i < len; i++)
-          args[i - 1] = arguments[i];
+  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];
+      if (isFn)
         handler.apply(this, args);
-    }
-  } else if (handler !== null && typeof handler === 'object') {
-    len = arguments.length;
-    args = new Array(len - 1);
-    for (i = 1; i < len; i++)
-      args[i - 1] = arguments[i];
-
-    listeners = handler.slice();
-    len = listeners.length;
-    for (i = 0; i < len; i++)
-      listeners[i].apply(this, args);
+      else {
+        len = handler.length;
+        listeners = arrayClone(handler, len);
+        for (i = 0; i < len; ++i)
+          listeners[i].apply(this, args);
+      }
   }
 
-  if (this.domain && this !== process)
-    this.domain.exit();
+  if (needDomainExit)
+    domain.exit();
 
   return true;
 };
 
 EventEmitter.prototype.addListener = function addListener(type, listener) {
   var m;
+  var events;
+  var existing;
 
   if (typeof listener !== 'function')
-    throw TypeError('listener must be a function');
-
-  if (!this._events)
-    this._events = {};
-
-  // To avoid recursion in the case that type === "newListener"! Before
-  // adding it to the listeners, first emit "newListener".
-  if (this._events.newListener)
-    this.emit('newListener', type,
-              typeof listener.listener === 'function' ?
-              listener.listener : listener);
+    throw new TypeError('listener must be a function');
+
+  events = this._events;
+  if (!events)
+    events = this._events = {};
+  else {
+    // To avoid recursion in the case that type === "newListener"! Before
+    // adding it to the listeners, first emit "newListener".
+    if (events.newListener) {
+      this.emit('newListener', type,
+                typeof listener.listener === 'function' ?
+                listener.listener : listener);
+    }
+    existing = events[type];
+  }
 
-  if (!this._events[type])
+  if (!existing)
     // Optimize the case of one listener. Don't need the extra array object.
-    this._events[type] = listener;
-  else if (typeof this._events[type] === 'object')
+    existing = events[type] = listener;
+  else if (typeof existing !== 'function')
     // If we've already got an array, just append.
-    this._events[type].push(listener);
+    existing.push(listener);
   else
     // Adding the second element, need to change to array.
-    this._events[type] = [this._events[type], listener];
+    existing = events[type] = [existing, listener];
 
   // Check for listener leak
-  if (this._events[type] !== null && typeof this._events[type] === 'object' &&
-      !this._events[type].warned) {
-    var m = $getMaxListeners(this);
-    if (m && m > 0 && this._events[type].length > m) {
-      this._events[type].warned = true;
+  if (typeof existing !== 'function' && !existing.warned) {
+    m = $getMaxListeners(this);
+    if (m && m > 0 && existing.length > m) {
+      existing.warned = true;
       console.error('(node) warning: possible EventEmitter memory ' +
                     'leak detected. %d %s listeners added. ' +
                     'Use emitter.setMaxListeners() to increase limit.',
-                    this._events[type].length, type);
+                    existing.length, type);
       console.trace();
     }
   }
@@ -172,7 +228,7 @@ EventEmitter.prototype.on = EventEmitter.prototype.addListener;
 
 EventEmitter.prototype.once = function once(type, listener) {
   if (typeof listener !== 'function')
-    throw TypeError('listener must be a function');
+    throw new TypeError('listener must be a function');
 
   var fired = false;
 
@@ -194,26 +250,29 @@ EventEmitter.prototype.once = function once(type, listener) {
 // emits a 'removeListener' event iff the listener was removed
 EventEmitter.prototype.removeListener =
     function removeListener(type, listener) {
-      var list, position, length, i;
+      var list, events, position, length, i;
 
       if (typeof listener !== 'function')
-        throw TypeError('listener must be a function');
+        throw new TypeError('listener must be a function');
 
-      if (!this._events || !this._events[type])
+      events = this._events;
+      if (!events)
+        return this;
+
+      list = events[type];
+      if (!list)
         return this;
 
-      list = this._events[type];
       length = list.length;
       position = -1;
 
       if (list === listener ||
-          (typeof list.listener === 'function' &&
-          list.listener === listener)) {
-        delete this._events[type];
-        if (this._events.removeListener)
+          (typeof list.listener === 'function' && list.listener === listener)) {
+        delete events[type];
+        if (events.removeListener)
           this.emit('removeListener', type, listener);
 
-      } else if (list !== null && typeof list === 'object') {
+      } else if (typeof list !== 'function') {
         for (i = length; i-- > 0;) {
           if (list[i] === listener ||
               (list[i].listener && list[i].listener === listener)) {
@@ -227,12 +286,12 @@ EventEmitter.prototype.removeListener =
 
         if (list.length === 1) {
           list.length = 0;
-          delete this._events[type];
+          delete events[type];
         } else {
           spliceOne(list, position);
         }
 
-        if (this._events.removeListener)
+        if (events.removeListener)
           this.emit('removeListener', type, listener);
       }
 
@@ -241,23 +300,26 @@ EventEmitter.prototype.removeListener =
 
 EventEmitter.prototype.removeAllListeners =
     function removeAllListeners(type) {
-      var key, listeners;
+      var listeners, events;
 
-      if (!this._events)
+      events = this._events;
+      if (!events)
         return this;
 
       // not listening for removeListener, no need to emit
-      if (!this._events.removeListener) {
+      if (!events.removeListener) {
         if (arguments.length === 0)
           this._events = {};
-        else if (this._events[type])
-          delete this._events[type];
+        else if (events[type])
+          delete events[type];
         return this;
       }
 
       // emit removeListener for all listeners on all events
       if (arguments.length === 0) {
-        for (key in this._events) {
+        var keys = Object.keys(events);
+        for (var i = 0, key; i < keys.length; ++i) {
+          key = keys[i];
           if (key === 'removeListener') continue;
           this.removeAllListeners(key);
         }
@@ -266,7 +328,7 @@ EventEmitter.prototype.removeAllListeners =
         return this;
       }
 
-      listeners = this._events[type];
+      listeners = events[type];
 
       if (typeof listeners === 'function') {
         this.removeListener(type, listeners);
@@ -275,30 +337,44 @@ EventEmitter.prototype.removeAllListeners =
         while (listeners.length)
           this.removeListener(type, listeners[listeners.length - 1]);
       }
-      delete this._events[type];
+      delete events[type];
 
       return this;
     };
 
 EventEmitter.prototype.listeners = function listeners(type) {
+  var evlistener;
   var ret;
-  if (!this._events || !this._events[type])
+  var events = this._events;
+
+  if (!events)
     ret = [];
-  else if (typeof this._events[type] === 'function')
-    ret = [this._events[type]];
-  else
-    ret = this._events[type].slice();
+  else {
+    evlistener = events[type];
+    if (!evlistener)
+      ret = [];
+    else if (typeof evlistener === 'function')
+      ret = [evlistener];
+    else
+      ret = arrayClone(evlistener);
+  }
+
   return ret;
 };
 
 EventEmitter.listenerCount = function(emitter, type) {
-  var ret;
-  if (!emitter._events || !emitter._events[type])
-    ret = 0;
-  else if (typeof emitter._events[type] === 'function')
-    ret = 1;
-  else
-    ret = emitter._events[type].length;
+  var evlistener;
+  var ret = 0;
+  var events = emitter._events;
+
+  if (events) {
+    evlistener = events[type];
+    if (typeof evlistener === 'function')
+      ret = 1;
+    else if (evlistener)
+      ret = evlistener.length;
+  }
+
   return ret;
 };
 
@@ -308,3 +384,17 @@ function spliceOne(list, index) {
     list[i] = list[k];
   list.pop();
 }
+
+function arrayClone(arr, len) {
+  var ret;
+  if (len === undefined)
+    len = arr.length;
+  if (len >= 50)
+    ret = arr.slice();
+  else {
+    ret = new Array(len);
+    for (var i = 0; i < len; i += 1)
+      ret[i] = arr[i];
+  }
+  return ret;
+}
index de9da7f..5c0d86a 100644 (file)
@@ -9,6 +9,7 @@ SyntaxError: Strict mode code may not include a with statement
     at Module._compile (module.js:*:*)
     at evalScript (node.js:*:*)
     at Socket.<anonymous> (node.js:*:*)
+    at emitNone (events.js:*:*)
     at Socket.emit (events.js:*:*)
     at _stream_readable.js:*:*
     at process._tickCallback (node.js:*:*)
@@ -25,6 +26,7 @@ Error: hello
     at Module._compile (module.js:*:*)
     at evalScript (node.js:*:*)
     at Socket.<anonymous> (node.js:*:*)
+    at emitNone (events.js:*:*)
     at Socket.emit (events.js:*:*)
     at _stream_readable.js:*:*
     at process._tickCallback (node.js:*:*)
@@ -39,6 +41,7 @@ Error: hello
     at Module._compile (module.js:*:*)
     at evalScript (node.js:*:*)
     at Socket.<anonymous> (node.js:*:*)
+    at emitNone (events.js:*:*)
     at Socket.emit (events.js:*:*)
     at _stream_readable.js:*:*
     at process._tickCallback (node.js:*:*)
@@ -54,6 +57,7 @@ ReferenceError: y is not defined
     at Module._compile (module.js:*:*)
     at evalScript (node.js:*:*)
     at Socket.<anonymous> (node.js:*:*)
+    at emitNone (events.js:*:*)
     at Socket.emit (events.js:*:*)
     at _stream_readable.js:*:*
     at process._tickCallback (node.js:*:*)