diff --git a/benchmark/events/ee-add-remove.js b/benchmark/events/ee-add-remove.js index 7b6ec35f29b636..ea6268bd011c57 100644 --- a/benchmark/events/ee-add-remove.js +++ b/benchmark/events/ee-add-remove.js @@ -2,18 +2,30 @@ const common = require('../common.js'); const events = require('events'); -const bench = common.createBenchmark(main, { n: [25e4] }); +const bench = common.createBenchmark(main, { + n: [5e6], + events: [0, 5], + listeners: [1, 5] +}); function main(conf) { - const n = conf.n | 0; + let n = conf.n | 0; + const eventsCOunt = conf.events | 0; + const listenersCount = conf.listeners | 0; const ee = new events.EventEmitter(); const listeners = []; + if (listenersCount === 1) + n *= 2; + var k; - for (k = 0; k < 10; k += 1) + for (k = 0; k < listenersCount; k += 1) listeners.push(function() {}); + for (k = 0; k < eventsCOunt; k++) + ee.on(`dummyunused${k}`, () => {}); + bench.start(); for (var i = 0; i < n; i += 1) { const dummy = (i % 2 === 0) ? 'dummy0' : 'dummy1'; diff --git a/benchmark/events/ee-emit-multi.js b/benchmark/events/ee-emit-multi.js new file mode 100644 index 00000000000000..b92206f8361d64 --- /dev/null +++ b/benchmark/events/ee-emit-multi.js @@ -0,0 +1,36 @@ +'use strict'; +const common = require('../common.js'); +const EventEmitter = require('events').EventEmitter; + +const bench = common.createBenchmark(main, { + n: [2e7], + listeners: [1, 5, 10], +}); + +function main(conf) { + var n = conf.n | 0; + const listeners = Math.max(conf.listeners | 0, 1); + + const ee = new EventEmitter(); + + if (listeners === 1) + n *= 5; + else if (listeners === 5) + n *= 2; + + for (var k = 0; k < listeners; k += 1) { + ee.on('dummy', function() {}); + ee.on(`dummy${k}`, function() {}); + } + + bench.start(); + for (var i = 0; i < n; i += 1) { + if (i % 3 === 0) + ee.emit('dummy', true, 5); + else if (i % 2 === 0) + ee.emit('dummy', true, 5, 10, false); + else + ee.emit('dummy'); + } + bench.end(n); +} diff --git a/benchmark/events/ee-emit.js b/benchmark/events/ee-emit.js index 3d7eb43b228b71..de60f938ddd5b7 100644 --- a/benchmark/events/ee-emit.js +++ b/benchmark/events/ee-emit.js @@ -3,20 +3,27 @@ const common = require('../common.js'); const EventEmitter = require('events').EventEmitter; const bench = common.createBenchmark(main, { - n: [2e6], + n: [2e7], argc: [0, 2, 4, 10], listeners: [1, 5, 10], }); function main(conf) { - const n = conf.n | 0; + var n = conf.n | 0; const argc = conf.argc | 0; const listeners = Math.max(conf.listeners | 0, 1); const ee = new EventEmitter(); - for (var k = 0; k < listeners; k += 1) + if (listeners === 1) + n *= 5; + else if (listeners === 5) + n *= 2; + + for (var k = 0; k < listeners; k += 1) { ee.on('dummy', function() {}); + ee.on(`dummy${k}`, function() {}); + } var i; switch (argc) { diff --git a/benchmark/events/ee-event-names.js b/benchmark/events/ee-event-names.js new file mode 100644 index 00000000000000..a9d03789089801 --- /dev/null +++ b/benchmark/events/ee-event-names.js @@ -0,0 +1,38 @@ +'use strict'; +const common = require('../common.js'); +const EventEmitter = require('events').EventEmitter; + +const bench = common.createBenchmark(main, { + n: [1e6], + listeners: [1, 10], + symbols: ['true', 'false'] +}); + +function main(conf) { + const n = conf.n | 0; + const listeners = conf.listeners | 0; + const useSymbols = conf.symbols === 'true'; + + const ee = new EventEmitter(); + + for (var k = 0; k < listeners; k += 1) { + ee.on(`dummy0${k}`, function() {}); + ee.on(`dummy1${k}`, function() {}); + ee.on(`dummy2${k}`, function() {}); + if (useSymbols) + ee.on(Symbol(`dummy${k}`), function() {}); + } + + ee.removeAllListeners('dummy01'); + ee.removeAllListeners('dummy11'); + ee.removeAllListeners('dummy21'); + ee.removeAllListeners('dummy06'); + ee.removeAllListeners('dummy16'); + ee.removeAllListeners('dummy26'); + + bench.start(); + for (var i = 0; i < n; i += 1) { + ee.eventNames(); + } + bench.end(n); +} diff --git a/benchmark/events/ee-listeners-many.js b/benchmark/events/ee-listeners-many.js index 6cb0682b1ca9c3..6173244814d5ad 100644 --- a/benchmark/events/ee-listeners-many.js +++ b/benchmark/events/ee-listeners-many.js @@ -2,7 +2,7 @@ const common = require('../common.js'); const EventEmitter = require('events').EventEmitter; -const bench = common.createBenchmark(main, { n: [5e6] }); +const bench = common.createBenchmark(main, { n: [1e7] }); function main(conf) { const n = conf.n | 0; diff --git a/benchmark/events/ee-listeners.js b/benchmark/events/ee-listeners.js index dff73de0b17fc4..53e24bd750af9f 100644 --- a/benchmark/events/ee-listeners.js +++ b/benchmark/events/ee-listeners.js @@ -2,7 +2,7 @@ const common = require('../common.js'); const EventEmitter = require('events').EventEmitter; -const bench = common.createBenchmark(main, { n: [5e6] }); +const bench = common.createBenchmark(main, { n: [5e7] }); function main(conf) { const n = conf.n | 0; diff --git a/benchmark/events/ee-once.js b/benchmark/events/ee-once.js index d9e87a2b0843af..43617d68814646 100644 --- a/benchmark/events/ee-once.js +++ b/benchmark/events/ee-once.js @@ -2,19 +2,27 @@ const common = require('../common.js'); const EventEmitter = require('events').EventEmitter; -const bench = common.createBenchmark(main, { n: [2e7] }); +const bench = common.createBenchmark(main, { + n: [5e6], + listeners: [1, 5] +}); function main(conf) { - const n = conf.n | 0; + let n = conf.n | 0; + const listeners = conf.listeners | 0; + + if (listeners === 1) + n *= 2; const ee = new EventEmitter(); function listener() {} bench.start(); - for (var i = 0; i < n; i += 1) { + for (var i = 0; i < n; ++i) { const dummy = (i % 2 === 0) ? 'dummy0' : 'dummy1'; - ee.once(dummy, listener); + for (var j = 0; j < listeners; ++j) + ee.once(dummy, listener); ee.emit(dummy); } bench.end(n); diff --git a/doc/api/events.md b/doc/api/events.md index 443137f1705e26..9788001215d344 100644 --- a/doc/api/events.md +++ b/doc/api/events.md @@ -574,6 +574,15 @@ to indicate an unlimited number of listeners. Returns a reference to the `EventEmitter`, so that calls can be chained. +### emitter.rawListeners(eventName) + +- `eventName` {any} + +Returns a copy of the array of listeners for the event named `eventName`, +including any wrappers (such as those created by `.once`). + [`--trace-warnings`]: cli.html#cli_trace_warnings [`EventEmitter.defaultMaxListeners`]: #events_eventemitter_defaultmaxlisteners [`domain`]: domain.html diff --git a/lib/events.js b/lib/events.js index 2a83ab7dc64c7b..a983943c236a32 100644 --- a/lib/events.js +++ b/lib/events.js @@ -36,6 +36,7 @@ EventEmitter.usingDomains = false; EventEmitter.prototype.domain = undefined; EventEmitter.prototype._events = undefined; +EventEmitter.prototype._eventsCount = 0; EventEmitter.prototype._maxListeners = undefined; // By default EventEmitters will print a warning if more than 10 listeners are @@ -81,7 +82,8 @@ EventEmitter.init = function() { this._eventsCount = 0; } - this._maxListeners = this._maxListeners || undefined; + if (!this._maxListeners) + this._maxListeners = undefined; }; // Obviously not all Emitters should be limited to 10. This function allows @@ -156,12 +158,13 @@ EventEmitter.prototype.emit = function emit(type, ...args) { } if (typeof handler === 'function') { - handler.apply(this, args); + Reflect.apply(handler, this, args); } else { - const len = handler.length; - const listeners = arrayClone(handler, len); - for (var i = 0; i < len; ++i) - listeners[i].apply(this, args); + const prevEmitting = handler.emitting; + handler.emitting = true; + for (var i = 0; i < handler.length; ++i) + Reflect.apply(handler[i], this, args); + handler.emitting = prevEmitting; } if (needDomainExit) @@ -171,16 +174,13 @@ EventEmitter.prototype.emit = function emit(type, ...args) { }; function _addListener(target, type, listener, prepend) { - var m; - var events; - var existing; - if (typeof listener !== 'function') { const errors = lazyErrors(); throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'listener', 'Function'); } - events = target._events; + var events = target._events; + var list; if (events === undefined) { events = target._events = Object.create(null); target._eventsCount = 0; @@ -188,49 +188,53 @@ function _addListener(target, type, listener, prepend) { // To avoid recursion in the case that type === "newListener"! Before // adding it to the listeners, first emit "newListener". if (events.newListener !== undefined) { - target.emit('newListener', type, - listener.listener ? listener.listener : listener); + target.emit('newListener', type, listener.listener || listener); // Re-assign `events` because a newListener handler could have caused the // this._events to be assigned to a new object events = target._events; } - existing = events[type]; + list = events[type]; } - if (existing === undefined) { + if (list === undefined) { // Optimize the case of one listener. Don't need the extra array object. - existing = events[type] = listener; + events[type] = listener; ++target._eventsCount; - } else { - if (typeof existing === 'function') { - // Adding the second element, need to change to array. - existing = events[type] = - prepend ? [listener, existing] : [existing, listener]; - // If we've already got an array, just append. - } else if (prepend) { - existing.unshift(listener); - } else { - existing.push(listener); - } + return target; + } - // Check for listener leak - if (!existing.warned) { - m = $getMaxListeners(target); - if (m && m > 0 && existing.length > m) { - existing.warned = true; - // No error code for this since it is a Warning - const w = new Error('Possible EventEmitter memory leak detected. ' + - `${existing.length} ${String(type)} listeners ` + - 'added. Use emitter.setMaxListeners() to ' + - 'increase limit'); - w.name = 'MaxListenersExceededWarning'; - w.emitter = target; - w.type = type; - w.count = existing.length; - process.emitWarning(w); - } + if (typeof list === 'function') { + // Adding the second element, need to change to array. + list = events[type] = prepend ? [listener, list] : [list, listener]; + } else if (list.emitting) { + const { warned } = list; + list = events[type] = + arrayCloneWithElement(list, listener, prepend ? 1 : 0); + if (warned) { + list.warned = true; + return target; } + } else if (prepend) { + list.unshift(listener); + } else { + list.push(listener); + } + + // Check for listener leak + const m = $getMaxListeners(target); + if (m > 0 && list.length > m && !list.warned) { + list.warned = true; + // No error code for this since it is a Warning + const w = new Error('Possible EventEmitter memory leak detected. ' + + `${list.length} ${String(type)} listeners ` + + 'added. Use emitter.setMaxListeners() to ' + + 'increase limit'); + w.name = 'MaxListenersExceededWarning'; + w.emitter = target; + w.type = type; + w.count = list.length; + process.emitWarning(w); } return target; @@ -247,11 +251,11 @@ EventEmitter.prototype.prependListener = return _addListener(this, type, listener, true); }; -function onceWrapper(...args) { +function onceWrapper() { if (!this.fired) { this.target.removeListener(this.type, this.wrapFn); this.fired = true; - this.listener.apply(this.target, args); + Reflect.apply(this.listener, this.target, arguments); } } @@ -286,36 +290,36 @@ EventEmitter.prototype.prependOnceListener = // Emits a 'removeListener' event if and only if the listener was removed. EventEmitter.prototype.removeListener = function removeListener(type, listener) { - var list, events, position, i, originalListener; - if (typeof listener !== 'function') { const errors = lazyErrors(); throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'listener', 'Function'); } - events = this._events; + const events = this._events; if (events === undefined) return this; - list = events[type]; + const list = events[type]; if (list === undefined) return this; if (list === listener || list.listener === listener) { - if (--this._eventsCount === 0) - this._events = Object.create(null); - else { - delete events[type]; - if (events.removeListener) - this.emit('removeListener', type, list.listener || listener); - } - } else if (typeof list !== 'function') { - position = -1; + events[type] = undefined; + --this._eventsCount; + if (events.removeListener !== undefined) + this.emit('removeListener', type, list.listener || listener); + return this; + } - for (i = list.length - 1; i >= 0; i--) { - if (list[i] === listener || list[i].listener === listener) { - originalListener = list[i].listener; + if (typeof list !== 'function') { + let position = -1; + + for (var i = list.length - 1; i >= 0; --i) { + const l = list[i]; + if (l === listener || l.listener === listener) { + if (l.listener) + listener = l.listener; position = i; break; } @@ -324,19 +328,25 @@ EventEmitter.prototype.removeListener = if (position < 0) return this; - if (position === 0) + if (list.length === 2) { + events[type] = list[position ? 0 : 1]; + } else if (list.emitting) { + const { warned } = list; + events[type] = sliceOne(list, position); + if (warned) + events[type].warned = true; + } else if (position === 0) { list.shift(); - else { + } else if (position === list.length - 1) { + list.pop(); + } else { if (spliceOne === undefined) spliceOne = require('internal/util').spliceOne; spliceOne(list, position); } - if (list.length === 1) - events[type] = list[0]; - if (events.removeListener !== undefined) - this.emit('removeListener', type, originalListener || listener); + this.emit('removeListener', type, listener); } return this; @@ -356,26 +366,23 @@ EventEmitter.prototype.removeAllListeners = this._events = Object.create(null); this._eventsCount = 0; } else if (events[type] !== undefined) { - if (--this._eventsCount === 0) - this._events = Object.create(null); - else - delete events[type]; + events[type] = undefined; + --this._eventsCount; } return this; } // emit removeListener for all listeners on all events if (arguments.length === 0) { - var keys = Object.keys(events); + const keys = Reflect.ownKeys(events); var key; for (i = 0; i < keys.length; ++i) { key = keys[i]; - if (key === 'removeListener') continue; + if (key === 'removeListener' || events[key] === undefined) + continue; this.removeAllListeners(key); } this.removeAllListeners('removeListener'); - this._events = Object.create(null); - this._eventsCount = 0; return this; } @@ -385,7 +392,7 @@ EventEmitter.prototype.removeAllListeners = this.removeListener(type, listeners); } else if (listeners !== undefined) { // LIFO order - for (i = listeners.length - 1; i >= 0; i--) { + for (i = listeners.length - 1; i >= 0; --i) { this.removeListener(type, listeners[i]); } } @@ -393,8 +400,8 @@ EventEmitter.prototype.removeAllListeners = return this; }; -EventEmitter.prototype.listeners = function listeners(type) { - const events = this._events; +function _listeners(target, type, unwrap) { + const events = target._events; if (events === undefined) return []; @@ -404,9 +411,17 @@ EventEmitter.prototype.listeners = function listeners(type) { return []; if (typeof evlistener === 'function') - return [evlistener.listener || evlistener]; + return unwrap ? [evlistener.listener || evlistener] : [evlistener]; + + return unwrap ? unwrapListeners(evlistener) : arrayClone(evlistener); +} + +EventEmitter.prototype.listeners = function listeners(type) { + return _listeners(this, type, true); +}; - return unwrapListeners(evlistener); +EventEmitter.prototype.rawListeners = function rawListeners(type) { + return _listeners(this, type, false); }; EventEmitter.listenerCount = function(emitter, type) { @@ -435,16 +450,62 @@ function listenerCount(type) { } EventEmitter.prototype.eventNames = function eventNames() { - return this._eventsCount > 0 ? Reflect.ownKeys(this._events) : []; + const count = this._eventsCount; + if (count === 0) + return []; + + const events = this._events; + const actualEventNames = new Array(count); + var j = 0; + for (var key in events) { + if (events[key] !== undefined) { + actualEventNames[j] = key; + j++; + } + } + + if (j < count) { + // We must have Symbols to fill in + const symbols = Object.getOwnPropertySymbols(events); + for (var i = 0; i < symbols.length; ++i) { + key = symbols[i]; + if (events[key] !== undefined) { + actualEventNames[j] = key; + j++; + } + } + } + + return actualEventNames; }; -function arrayClone(arr, n) { - var copy = new Array(n); - for (var i = 0; i < n; ++i) +function arrayClone(arr) { + const copy = new Array(arr.length); + for (var i = 0; i < arr.length; ++i) copy[i] = arr[i]; return copy; } +function arrayCloneWithElement(arr, element, prepend) { + const len = arr.length; + const copy = new Array(len + 1); + for (var i = 0 + prepend; i < len + prepend; ++i) + copy[i] = arr[i - prepend]; + copy[prepend ? 0 : len] = element; + return copy; +} + +function sliceOne(arr, index) { + const len = arr.length - 1; + const copy = new Array(len); + for (var i = 0, offset = 0; i < len; ++i) { + if (i === index) + offset = 1; + copy[i] = arr[i + offset]; + } + return copy; +} + function unwrapListeners(arr) { const ret = new Array(arr.length); for (var i = 0; i < ret.length; ++i) { diff --git a/lib/internal/bootstrap_node.js b/lib/internal/bootstrap_node.js index c75578f7692068..3f43c23e8d7c0b 100644 --- a/lib/internal/bootstrap_node.js +++ b/lib/internal/bootstrap_node.js @@ -13,7 +13,6 @@ function startup() { const EventEmitter = NativeModule.require('events'); - process._eventsCount = 0; const origProcProto = Object.getPrototypeOf(process); Object.setPrototypeOf(origProcProto, EventEmitter.prototype); diff --git a/lib/vm.js b/lib/vm.js index b34f10dbee5ff8..0daa294da68642 100644 --- a/lib/vm.js +++ b/lib/vm.js @@ -43,7 +43,7 @@ const realRunInThisContext = Script.prototype.runInThisContext; const realRunInContext = Script.prototype.runInContext; Script.prototype.runInThisContext = function(options) { - if (options && options.breakOnSigint && process._events.SIGINT) { + if (options && options.breakOnSigint && process.listenerCount('SIGINT')) { return sigintHandlersWrap(realRunInThisContext, this, [options]); } else { return realRunInThisContext.call(this, options); @@ -51,7 +51,7 @@ Script.prototype.runInThisContext = function(options) { }; Script.prototype.runInContext = function(contextifiedSandbox, options) { - if (options && options.breakOnSigint && process._events.SIGINT) { + if (options && options.breakOnSigint && process.listenerCount('SIGINT')) { return sigintHandlersWrap(realRunInContext, this, [contextifiedSandbox, options]); } else { @@ -82,14 +82,7 @@ function createScript(code, options) { // Remove all SIGINT listeners and re-attach them after the wrapped function // has executed, so that caught SIGINT are handled by the listeners again. function sigintHandlersWrap(fn, thisArg, argsArray) { - // Using the internal list here to make sure `.once()` wrappers are used, - // not the original ones. - let sigintListeners = process._events.SIGINT; - - if (Array.isArray(sigintListeners)) - sigintListeners = sigintListeners.slice(); - else - sigintListeners = [sigintListeners]; + const sigintListeners = process.rawListeners('SIGINT'); process.removeAllListeners('SIGINT'); diff --git a/src/env.h b/src/env.h index 083c09a8134efb..dcb9f57646182e 100644 --- a/src/env.h +++ b/src/env.h @@ -145,7 +145,6 @@ class ModuleWrap; V(env_pairs_string, "envPairs") \ V(errno_string, "errno") \ V(error_string, "error") \ - V(events_string, "_events") \ V(exiting_string, "_exiting") \ V(exit_code_string, "exitCode") \ V(exit_string, "exit") \ diff --git a/src/node.cc b/src/node.cc index c35fd612b48149..c06335f8bf7cdc 100644 --- a/src/node.cc +++ b/src/node.cc @@ -3292,12 +3292,6 @@ void SetupProcessObject(Environment* env, env->SetMethod(process, "_setupNextTick", SetupNextTick); env->SetMethod(process, "_setupPromises", SetupPromises); env->SetMethod(process, "_setupDomainUse", SetupDomainUse); - - // pre-set _events object for faster emit checks - Local events_obj = Object::New(env->isolate()); - CHECK(events_obj->SetPrototype(env->context(), - Null(env->isolate())).FromJust()); - process->Set(env->events_string(), events_obj); } diff --git a/test/parallel/test-event-emitter-listeners.js b/test/parallel/test-event-emitter-listeners.js index 0736e3103e9e9e..08a2fa91bf2ab5 100644 --- a/test/parallel/test-event-emitter-listeners.js +++ b/test/parallel/test-event-emitter-listeners.js @@ -82,3 +82,14 @@ function listener2() {} const s = new TestStream(); assert.deepStrictEqual(s.listeners('foo'), []); } + +{ + const ee = new events.EventEmitter(); + ee.on('foo', listener); + ee.once('foo', listener); + const wrappedListeners = ee.rawListeners('foo'); + assert.strictEqual(wrappedListeners.length, 2); + assert.strictEqual(wrappedListeners[0], listener); + assert.notStrictEqual(wrappedListeners[1], listener); + assert.strictEqual(wrappedListeners[1].listener, listener); +} diff --git a/test/parallel/test-process-emit.js b/test/parallel/test-process-emit.js index 8c2ad675cf8c62..f7dc5ef615ce1a 100644 --- a/test/parallel/test-process-emit.js +++ b/test/parallel/test-process-emit.js @@ -19,4 +19,5 @@ process.emit('normal', 'normalData'); process.emit(sym, 'symbolData'); process.emit('SIGPIPE', 'signalData'); -assert.strictEqual(Number.isNaN(process._eventsCount), false); +assert.strictEqual(process._eventsCount, + Reflect.ownKeys(process._events).length); diff --git a/test/parallel/test-readline-interface.js b/test/parallel/test-readline-interface.js index fd622f8cabc564..59cf95a8efb251 100644 --- a/test/parallel/test-readline-interface.js +++ b/test/parallel/test-readline-interface.js @@ -39,7 +39,8 @@ class FakeInput extends EventEmitter { function isWarned(emitter) { for (const name in emitter) { const listeners = emitter[name]; - if (listeners.warned) return true; + if (listeners && listeners.warned) + return true; } return false; }