From 0f23be8da97a2fe75752d55f8177a8bc744ab594 Mon Sep 17 00:00:00 2001 From: Brian White Date: Sat, 21 Feb 2015 02:20:03 -0500 Subject: [PATCH] events: improve eventEmitter.once() performance This commit improves once() performance by storing the event handler directly instead of creating a wrapper function every time. These changes bring ~150% increase in performance when simply adding once() event handlers, ~220% increase in the included ee-emit-once benchmark, and a ~50% increase in the included ee-add-remove-once benchmark. --- benchmark/events/ee-add-remove-once.js | 23 ++ benchmark/events/ee-emit-once.js | 19 + lib/_stream_readable.js | 28 +- lib/events.js | 354 ++++++++++++++---- src/env.h | 1 + src/node.cc | 3 + .../test-event-emitter-listener-count.js | 5 + test/parallel/test-event-emitter-listeners.js | 23 +- .../test-event-emitter-method-names.js | 2 +- 9 files changed, 376 insertions(+), 82 deletions(-) create mode 100644 benchmark/events/ee-add-remove-once.js create mode 100644 benchmark/events/ee-emit-once.js diff --git a/benchmark/events/ee-add-remove-once.js b/benchmark/events/ee-add-remove-once.js new file mode 100644 index 00000000000000..0d044b728512ed --- /dev/null +++ b/benchmark/events/ee-add-remove-once.js @@ -0,0 +1,23 @@ +var common = require('../common.js'); +var EventEmitter = require('events').EventEmitter; + +var bench = common.createBenchmark(main, {n: [25e6]}); + +function main(conf) { + var n = conf.n | 0; + + var ee = new EventEmitter(); + var listeners = []; + + for (var k = 0; k < 10; k += 1) + listeners.push(function() {}); + + bench.start(); + for (var i = 0; i < n; i += 1) { + for (var k = listeners.length; --k >= 0; /* empty */) + ee.once('dummy', listeners[k]); + for (var k = listeners.length; --k >= 0; /* empty */) + ee.removeListener('dummy', listeners[k]); + } + bench.end(n); +} diff --git a/benchmark/events/ee-emit-once.js b/benchmark/events/ee-emit-once.js new file mode 100644 index 00000000000000..6c53019e86dc2c --- /dev/null +++ b/benchmark/events/ee-emit-once.js @@ -0,0 +1,19 @@ +var common = require('../common.js'); +var EventEmitter = require('events').EventEmitter; + +var bench = common.createBenchmark(main, {n: [25e6]}); + +function main(conf) { + var n = conf.n | 0; + + var ee = new EventEmitter(); + + function noop() {} + + bench.start(); + for (var i = 0; i < n; i += 1) { + ee.once('dummy', noop); + ee.emit('dummy'); + } + bench.end(n); +} diff --git a/lib/_stream_readable.js b/lib/_stream_readable.js index c64333c58a1f16..00e468a9d9d389 100644 --- a/lib/_stream_readable.js +++ b/lib/_stream_readable.js @@ -555,10 +555,16 @@ Readable.prototype.pipe = function(dest, pipeOpts) { // is attached before any userland ones. NEVER DO THIS. if (!dest._events || !dest._events.error) dest.on('error', onerror); - else if (Array.isArray(dest._events.error)) - dest._events.error.unshift(onerror); - else - dest._events.error = [onerror, dest._events.error]; + else { + var existingHandler = dest._events.error; + if (Array.isArray(existingHandler)) + existingHandler.unshift(onerror); + else { + var handlers = [onerror, existingHandler]; + handlers.onceCount = (existingHandler.once ? 1 : 0); + dest._events.error = handlers; + } + } // Both close and finish should trigger unpipe, but only once. @@ -662,9 +668,7 @@ Readable.prototype.unpipe = function(dest) { // set up data events if they are asked for // Ensure readable listeners eventually get something -Readable.prototype.on = function(ev, fn) { - var res = Stream.prototype.on.call(this, ev, fn); - +Readable.prototype._on = function(ev) { // If listening to data, and it has not explicitly been paused, // then call resume to start the flow of data on the next tick. if (ev === 'data' && false !== this._readableState.flowing) { @@ -684,7 +688,15 @@ Readable.prototype.on = function(ev, fn) { } } } - +}; +Readable.prototype.on = function(ev, fn) { + var res = Stream.prototype.on.call(this, ev, fn); + this._on(ev); + return res; +}; +Readable.prototype.once = function(ev, fn) { + var res = Stream.prototype.once.call(this, ev, fn); + this._on(ev); return res; }; Readable.prototype.addListener = Readable.prototype.on; diff --git a/lib/events.js b/lib/events.js index 5860254654d8f1..bc7fd1e721704a 100644 --- a/lib/events.js +++ b/lib/events.js @@ -86,6 +86,38 @@ function emitNone(handler, isFn, self) { listeners[i].call(self); } } +function emitNoneOnce(handler, isFn, self, ev) { + var fired; + if (isFn) { + fired = handler.fired; + if (!fired) + handler.fired = true; + handler = handler.once; + self.removeListener(ev, handler); + if (fired) + return; + handler.call(self); + } else { + fired = false; + var len = handler.length; + var listeners = arrayClone(handler, len); + var once; + for (var i = 0; i < len; ++i) { + handler = listeners[i]; + once = handler.once; + if (once !== undefined) { + fired = handler.fired; + if (!fired) + handler.fired = true; + handler = once; + self.removeListener(ev, handler); + if (fired) + continue; + } + handler.call(self); + } + } +} function emitOne(handler, isFn, self, arg1) { if (isFn) handler.call(self, arg1); @@ -96,6 +128,38 @@ function emitOne(handler, isFn, self, arg1) { listeners[i].call(self, arg1); } } +function emitOneOnce(handler, isFn, self, ev, arg1) { + var fired; + if (isFn) { + fired = handler.fired; + if (!fired) + handler.fired = true; + handler = handler.once; + self.removeListener(ev, handler); + if (fired) + return; + handler.call(self, arg1); + } else { + fired = false; + var len = handler.length; + var listeners = arrayClone(handler, len); + var once; + for (var i = 0; i < len; ++i) { + handler = listeners[i]; + once = handler.once; + if (once) { + fired = handler.fired; + if (!fired) + handler.fired = true; + handler = once; + self.removeListener(ev, handler); + if (fired) + continue; + } + handler.call(self, arg1); + } + } +} function emitTwo(handler, isFn, self, arg1, arg2) { if (isFn) handler.call(self, arg1, arg2); @@ -106,6 +170,38 @@ function emitTwo(handler, isFn, self, arg1, arg2) { listeners[i].call(self, arg1, arg2); } } +function emitTwoOnce(handler, isFn, self, ev, arg1, arg2) { + var fired; + if (isFn) { + fired = handler.fired; + if (!fired) + handler.fired = true; + handler = handler.once; + self.removeListener(ev, handler); + if (fired) + return; + handler.call(self, arg1, arg2); + } else { + fired = false; + var len = handler.length; + var listeners = arrayClone(handler, len); + var once; + for (var i = 0; i < len; ++i) { + handler = listeners[i]; + once = handler.once; + if (once) { + fired = handler.fired; + if (!fired) + handler.fired = true; + handler = once; + self.removeListener(ev, handler); + if (fired) + continue; + } + handler.call(self, arg1, arg2); + } + } +} function emitThree(handler, isFn, self, arg1, arg2, arg3) { if (isFn) handler.call(self, arg1, arg2, arg3); @@ -116,7 +212,38 @@ function emitThree(handler, isFn, self, arg1, arg2, arg3) { listeners[i].call(self, arg1, arg2, arg3); } } - +function emitThreeOnce(handler, isFn, self, ev, arg1, arg2, arg3) { + var fired; + if (isFn) { + fired = handler.fired; + if (!fired) + handler.fired = true; + handler = handler.once; + self.removeListener(ev, handler); + if (fired) + return; + handler.call(self, arg1, arg2, arg3); + } else { + fired = false; + var len = handler.length; + var listeners = arrayClone(handler, len); + var once; + for (var i = 0; i < len; ++i) { + handler = listeners[i]; + once = handler.once; + if (once) { + fired = handler.fired; + if (!fired) + handler.fired = true; + handler = once; + self.removeListener(ev, handler); + if (fired) + continue; + } + handler.call(self, arg1, arg2, arg3); + } + } +} function emitMany(handler, isFn, self, args) { if (isFn) handler.apply(self, args); @@ -127,9 +254,43 @@ function emitMany(handler, isFn, self, args) { listeners[i].apply(self, args); } } +function emitManyOnce(handler, isFn, self, ev, args) { + var fired; + if (isFn) { + fired = handler.fired; + if (!fired) + handler.fired = true; + handler = handler.once; + self.removeListener(ev, handler); + if (fired) + return; + handler.apply(self, args); + } else { + fired = false; + var len = handler.length; + var listeners = arrayClone(handler, len); + var once; + for (var i = 0; i < len; ++i) { + handler = listeners[i]; + once = handler.once; + if (once) { + fired = handler.fired; + if (!fired) + handler.fired = true; + handler = once; + self.removeListener(ev, handler); + if (fired) + continue; + } + handler.apply(self, args); + } + } +} EventEmitter.prototype.emit = function emit(type) { - var er, handler, len, args, i, events, domain; + var er, handler, len, args, i, events, domain, onceCount; + var isArray = false; + var useOnce = false; var needDomainExit = false; var doError = (type === 'error'); @@ -172,28 +333,52 @@ EventEmitter.prototype.emit = function emit(type) { needDomainExit = true; } - var isFn = typeof handler === 'function'; len = arguments.length; + + if (typeof handler !== 'function') { + onceCount = handler.onceCount; + useOnce = (onceCount !== 0); + isArray = (onceCount !== undefined); + } + switch (len) { // fast cases case 1: - emitNone(handler, isFn, this); + if (!useOnce) + emitNone(handler, !isArray, this); + else + emitNoneOnce(handler, !isArray, this, type); break; case 2: - emitOne(handler, isFn, this, arguments[1]); + if (!useOnce) + emitOne(handler, !isArray, this, arguments[1]); + else + emitOneOnce(handler, !isArray, this, type, arguments[1]); break; case 3: - emitTwo(handler, isFn, this, arguments[1], arguments[2]); + if (!useOnce) + emitTwo(handler, !isArray, this, arguments[1], arguments[2]); + else + emitTwoOnce(handler, !isArray, this, type, arguments[1], arguments[2]); break; case 4: - emitThree(handler, isFn, this, arguments[1], arguments[2], arguments[3]); + if (!useOnce) { + emitThree(handler, !isArray, this, arguments[1], arguments[2], + arguments[3]); + } else { + emitThreeOnce(handler, !isArray, this, type, 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); + if (!useOnce) + emitMany(handler, !isArray, this, args); + else + emitManyOnce(handler, !isArray, this, type, args); } if (needDomainExit) @@ -202,12 +387,12 @@ EventEmitter.prototype.emit = function emit(type) { return true; }; -EventEmitter.prototype.addListener = function addListener(type, listener) { +EventEmitter.prototype._addListener = function _addListener(type, fn, once) { var m; var events; var existing; - if (typeof listener !== 'function') + if (typeof fn !== 'function') throw new TypeError('"listener" argument must be a function'); events = this._events; @@ -218,11 +403,10 @@ EventEmitter.prototype.addListener = function addListener(type, listener) { // 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, - listener.listener ? listener.listener : listener); + this.emit('newListener', type, fn); - // Re-assign `events` because a newListener handler could have caused the - // this._events to be assigned to a new object + // Re-assign `events` because a newListener handler could have caused + // `this._events` to be assigned to a new object events = this._events; } existing = events[type]; @@ -230,15 +414,32 @@ EventEmitter.prototype.addListener = function addListener(type, listener) { if (!existing) { // Optimize the case of one listener. Don't need the extra array object. - existing = events[type] = listener; + if (once) + existing = events[type] = { once: fn, fired: false }; + else + existing = events[type] = fn; ++this._eventsCount; } else { - if (typeof existing === 'function') { + var existingOnce = existing.once; + if (typeof existing === 'function' || existingOnce) { // Adding the second element, need to change to array. - existing = events[type] = [existing, listener]; + if (once) { + existing = events[type] = [ + existing, + { once: fn, fired: false } + ]; + existing.onceCount = (existingOnce ? 2 : 1); + } else { + existing = events[type] = [existing, fn]; + existing.onceCount = (existingOnce ? 1 : 0); + } } else { // If we've already got an array, just append. - existing.push(listener); + if (once) { + existing.push({ once: fn, fired: false }); + ++existing.onceCount; + } else + existing.push(fn); } // Check for listener leak @@ -261,33 +462,24 @@ EventEmitter.prototype.addListener = function addListener(type, listener) { return this; }; -EventEmitter.prototype.on = EventEmitter.prototype.addListener; - -EventEmitter.prototype.once = function once(type, listener) { - if (typeof listener !== 'function') - throw new TypeError('"listener" argument must be a function'); - - var fired = false; - - function g() { - this.removeListener(type, g); - - if (!fired) { - fired = true; - listener.apply(this, arguments); - } - } +EventEmitter.prototype.on = function on(type, fn) { + if (typeof this._addListener !== 'function') + return EventEmitter.prototype._addListener.call(this, type, fn, false); + return this._addListener(type, fn, false); +}; - g.listener = listener; - this.on(type, g); +EventEmitter.prototype.addListener = EventEmitter.prototype.on; - return this; +EventEmitter.prototype.once = function once(type, fn) { + if (typeof this._addListener !== 'function') + return EventEmitter.prototype._addListener.call(this, type, fn, true); + return this._addListener(type, fn, true); }; // emits a 'removeListener' event iff the listener was removed EventEmitter.prototype.removeListener = function removeListener(type, listener) { - var list, events, position, i; + var list, events, position, i, curListener, len, once, isOnce; if (typeof listener !== 'function') throw new TypeError('"listener" argument must be a function'); @@ -300,7 +492,7 @@ EventEmitter.prototype.removeListener = if (!list) return this; - if (list === listener || (list.listener && list.listener === listener)) { + if ((list.once || list) === listener) { if (--this._eventsCount === 0) this._events = {}; else { @@ -310,28 +502,40 @@ EventEmitter.prototype.removeListener = } } else if (typeof list !== 'function') { position = -1; - - for (i = list.length; i-- > 0;) { - if (list[i] === listener || - (list[i].listener && list[i].listener === listener)) { - position = i; - break; + len = list.length; + isOnce = false; + + if (list.onceCount > 0) { + for (i = len; i-- > 0;) { + curListener = list[i]; + once = curListener.once; + if ((once || curListener) === listener) { + isOnce = (once === listener); + position = i; + break; + } + } + } else { + for (i = len; i-- > 0;) { + curListener = list[i]; + if (curListener === listener) { + position = i; + break; + } } } if (position < 0) return this; - if (list.length === 1) { - list[0] = undefined; - if (--this._eventsCount === 0) { - this._events = {}; - return this; - } else { - delete events[type]; - } + if (len !== 2) { + if (isOnce) + --list.onceCount; + spliceOne(list, position, len); + } else if (position === 0) { + events[type] = list[1]; } else { - spliceOne(list, position); + events[type] = list[0]; } if (events.removeListener) @@ -378,14 +582,16 @@ EventEmitter.prototype.removeAllListeners = } listeners = events[type]; + if (!listeners) + return this; - if (typeof listeners === 'function') { + if (typeof (listeners.once || listeners) === 'function') { this.removeListener(type, listeners); } else if (listeners) { // LIFO order - do { - this.removeListener(type, listeners[listeners.length - 1]); - } while (listeners[0]); + for (i = listeners.length; i-- > 0;) { + this.removeListener(type, listeners[i]); + } } return this; @@ -402,10 +608,15 @@ EventEmitter.prototype.listeners = function listeners(type) { evlistener = events[type]; if (!evlistener) ret = []; - else if (typeof evlistener === 'function') - ret = [evlistener]; - else + else if (evlistener.onceCount === 0) ret = arrayClone(evlistener, evlistener.length); + else { + var once = evlistener.once; + if (typeof (once || evlistener) === 'function') + ret = [once || evlistener]; + else + ret = onceClone(evlistener); + } } return ret; @@ -425,8 +636,10 @@ function listenerCount(type) { if (events) { const evlistener = events[type]; - - if (typeof evlistener === 'function') { + if (typeof evlistener === 'function' || + (typeof evlistener === 'object' && + evlistener !== null && + typeof evlistener.once === 'function')) { return 1; } else if (evlistener) { return evlistener.length; @@ -437,12 +650,23 @@ function listenerCount(type) { } // About 1.5x faster than the two-arg version of Array#splice(). -function spliceOne(list, index) { - for (var i = index, k = i + 1, n = list.length; k < n; i += 1, k += 1) +function spliceOne(list, index, len) { + for (var i = index, k = i + 1, n = len || list.length; k < n; i += 1, k += 1) list[i] = list[k]; list.pop(); } + +function onceClone(arr) { + var len = arr.length; + var ret = new Array(len); + for (var i = 0, fn; i < len; i += 1) { + fn = arr[i]; + ret[i] = fn.once || fn; + } + return ret; +} + function arrayClone(arr, i) { var copy = new Array(i); while (i--) diff --git a/src/env.h b/src/env.h index e160e62310e8ec..3868d9170ada0b 100644 --- a/src/env.h +++ b/src/env.h @@ -152,6 +152,7 @@ namespace node { V(nsname_string, "nsname") \ V(ocsp_request_string, "OCSPRequest") \ V(offset_string, "offset") \ + V(once_string, "once") \ V(onchange_string, "onchange") \ V(onclienthello_string, "onclienthello") \ V(oncomplete_string, "oncomplete") \ diff --git a/src/node.cc b/src/node.cc index ab586272d30549..437a733d25697f 100644 --- a/src/node.cc +++ b/src/node.cc @@ -966,6 +966,9 @@ static bool DomainHasErrorHandler(const Environment* env, domain_event_listeners_o->Get(env->error_string()); if (domain_error_listeners_v->IsFunction() || + (domain_error_listeners_v->IsObject() && + domain_error_listeners_v.As()->Get(env->once_string()) + ->IsFunction()) || (domain_error_listeners_v->IsArray() && domain_error_listeners_v.As()->Length() > 0)) return true; diff --git a/test/parallel/test-event-emitter-listener-count.js b/test/parallel/test-event-emitter-listener-count.js index ebfed8b2ee33b1..832d70eaea5a21 100644 --- a/test/parallel/test-event-emitter-listener-count.js +++ b/test/parallel/test-event-emitter-listener-count.js @@ -16,3 +16,8 @@ assert.strictEqual(emitter.listenerCount('foo'), 2); assert.strictEqual(emitter.listenerCount('bar'), 0); assert.strictEqual(emitter.listenerCount('baz'), 1); assert.strictEqual(emitter.listenerCount(123), 1); + + +const emitter2 = new EventEmitter(); +emitter2.once('foo', function() {}); +assert.strictEqual(emitter2.listenerCount('foo'), 1); diff --git a/test/parallel/test-event-emitter-listeners.js b/test/parallel/test-event-emitter-listeners.js index 77c44907b62d8c..27ba0d56ccf611 100644 --- a/test/parallel/test-event-emitter-listeners.js +++ b/test/parallel/test-event-emitter-listeners.js @@ -1,32 +1,39 @@ 'use strict'; require('../common'); -var assert = require('assert'); -var events = require('events'); +const assert = require('assert'); +const events = require('events'); function listener() {} function listener2() {} -var e1 = new events.EventEmitter(); +const e1 = new events.EventEmitter(); e1.on('foo', listener); -var fooListeners = e1.listeners('foo'); +const fooListeners = e1.listeners('foo'); assert.deepEqual(e1.listeners('foo'), [listener]); e1.removeAllListeners('foo'); assert.deepEqual(e1.listeners('foo'), []); assert.deepEqual(fooListeners, [listener]); -var e2 = new events.EventEmitter(); +const e2 = new events.EventEmitter(); e2.on('foo', listener); -var e2ListenersCopy = e2.listeners('foo'); +const e2ListenersCopy = e2.listeners('foo'); assert.deepEqual(e2ListenersCopy, [listener]); assert.deepEqual(e2.listeners('foo'), [listener]); e2ListenersCopy.push(listener2); assert.deepEqual(e2.listeners('foo'), [listener]); assert.deepEqual(e2ListenersCopy, [listener, listener2]); -var e3 = new events.EventEmitter(); +const e3 = new events.EventEmitter(); e3.on('foo', listener); -var e3ListenersCopy = e3.listeners('foo'); +const e3ListenersCopy = e3.listeners('foo'); e3.on('foo', listener2); assert.deepEqual(e3.listeners('foo'), [listener, listener2]); assert.deepEqual(e3ListenersCopy, [listener]); + +const e4 = new events.EventEmitter(); +e4.once('foo', listener); +const e4ListenersCopy = e4.listeners('foo'); +e4.on('foo', listener2); +assert.deepEqual(e4.listeners('foo'), [listener, listener2]); +assert.deepEqual(e4ListenersCopy, [listener]); diff --git a/test/parallel/test-event-emitter-method-names.js b/test/parallel/test-event-emitter-method-names.js index c1e6540f0184af..a43ac74bfa89b2 100644 --- a/test/parallel/test-event-emitter-method-names.js +++ b/test/parallel/test-event-emitter-method-names.js @@ -7,7 +7,7 @@ var E = events.EventEmitter.prototype; assert.equal(E.constructor.name, 'EventEmitter'); assert.equal(E.on, E.addListener); // Same method. Object.getOwnPropertyNames(E).forEach(function(name) { - if (name === 'constructor' || name === 'on') return; + if (name === 'constructor' || name === 'addListener') return; if (typeof E[name] !== 'function') return; assert.equal(E[name].name, name); });