diff --git a/benchmark/events/ee-add-remove.js b/benchmark/events/ee-add-remove.js index 54e680f74ae3e1..70b9400ecaa30a 100644 --- a/benchmark/events/ee-add-remove.js +++ b/benchmark/events/ee-add-remove.js @@ -2,16 +2,23 @@ const common = require('../common.js'); const events = require('events'); -const bench = common.createBenchmark(main, { n: [1e6] }); +const bench = common.createBenchmark(main, { + n: [5e6], + staleEventsCount: [0, 5], + listenerCount: [1, 5], +}); -function main({ n }) { +function main({ n, staleEventsCount, listenerCount }) { const ee = new events.EventEmitter(); const listeners = []; var k; - for (k = 0; k < 10; k += 1) + for (k = 0; k < listenerCount; k += 1) listeners.push(function() {}); + for (k = 0; k < staleEventsCount; 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..09194e63215e22 --- /dev/null +++ b/benchmark/events/ee-emit-multi.js @@ -0,0 +1,28 @@ +'use strict'; +const common = require('../common.js'); +const EventEmitter = require('events').EventEmitter; + +const bench = common.createBenchmark(main, { + n: [2e7], + listenerCount: [1, 5, 10], +}); + +function main({ n, listenerCount }) { + const ee = new EventEmitter(); + + for (var k = 0; k < listenerCount; 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 686ed10d3ecbfd..a976f47a3077d7 100644 --- a/benchmark/events/ee-emit.js +++ b/benchmark/events/ee-emit.js @@ -5,13 +5,13 @@ const EventEmitter = require('events').EventEmitter; const bench = common.createBenchmark(main, { n: [2e6], argc: [0, 2, 4, 10], - listeners: [1, 5, 10], + listenerCount: [1, 5, 10], }); -function main({ n, argc, listeners }) { +function main({ n, argc, listenerCount }) { const ee = new EventEmitter(); - for (var k = 0; k < listeners; k += 1) + for (var k = 0; k < listenerCount; k += 1) ee.on('dummy', function() {}); var i; diff --git a/benchmark/events/ee-event-names.js b/benchmark/events/ee-event-names.js new file mode 100644 index 00000000000000..0f262c6a471aec --- /dev/null +++ b/benchmark/events/ee-event-names.js @@ -0,0 +1,28 @@ +'use strict'; +const common = require('../common.js'); +const EventEmitter = require('events').EventEmitter; + +const bench = common.createBenchmark(main, { n: [1e6] }); + +function main({ n }) { + const ee = new EventEmitter(); + + for (var k = 0; k < 9; k += 1) { + ee.on(`dummy0${k}`, function() {}); + ee.on(`dummy1${k}`, function() {}); + ee.on(`dummy2${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 9a1562eb2c005c..4542619cc9dc36 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({ n }) { const ee = new EventEmitter(); diff --git a/benchmark/events/ee-once.js b/benchmark/events/ee-once.js index e1a09fb4b71167..6b2d0c97a73ed5 100644 --- a/benchmark/events/ee-once.js +++ b/benchmark/events/ee-once.js @@ -2,17 +2,22 @@ const common = require('../common.js'); const EventEmitter = require('events').EventEmitter; -const bench = common.createBenchmark(main, { n: [2e7] }); +const bench = common.createBenchmark(main, { + n: [5e6], + listenerCount: [1, 5, 10], +}); -function main({ n }) { + +function main({ n, listenerCount }) { 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 < listenerCount; ++j) + ee.once(dummy, listener); ee.emit(dummy); } bench.end(n); diff --git a/deps/v8/src/builtins/builtins-collections-gen.cc b/deps/v8/src/builtins/builtins-collections-gen.cc index 5c3f26374689dc..6b706e03622608 100644 --- a/deps/v8/src/builtins/builtins-collections-gen.cc +++ b/deps/v8/src/builtins/builtins-collections-gen.cc @@ -1474,14 +1474,17 @@ TF_BUILTIN(MapPrototypeDelete, CollectionsBuiltinsAssembler) { LoadFixedArrayElement(table, OrderedHashMap::kNumberOfBucketsIndex); // If there fewer elements than #buckets / 2, shrink the table. - Label shrink(this); - GotoIf(SmiLessThan(SmiAdd(number_of_elements, number_of_elements), - number_of_buckets), - &shrink); - Return(TrueConstant()); + Label dont_shrink(this); + GotoIf(SmiGreaterThanOrEqual(SmiAdd(number_of_elements, number_of_elements), + number_of_buckets), + &dont_shrink); + // If #buckets is less than 8 then don't shrink the table + GotoIf(SmiLessThan(number_of_buckets, SmiConstant(8)), + &dont_shrink); - BIND(&shrink); CallRuntime(Runtime::kMapShrink, context, receiver); + + BIND(&dont_shrink); Return(TrueConstant()); } diff --git a/lib/events.js b/lib/events.js index ff1648d6aa13e7..91d114bbaa091c 100644 --- a/lib/events.js +++ b/lib/events.js @@ -21,6 +21,9 @@ 'use strict'; +const kEvents = Symbol('events'); +const kEventsProxy = Symbol('events-proxy'); + var spliceOne; function EventEmitter() { @@ -33,7 +36,8 @@ EventEmitter.EventEmitter = EventEmitter; EventEmitter.usingDomains = false; -EventEmitter.prototype._events = undefined; +EventEmitter.prototype[kEvents] = undefined; +EventEmitter.prototype[kEventsProxy] = undefined; EventEmitter.prototype._eventsCount = 0; EventEmitter.prototype._maxListeners = undefined; @@ -64,11 +68,80 @@ Object.defineProperty(EventEmitter, 'defaultMaxListeners', { } }); +const proxyEventsHandler = { + get(emitter, prop) { + const events = emitter[kEvents]; + if (events === undefined) + return undefined; + return events.get(prop); + }, + set(emitter, prop, value) { + let events = emitter[kEvents]; + if (events === undefined) { + events = emitter[kEvents] = new Map(); + emitter._eventsCount = 0; + } + events.set(prop, value); + return true; + }, + has(emitter, prop) { + const events = emitter[kEvents]; + return events !== undefined && events.has(prop); + }, + getPrototypeOf() { + return null; + }, + deleteProperty(emitter, prop) { + const events = emitter[kEvents]; + if (events !== undefined) + events.delete(prop); + return true; + }, + ownKeys(emitter) { + return emitter.eventNames(); + }, + defineProperty(emitter, prop, descriptor) { + if (!descriptor.writable || !descriptor.configurable) + return false; + proxyEventsHandler.set(emitter, prop, descriptor.value); + return true; + }, + getOwnPropertyDescriptor(emitter, prop) { + const value = proxyEventsHandler.get(emitter, prop); + if (value === undefined) + return undefined; + return { + value, + writable: true, + enumerable: true, + configurable: true, + }; + }, +}; + +Object.defineProperty(EventEmitter.prototype, '_events', { + enumerable: true, + get: function() { + let proxy = this[kEventsProxy]; + if (proxy === undefined && this[kEvents] !== undefined) + proxy = new Proxy(this, proxyEventsHandler); + return proxy; + }, + set: function(value) { + if (this[kEvents] === undefined) + this.init(); + if (!value || Array.isArray(value) && value.length === 0) { + this[kEvents] = new Map(); + this._eventsCount = 0; + } + } +}); + EventEmitter.init = function() { - if (this._events === undefined || - this._events === Object.getPrototypeOf(this)._events) { - this._events = Object.create(null); + if (this[kEvents] === undefined || + this[kEvents] === Object.getPrototypeOf(this)[kEvents]) { + this[kEvents] = new Map(); this._eventsCount = 0; } @@ -140,9 +213,9 @@ function enhanceStackTrace(err, own) { EventEmitter.prototype.emit = function emit(type, ...args) { let doError = (type === 'error'); - const events = this._events; + const events = this[kEvents]; if (events !== undefined) - doError = (doError && events.error === undefined); + doError = (doError && !events.has('error')); else if (!doError) return false; @@ -173,7 +246,7 @@ EventEmitter.prototype.emit = function emit(type, ...args) { throw err; // Unhandled 'error' event } - const handler = events[type]; + const handler = events.get(type); if (handler === undefined) return false; @@ -200,38 +273,38 @@ function _addListener(target, type, listener, prepend) { throw new errors.ERR_INVALID_ARG_TYPE('listener', 'Function', listener); } - events = target._events; - if (events === undefined) { - events = target._events = Object.create(null); - target._eventsCount = 0; - } else { + events = target[kEvents]; + if (events !== undefined) { // To avoid recursion in the case that type === "newListener"! Before // adding it to the listeners, first emit "newListener". - if (events.newListener !== undefined) { + if (events.has('newListener')) { target.emit('newListener', type, listener.listener ? 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; + // this[kEvents] to be assigned to a new object + events = target[kEvents]; } - existing = events[type]; + existing = events.get(type); + } else { + events = target[kEvents] = new Map(); + target._eventsCount = 0; } if (existing === undefined) { // Optimize the case of one listener. Don't need the extra array object. - existing = events[type] = listener; + events.set(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]; + existing = prepend ? [listener, existing] : [existing, listener]; + events.set(type, existing); // If we've already got an array, just append. - } else if (prepend) { - existing.unshift(listener); - } else { + } else if (!prepend) { existing.push(listener); + } else { + existing.unshift(listener); } // Check for listener leak @@ -311,22 +384,19 @@ EventEmitter.prototype.removeListener = throw new errors.ERR_INVALID_ARG_TYPE('listener', 'Function', listener); } - events = this._events; + events = this[kEvents]; if (events === undefined) return this; - list = events[type]; + list = events.get(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); - } + --this._eventsCount; + events.delete(type); + if (events.has('removeListener')) + this.emit('removeListener', type, list.listener || listener); } else if (typeof list !== 'function') { position = -1; @@ -343,6 +413,8 @@ EventEmitter.prototype.removeListener = if (position === 0) list.shift(); + else if (position === list.length - 1) + list.length -= 1; else { if (spliceOne === undefined) spliceOne = require('internal/util').spliceOne; @@ -350,9 +422,9 @@ EventEmitter.prototype.removeListener = } if (list.length === 1) - events[type] = list[0]; + events.set(type, list[0]); - if (events.removeListener !== undefined) + if (events.has('removeListener')) this.emit('removeListener', type, originalListener || listener); } @@ -365,40 +437,42 @@ EventEmitter.prototype.removeAllListeners = function removeAllListeners(type) { var listeners, events, i; - events = this._events; + events = this[kEvents]; if (events === undefined) return this; // not listening for removeListener, no need to emit - if (events.removeListener === undefined) { + if (!events.has('removeListener')) { if (arguments.length === 0) { - this._events = Object.create(null); + this[kEvents] = new Map(); this._eventsCount = 0; - } else if (events[type] !== undefined) { - if (--this._eventsCount === 0) - this._events = Object.create(null); - else - delete events[type]; + } else if (events.delete(type)) { + --this._eventsCount; } return this; } // emit removeListener for all listeners on all events if (arguments.length === 0) { - var keys = Object.keys(events); + const len = events.size; + var keys = new Array(len); + i = 0; + for (var value of events.keys()) + keys[i++] = value; + var key; - for (i = 0; i < keys.length; ++i) { + for (i = 0; i < len; ++i) { key = keys[i]; if (key === 'removeListener') continue; this.removeAllListeners(key); } this.removeAllListeners('removeListener'); - this._events = Object.create(null); + this[kEvents] = new Map(); this._eventsCount = 0; return this; } - listeners = events[type]; + listeners = events.get(type); if (typeof listeners === 'function') { this.removeListener(type, listeners); @@ -413,12 +487,12 @@ EventEmitter.prototype.removeAllListeners = }; function _listeners(target, type, unwrap) { - const events = target._events; + const events = target[kEvents]; if (events === undefined) return []; - const evlistener = events[type]; + const evlistener = events.get(type); if (evlistener === undefined) return []; @@ -447,23 +521,32 @@ EventEmitter.listenerCount = function(emitter, type) { EventEmitter.prototype.listenerCount = listenerCount; function listenerCount(type) { - const events = this._events; + const events = this[kEvents]; if (events !== undefined) { - const evlistener = events[type]; + const evlistener = events.get(type); - if (typeof evlistener === 'function') { + if (typeof evlistener === 'function') return 1; - } else if (evlistener !== undefined) { + else if (evlistener !== undefined) return evlistener.length; - } } return 0; } EventEmitter.prototype.eventNames = function eventNames() { - return this._eventsCount > 0 ? Reflect.ownKeys(this._events) : []; + if (this._eventsCount === 0) + return []; + + const map = this[kEvents]; + const len = map.size; + const names = new Array(len); + var i = 0; + for (var key of map.keys()) { + names[i++] = key; + } + return names; }; function arrayClone(arr, n) { diff --git a/test/parallel/test-benchmark-events.js b/test/parallel/test-benchmark-events.js index 06be60a84abb5b..98c80566c7b3ec 100644 --- a/test/parallel/test-benchmark-events.js +++ b/test/parallel/test-benchmark-events.js @@ -5,5 +5,5 @@ require('../common'); const runBenchmark = require('../common/benchmark'); runBenchmark('events', - ['argc=0', 'listeners=1', 'n=1'], + ['argc=0', 'staleEventsCount=0', 'listenerCount=1', 'n=1'], { NODEJS_BENCHMARK_ZERO_ALLOWED: 1 });