From b677b844fc1de328a0f2b0151bdfc045cb5d0c81 Mon Sep 17 00:00:00 2001 From: Brian White Date: Thu, 5 Feb 2015 15:35:33 -0500 Subject: [PATCH] events: optimize various functions 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 Reviewed-By: Evan Lucas --- lib/events.js | 286 ++++++++++++++++++++++++++-------------- test/message/stdin_messages.out | 4 + 2 files changed, 192 insertions(+), 98 deletions(-) diff --git a/lib/events.js b/lib/events.js index cece093..3e6f1af 100644 --- a/lib/events.js +++ b/lib/events.js @@ -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; +} diff --git a/test/message/stdin_messages.out b/test/message/stdin_messages.out index de9da7f..5c0d86a 100644 --- a/test/message/stdin_messages.out +++ b/test/message/stdin_messages.out @@ -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. (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. (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. (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. (node.js:*:*) + at emitNone (events.js:*:*) at Socket.emit (events.js:*:*) at _stream_readable.js:*:* at process._tickCallback (node.js:*:*) -- 2.7.4