From f62d869c60d9e0716cf77c7a5327910c3f4ad866 Mon Sep 17 00:00:00 2001 From: Raz Luvaton <16746759+rluvaton@users.noreply.github.com> Date: Sun, 28 Apr 2024 14:18:24 +0300 Subject: [PATCH 01/69] events: add fast and slow path --- lib/events.js | 1210 +++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 1023 insertions(+), 187 deletions(-) diff --git a/lib/events.js b/lib/events.js index 4d8b5bbb1e6f3f..7fbe8e2ecdd69d 100644 --- a/lib/events.js +++ b/lib/events.js @@ -94,6 +94,12 @@ const kMaxEventTargetListenersWarned = Symbol('events.maxEventTargetListenersWarned'); const kWatermarkData = SymbolFor('nodejs.watermarkData'); +const kImpl = Symbol('kImpl'); +const kCaptureValue = Symbol('kCaptureValue'); +const kIsFastPath = Symbol('kIsFastPath'); +const kSwitchToSlowPath = Symbol('kSwitchToSlowPath'); +const kContinueAddEventListenerInSlowMode = Symbol('kContinueAddEventListenerInSlowMode'); + let EventEmitterAsyncResource; // The EventEmitterAsyncResource has to be initialized lazily because event.js // is loaded so early in the bootstrap process, before async_hooks is available. @@ -256,16 +262,95 @@ ObjectDefineProperty(EventEmitter, 'EventEmitterAsyncResource', { EventEmitter.errorMonitor = kErrorMonitor; -// The default for captureRejections is false -ObjectDefineProperty(EventEmitter.prototype, kCapture, { - __proto__: null, - value: false, - writable: true, - enumerable: false, +function isEventUnsupportedForFastPath(type) { + return type === 'newListener' || type === 'removeListener' || type === kErrorMonitor; +} + +ObjectDefineProperties(EventEmitter.prototype, { + [kImpl]: { + __proto__: null, + value: undefined, + enumerable: false, + configurable: false, + writable: true, + }, + [kIsFastPath]: { + __proto__: null, + value: true, + enumerable: false, + configurable: false, + writable: true, + }, + [kCaptureValue]: { + __proto__: null, + value: false, + enumerable: false, + configurable: false, + writable: true, + }, + + // The default for captureRejections is false + [kCapture]: { + __proto__: null, + get() { + return this[kCaptureValue]; + }, + set(value) { + this[kCaptureValue] = value; + + if (value) { + this[kSwitchToSlowPath](); + } + }, + enumerable: false, + }, + _events: { + __proto__: null, + enumerable: true, + + get: function() { + // TODO - remove optional chaining + return this[kImpl]?._events; + }, + set: function(arg) { + // TODO - remove optional chaining + if(!this[kImpl]) { + // TODO(rluvaton): find a better way and also support array with length 1? + // TODO(rluvaton): if have newListener and removeListener events, switch to slow path + + const shouldBeFastPath = arg === undefined || Object.entries(arg).every(([key, value]) => !isEventUnsupportedForFastPath(key) && typeof value === 'undefined' || typeof value === 'function'); + + this[kImpl] = shouldBeFastPath ? new FastEventEmitter(this) : new SlowEventEmitter(this); + this[kIsFastPath] = shouldBeFastPath; + // return; + } + // TODO - might need to change to slow path + // TODO - deprecate this + this[kImpl]._events = arg; + }, + }, + _eventsCount: { + __proto__: null, + + enumerable: true, + + get: function() { + return this[kImpl]?._eventsCount; + }, + set: function(arg) { + // TODO - remove optional chaining + if(!this[kImpl]) { + // TODO - don't use slow by default here + this[kImpl] = new SlowEventEmitter(this); + this[kIsFastPath] = false; + // return; + } + // TODO - deprecate this + this[kImpl]._eventsCount = arg; + }, + }, }); -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 @@ -342,6 +427,27 @@ EventEmitter.setMaxListeners = // re-definitions, such as the one in the Domain module (lib/domain.js). EventEmitter.init = function(opts) { + if (opts?.captureRejections) { + validateBoolean(opts.captureRejections, 'options.captureRejections'); + this[kCapture] = Boolean(opts.captureRejections); + } else { + // Assigning the kCapture property directly saves an expensive + // prototype lookup in a very sensitive hot path. + this[kCapture] = EventEmitter.prototype[kCapture]; + } + + if(this[kCapture]) { + if(this[kIsFastPath]) { + this[kSwitchToSlowPath](); + } else if(!this[kImpl]) { + this[kImpl] = new SlowEventEmitter(this); + this[kIsFastPath] = false; + } + } + this[kImpl] ??= new FastEventEmitter(this, opts); + this[kIsFastPath] ??= true; + + // TODO - update this if (this._events === undefined || this._events === ObjectGetPrototypeOf(this)._events) { this._events = { __proto__: null }; @@ -352,17 +458,21 @@ EventEmitter.init = function(opts) { } this._maxListeners = this._maxListeners || undefined; +}; - - if (opts?.captureRejections) { - validateBoolean(opts.captureRejections, 'options.captureRejections'); - this[kCapture] = Boolean(opts.captureRejections); - } else { - // Assigning the kCapture property directly saves an expensive - // prototype lookup in a very sensitive hot path. - this[kCapture] = EventEmitter.prototype[kCapture]; +EventEmitter.prototype[kSwitchToSlowPath] = function() { + if(!this[kIsFastPath]) { + return; } -}; + + this[kIsFastPath] = false; + this[kImpl] = SlowEventEmitter.fromFastEventEmitter(this[kImpl]); +} + +EventEmitter.prototype[kContinueAddEventListenerInSlowMode] = function(type, listener, prepend) { + // TODO - this is in case while adding listener another listener added in the middle using the newListener event + // TODO - maybe for simplicity we can just switch to slow mode if have newListener event +} function addCatch(that, promise, type, args) { if (!that[kCapture]) { @@ -465,84 +575,7 @@ function enhanceStackTrace(err, own) { * @returns {boolean} */ EventEmitter.prototype.emit = function emit(type, ...args) { - let doError = (type === 'error'); - - const events = this._events; - if (events !== undefined) { - if (doError && events[kErrorMonitor] !== undefined) - this.emit(kErrorMonitor, ...args); - doError = (doError && events.error === undefined); - } else if (!doError) - return false; - - // If there is no 'error' event listener then throw. - if (doError) { - let er; - if (args.length > 0) - er = args[0]; - if (er instanceof Error) { - try { - const capture = {}; - ErrorCaptureStackTrace(capture, EventEmitter.prototype.emit); - ObjectDefineProperty(er, kEnhanceStackBeforeInspector, { - __proto__: null, - value: FunctionPrototypeBind(enhanceStackTrace, this, er, capture), - configurable: true, - }); - } catch { - // Continue regardless of error. - } - - // Note: The comments on the `throw` lines are intentional, they show - // up in Node's output if this results in an unhandled exception. - throw er; // Unhandled 'error' event - } - - let stringifiedEr; - try { - stringifiedEr = inspect(er); - } catch { - stringifiedEr = er; - } - - // At least give some kind of context to the user - const err = new ERR_UNHANDLED_ERROR(stringifiedEr); - err.context = er; - throw err; // Unhandled 'error' event - } - - const handler = events[type]; - - if (handler === undefined) - return false; - - if (typeof handler === 'function') { - const result = handler.apply(this, args); - - // We check if result is undefined first because that - // is the most common case so we do not pay any perf - // penalty - if (result !== undefined && result !== null) { - addCatch(this, result, type, args); - } - } else { - const len = handler.length; - const listeners = arrayClone(handler); - for (let i = 0; i < len; ++i) { - const result = listeners[i].apply(this, args); - - // We check if result is undefined first because that - // is the most common case so we do not pay any perf - // penalty. - // This code is duplicated because extracting it away - // would make it non-inlineable. - if (result !== undefined && result !== null) { - addCatch(this, result, type, args); - } - } - } - - return true; + return this[kImpl].emit(type, ...args); }; function _addListener(target, type, listener, prepend) { @@ -609,7 +642,14 @@ function _addListener(target, type, listener, prepend) { * @returns {EventEmitter} */ EventEmitter.prototype.addListener = function addListener(type, listener) { - return _addListener(this, type, listener, false); + checkListener(listener); + + // TODO - don't handle newListener and removeListener events in fast path for complexity reasons + if(this[kIsFastPath] && (isEventUnsupportedForFastPath(type) || this[kImpl].isListenerAlreadyExists(type))) { + this[kSwitchToSlowPath](); + } + + return this[kImpl].addListener(type, listener); }; EventEmitter.prototype.on = EventEmitter.prototype.addListener; @@ -623,7 +663,13 @@ EventEmitter.prototype.on = EventEmitter.prototype.addListener; */ EventEmitter.prototype.prependListener = function prependListener(type, listener) { - return _addListener(this, type, listener, true); + checkListener(listener); + + if(this[kIsFastPath] && (type === 'newListener' || type === 'removeListener' || this[kImpl].isListenerAlreadyExists(type))) { + this[kSwitchToSlowPath](); + } + + return this[kImpl].addListener(type, listener, true); }; function onceWrapper() { @@ -682,53 +728,7 @@ EventEmitter.prototype.removeListener = function removeListener(type, listener) { checkListener(listener); - const events = this._events; - if (events === undefined) - return this; - - const list = events[type]; - if (list === undefined) - return this; - - if (list === listener || list.listener === listener) { - this._eventsCount -= 1; - - if (this[kShapeMode]) { - events[type] = undefined; - } else if (this._eventsCount === 0) { - this._events = { __proto__: null }; - } else { - delete events[type]; - if (events.removeListener) - this.emit('removeListener', type, list.listener || listener); - } - } else if (typeof list !== 'function') { - let position = -1; - - for (let i = list.length - 1; i >= 0; i--) { - if (list[i] === listener || list[i].listener === listener) { - position = i; - break; - } - } - - if (position < 0) - return this; - - if (position === 0) - list.shift(); - 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, listener); - } + this[kImpl].removeListener(type, listener); return this; }; @@ -744,49 +744,7 @@ EventEmitter.prototype.off = EventEmitter.prototype.removeListener; */ EventEmitter.prototype.removeAllListeners = function removeAllListeners(type) { - const events = this._events; - if (events === undefined) - return this; - - // Not listening for removeListener, no need to emit - if (events.removeListener === undefined) { - if (arguments.length === 0) { - this._events = { __proto__: null }; - this._eventsCount = 0; - } else if (events[type] !== undefined) { - if (--this._eventsCount === 0) - this._events = { __proto__: null }; - else - delete events[type]; - } - this[kShapeMode] = false; - return this; - } - - // Emit removeListener for all listeners on all events - if (arguments.length === 0) { - for (const key of ReflectOwnKeys(events)) { - if (key === 'removeListener') continue; - this.removeAllListeners(key); - } - this.removeAllListeners('removeListener'); - this._events = { __proto__: null }; - this._eventsCount = 0; - this[kShapeMode] = false; - return this; - } - - const listeners = events[type]; - - if (typeof listeners === 'function') { - this.removeListener(type, listeners); - } else if (listeners !== undefined) { - // LIFO order - for (let i = listeners.length - 1; i >= 0; i--) { - this.removeListener(type, listeners[i]); - } - } - + this[kImpl].removeAllListeners(type); return this; }; @@ -1225,3 +1183,881 @@ function listenersController() { }, }; } + +// ---------- FAST + +// TODO - should have the same API as slow_event_emitter + +// TODO - not supporting +// 1. kCapture - false by default +// 2. _maxListeners (but still need to save the data), should be saved on the parent +// 3. kErrorMonitor - undefined by default +// TODO - add comment for what this is optimized for +class FastEventEmitter { + // TODO - have a way to support passing _events + // TODO - add comment on how events are stored + _events = undefined; + _eventsCount + // TODO - convert to symbol and rename + eventEmitterTranslationLayer = undefined; + stale = false; + + // TODO - use opts + constructor(eventEmitterTranslationLayer, opts) { + this.eventEmitterTranslationLayer = eventEmitterTranslationLayer; + // TODO - check this: + // If you're updating this function definition, please also update any + // re-definitions, such as the one in the Domain module (lib/domain.js). + if (this._events === undefined || + // TODO - this is not correct + // TODO - change the this here? + this._events === ObjectGetPrototypeOf(this)._events) { + // TODO - removed the __proto__ assignment + this._events = { }; + this._eventsCount = 0; + this[kShapeMode] = false; + } else { + this[kShapeMode] = true; + } + } + + /** + * Synchronously calls each of the listeners registered + * for the event. + * @param {string | symbol} type + * @param {...any} [args] + * @returns {boolean} + */ + emit(type, ...args) { + const events = this._events; + if(type === 'error' && events?.error === undefined) { + throwErrorOnMissingErrorHandler.apply(this, args); + // TODO - add assertion that should not reach here; + return false; + } + + // TODO(rluvaton): will it be faster to add check if events is undefined instead? + const handler = events?.[type]; + + if(handler === undefined) { + return false; + } + + // TODO - change + handler.apply(this.eventEmitterTranslationLayer, args); + + return true; + }; + + // TODO - should switch to slow mode + isListenerAlreadyExists(type) { + return this._events?.[type] !== undefined; + } + + /** + * Adds a listener to the event emitter. + * @param {string | symbol} type + * @param {Function} listener + * @param {boolean} prepend not used here as we are in fast mode and only have single listener + * @returns {EventEmitter} + */ + addListener(type, listener, prepend = undefined) { + // TODO - add validation before getting here + // TODO - if got here that can add without switching to slow mode + let events; + + events = this._events; + // TODO - simplify this + if (events === undefined) { + events = this._events = { + // TODO - change this? + __proto__: null + }; + this._eventsCount = 0; + } else { + // TODO(rluvaton): support addListener and removeListener events + // // To avoid recursion in the case that type === "newListener"! Before + // // adding it to the listeners, first emit "newListener". + // if (events.newListener !== undefined) { + // // TODO(rluvaton): use apply to pass the parent eventEmitter + // this.emit('newListener', type, + // listener.listener ?? listener); + // + // + // // TODO - here we could already have aq listener to the same value so need to switch to slow mode + // + // if(this.stale) { + // // TODO - continue in the slow mode + // this.eventEmitterTranslationLayer[kContinueInSlowMode](type, listener, prepend); + // return; + // } + // + // if(this._events[type]) { + // // TODO(rluvaton): try to find a better way + // this.stale = true; + // this.eventEmitterTranslationLayer[kContinueInSlowMode](type, listener, prepend); + // + // // TODO(rluvaton): continue in the slow mode + // return; + // } + // + // // Re-assign `events` because a newListener handler could have caused the + // // this._events to be assigned to a new object + // // TODO(rluvaton): change this + // events = this._events; + // } + } + + // Optimize the case of one listener. Don't need the extra array object. + events[type] = listener; + ++this._eventsCount; + + } + + /** + * Removes the specified `listener`. + * @param {string | symbol} type + * @param {Function} listener + * @returns {EventEmitter} + */ + removeListener(type, listener) { + // TODO - add validation before getting here + // TODO - parent function should return `this` + + const events = this._events; + if (events === undefined) + return undefined; + + const list = events[type]; + if (list === undefined || (list !== listener && list.listener !== listener)) + return undefined; + + this._eventsCount -= 1; + + if (this[kShapeMode]) { + events[type] = undefined; + } else if (this._eventsCount === 0) { + // TODO - keep this? + this._events = {__proto__: null}; + } else { + delete events[type]; + // TODO(rluvaton): support addListener and removeListener events + // if (events.removeListener) + // // TODO(rluvaton): use apply to pass the parent eventEmitter + // this.emit('removeListener', type, list.listener || listener); + } + }; + + /** + * Removes all listeners from the event emitter. (Only + * removes listeners for a specific event name if specified + * as `type`). + * @param {string | symbol} [type] + * @returns {EventEmitter} + */ + removeAllListeners(type) { + // TODO - parent function should return `this` + const events = this._events; + if (events === undefined) + return undefined; + + // Not listening for removeListener, no need to emit + if (events.removeListener === undefined) { + if (arguments.length === 0) { + this._events = { __proto__: null }; + this._eventsCount = 0; + } else if (events[type] !== undefined) { + if (--this._eventsCount === 0) + this._events = { __proto__: null }; + else + delete events[type]; + } + this[kShapeMode] = false; + + return undefined; + } + + // Emit removeListener for all listeners on all events + if (arguments.length === 0) { + for (const key of ReflectOwnKeys(events)) { + if (key === 'removeListener') continue; + this.removeAllListeners(key); + } + this.removeAllListeners('removeListener'); + this._events = { __proto__: null }; + this._eventsCount = 0; + this[kShapeMode] = false; + + return undefined; + } + + const listeners = events[type]; + + if (typeof listeners === 'function') { + this.removeListener(type, listeners); + } + }; +} + +// --------------- SLOW + + +// TODO - should have the same API as slow_event_emitter + +// TODO - not supporting +// 2. _maxListeners (but still need to save the data), should be saved on the parent +// 3. kErrorMonitor - undefined by default +// TODO - add comment for what this is optimized for +class SlowEventEmitter { + // TODO - have a way to support passing _events + // TODO - add comment on how events are stored + _events = undefined; + _eventsCount = 0 + // TODO - convert to symbol and rename + eventEmitterTranslationLayer = undefined; + + /** + * + * @param {FastEventEmitter} fastEventEmitter + */ + static fromFastEventEmitter(fastEventEmitter) { + const eventEmitter = new SlowEventEmitter(fastEventEmitter.eventEmitterTranslationLayer); + + // TODO - add missing + eventEmitter._events = fastEventEmitter._events; + eventEmitter._eventsCount = fastEventEmitter._eventsCount; + eventEmitter[kShapeMode] = fastEventEmitter[kShapeMode]; + + return eventEmitter; + } + + // TODO - use opts + constructor(eventEmitterTranslationLayer, opts) { + this.eventEmitterTranslationLayer = eventEmitterTranslationLayer; + // TODO - + } + + /** + * Synchronously calls each of the listeners registered + * for the event. + * @param {string | symbol} type + * @param {...any} [args] + * @returns {boolean} + */ + emit(type, ...args) { + let doError = (type === 'error'); + + const events = this._events; + if (events !== undefined) { + if (doError && events[kErrorMonitor] !== undefined) + this.emit(kErrorMonitor, ...args); + doError = (doError && events.error === undefined); + } else if (!doError) + return false; + + // If there is no 'error' event listener then throw. + if (doError) { + throwErrorOnMissingErrorHandler(args); + } + + const handler = events[type]; + + if (handler === undefined) + return false; + + if (typeof handler === 'function') { + const result = handler.apply(this, args); + + // We check if result is undefined first because that + // is the most common case so we do not pay any perf + // penalty + if (result !== undefined && result !== null) { + addCatch(this, result, type, args); + } + } else { + const len = handler.length; + const listeners = arrayClone(handler); + for (let i = 0; i < len; ++i) { + const result = listeners[i].apply(this, args); + + // We check if result is undefined first because that + // is the most common case so we do not pay any perf + // penalty. + // This code is duplicated because extracting it away + // would make it non-inlineable. + if (result !== undefined && result !== null) { + addCatch(this, result, type, args); + } + } + } + + return true; + }; + + /** + * Adds a listener to the event emitter. + * @param {string | symbol} type + * @param {Function} listener + * @returns {EventEmitter} + */ + addListener(type, listener, prepend) { + debugger; + const target = this.eventEmitterTranslationLayer; + let m; + let events; + let existing; + + events = this._events; + if (events === undefined) { + events = this._events = {__proto__: null}; + this._eventsCount = 0; + } else { + // To avoid recursion in the case that type === "newListener"! Before + // adding it to the listeners, first emit "newListener". + if (events.newListener !== undefined) { + // TODO - emit this to be the eventEmitterTranslationLayer + this.eventEmitterTranslationLayer.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 = this._events; + } + existing = events[type]; + } + + if (existing === undefined) { + // Optimize the case of one listener. Don't need the extra array object. + events[type] = listener; + ++this._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); + } + + // Check for listener leak + // TODO - move away from this + m = _getMaxListeners(target); + if (m > 0 && existing.length > m && !existing.warned) { + existing.warned = true; + // No error code for this since it is a Warning + const w = genericNodeError( + `Possible EventEmitter memory leak detected. ${existing.length} ${String(type)} listeners ` + + `added to ${inspect(target, {depth: -1})}. Use emitter.setMaxListeners() to increase limit`, + {name: 'MaxListenersExceededWarning', emitter: target, type: type, count: existing.length}); + process.emitWarning(w); + } + } + + return this; + } + + /** + * Removes the specified `listener`. + * @param {string | symbol} type + * @param {Function} listener + * @returns {EventEmitter} + */ + removeListener(type, listener) { + const events = this._events; + if (events === undefined) + return undefined; + + const list = events[type]; + if (list === undefined) + return undefined; + + if (list === listener || list.listener === listener) { + this._eventsCount -= 1; + + if (this[kShapeMode]) { + events[type] = undefined; + } else if (this._eventsCount === 0) { + this._events = {__proto__: null}; + } else { + delete events[type]; + if (events.removeListener) + this.emit('removeListener', type, list.listener || listener); + } + } else if (typeof list !== 'function') { + let position = -1; + + for (let i = list.length - 1; i >= 0; i--) { + if (list[i] === listener || list[i].listener === listener) { + position = i; + break; + } + } + + if (position < 0) + return this; + + if (position === 0) + list.shift(); + 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, listener); + } + + return undefined; + }; + + /** + * Removes all listeners from the event emitter. (Only + * removes listeners for a specific event name if specified + * as `type`). + * @param {string | symbol} [type] + * @returns {EventEmitter} + */ + removeAllListeners(type) { + const events = this._events; + if (events === undefined) + return undefined; + + // Not listening for removeListener, no need to emit + if (events.removeListener === undefined) { + if (arguments.length === 0) { + this._events = {__proto__: null}; + this._eventsCount = 0; + } else if (events[type] !== undefined) { + if (--this._eventsCount === 0) + this._events = {__proto__: null}; + else + delete events[type]; + } + this[kShapeMode] = false; + return undefined; + } + + // Emit removeListener for all listeners on all events + if (arguments.length === 0) { + for (const key of ReflectOwnKeys(events)) { + if (key === 'removeListener') continue; + this.removeAllListeners(key); + } + this.removeAllListeners('removeListener'); + this._events = {__proto__: null}; + this._eventsCount = 0; + this[kShapeMode] = false; + return undefined; + } + + const listeners = events[type]; + + if (typeof listeners === 'function') { + this.removeListener(type, listeners); + } else if (listeners !== undefined) { + // LIFO order + for (let i = listeners.length - 1; i >= 0; i--) { + this.removeListener(type, listeners[i]); + } + } + + return undefined; + }; +} + + + +function addCatch(that, promise, type, args) { + if (!that.eventEmitterTranslationLayer[kCapture]) { + return; + } + + // Handle Promises/A+ spec, then could be a getter + // that throws on second use. + try { + const then = promise.then; + + if (typeof then === 'function') { + then.call(promise, undefined, function (err) { + // The callback is called with nextTick to avoid a follow-up + // rejection from this promise. + process.nextTick(emitUnhandledRejectionOrErr, that, err, type, args); + }); + } + } catch (err) { + that.emit('error', err); + } +} + +function emitUnhandledRejectionOrErr(ee, err, type, args) { + // TODO - kRejection should move to parent + if (typeof ee.eventEmitterTranslationLayer[kRejection] === 'function') { + ee.eventEmitterTranslationLayer[kRejection](err, type, ...args); + } else { + // We have to disable the capture rejections mechanism, otherwise + // we might end up in an infinite loop. + const prev = ee.eventEmitterTranslationLayer[kCapture]; + + // If the error handler throws, it is not catchable and it + // will end up in 'uncaughtException'. We restore the previous + // value of kCapture in case the uncaughtException is present + // and the exception is handled. + try { + ee.eventEmitterTranslationLayer[kCapture] = false; + ee.emit('error', err); + } finally { + ee.eventEmitterTranslationLayer[kCapture] = prev; + } + } +} + +// ------- Internal helpers + + +// TODO - move this to a different file +// TODO - rename +function throwErrorOnMissingErrorHandler(args) { + let er; + if (args.length > 0) + er = args[0]; + + if (er instanceof Error) { + try { + const capture = {}; + ErrorCaptureStackTrace(capture, EventEmitter.prototype.emit); + ObjectDefineProperty(er, kEnhanceStackBeforeInspector, { + __proto__: null, + value: FunctionPrototypeBind(enhanceStackTrace, this, er, capture), + configurable: true, + }); + } catch { + // Continue regardless of error. + } + + // Note: The comments on the `throw` lines are intentional, they show + // up in Node's output if this results in an unhandled exception. + throw er; // Unhandled 'error' event + } + + let stringifiedEr; + try { + stringifiedEr = inspect(er); + } catch { + stringifiedEr = er; + } + + // At least give some kind of context to the user + const err = new ERR_UNHANDLED_ERROR(stringifiedEr); + err.context = er; + throw err; // Unhandled 'error' event +} + + +// TODO - move this +function enhanceStackTrace(err, own) { + let ctorInfo = ''; + try { + const { name } = this.constructor; + if (name !== 'EventEmitter') + ctorInfo = ` on ${name} instance`; + } catch { + // Continue regardless of error. + } + const sep = `\nEmitted 'error' event${ctorInfo} at:\n`; + + const errStack = ArrayPrototypeSlice( + StringPrototypeSplit(err.stack, '\n'), 1); + const ownStack = ArrayPrototypeSlice( + StringPrototypeSplit(own.stack, '\n'), 1); + + const { len, offset } = identicalSequenceRange(ownStack, errStack); + if (len > 0) { + ArrayPrototypeSplice(ownStack, offset + 1, len - 2, + ' [... lines matching original stack trace ...]'); + } + + return err.stack + sep + ArrayPrototypeJoin(ownStack, '\n'); +} + + +function arrayClone(arr) { + // At least since V8 8.3, this implementation is faster than the previous + // which always used a simple for-loop + switch (arr.length) { + case 2: + return [arr[0], arr[1]]; + case 3: + return [arr[0], arr[1], arr[2]]; + case 4: + return [arr[0], arr[1], arr[2], arr[3]]; + case 5: + return [arr[0], arr[1], arr[2], arr[3], arr[4]]; + case 6: + return [arr[0], arr[1], arr[2], arr[3], arr[4], arr[5]]; + } + return ArrayPrototypeSlice(arr); +} + +// --------- Current + + + +// TODO - change this back to function prototype +// class EventEmitter { +// // [kImpl] = undefined; +// // [kIsFastPath] = true; +// // [kCaptureValue] = false; +// +// // _maxListeners = 0; +// // +// // // TODO - backwards compat +// // get _eventsCount() { +// // return this[kImpl]._eventsCount; +// // } +// // +// // set _eventsCount(n) { +// // // TODO - deprecate this +// // this[kImpl]._eventsCount = n; +// // } +// // +// // get _events() { +// // return this[kImpl]._events; +// // } +// // +// // set _events(events) { +// // // TODO - might need to change to slow path +// // // TODO - deprecate this +// // this[kImpl]._events = events; +// // } +// +// // TODO - add backwards compat +// // +// // constructor(opt) { +// // this[kImpl] = new FastEventEmitter(this, opt); +// // this[kIsFastPath] = true; +// // // TODO - call init +// // } +// +// /** +// * Increases the max listeners of the event emitter. +// * @param {number} n +// * @returns {EventEmitter} +// */ +// setMaxListeners(n) { +// validateNumber(n, 'setMaxListeners', 0); +// this._maxListeners = n; +// return this; +// } +// +// /** +// * Returns the current max listener value for the event emitter. +// * @returns {number} +// */ +// getMaxListeners() { +// return _getMaxListeners(this); +// } +// +// /** +// * Synchronously calls each of the listeners registered +// * for the event. +// * @param {string | symbol} type +// * @param {...any} [args] +// * @returns {boolean} +// */ +// emit(type, ...args) { +// return this[kImpl].emit(type, ...args); +// } +// +// /** +// * Adds a listener to the event emitter. +// * @param {string | symbol} type +// * @param {Function} listener +// * @returns {EventEmitter} +// */ +// addListener(type, listener) { +// checkListener(listener); +// +// if(this[kIsFastPath] && this[kImpl].isListenerAlreadyExists(type)) { +// this[kSwitchToSlowPath](); +// } +// +// return this[kImpl].addListener(type, listener); +// } +// +// /** +// * Adds a listener to the event emitter. +// * @param {string | symbol} type +// * @param {Function} listener +// * @returns {EventEmitter} +// */ +// // TODO - change to on = addListener +// on(type, listener) { +// checkListener(listener); +// +// if(this[kIsFastPath] && this[kImpl].isListenerAlreadyExists(type)) { +// this[kSwitchToSlowPath](); +// } +// +// return this[kImpl].addListener(type, listener); +// } +// +// +// /** +// * Adds the `listener` function to the beginning of +// * the listeners array. +// * @param {string | symbol} type +// * @param {Function} listener +// * @returns {EventEmitter} +// */ +// prependListener(type, listener) { +// checkListener(listener); +// +// if(this[kIsFastPath] && this[kImpl].isListenerAlreadyExists(type)) { +// this[kSwitchToSlowPath](); +// } +// +// return this[kImpl].addListener(type, listener, true); +// } +// +// +// /** +// * Adds a one-time `listener` function to the event emitter. +// * @param {string | symbol} type +// * @param {Function} listener +// * @returns {EventEmitter} +// */ +// once(type, listener) { +// checkListener(listener); +// +// this.on(type, _onceWrap(this, type, listener)); +// return this; +// }; +// +// +// /** +// * Adds a one-time `listener` function to the beginning of +// * the listeners array. +// * @param {string | symbol} type +// * @param {Function} listener +// * @returns {EventEmitter} +// */ +// prependOnceListener(type, listener) { +// checkListener(listener); +// +// this.prependListener(type, _onceWrap(this, type, listener)); +// return this; +// } +// +// +// /** +// * Removes the specified `listener` from the listeners array. +// * @param {string | symbol} type +// * @param {Function} listener +// * @returns {EventEmitter} +// */ +// removeListener(type, listener) { +// checkListener(listener); +// +// this[kImpl].removeListener(type, listener); +// +// return this; +// } +// +// // TODO - EventEmitter.prototype.off = EventEmitter.prototype.removeListener; +// off(type, listener) { +// // TODO - did remove listener had checkListener +// checkListener(listener); +// +// this[kImpl].removeListener(type, listener); +// +// return this; +// } +// +// /** +// * Removes all listeners from the event emitter. (Only +// * removes listeners for a specific event name if specified +// * as `type`). +// * @param {string | symbol} [type] +// * @returns {EventEmitter} +// */ +// removeAllListeners(type) { +// this[kImpl].removeAllListeners(type); +// return this; +// } +// +// /** +// * Returns a copy of the array of listeners for the event name +// * specified as `type`. +// * @param {string | symbol} type +// * @returns {Function[]} +// */ +// listeners(type) { +// return _listeners(this, type, true); +// }; +// +// /** +// * Returns a copy of the array of listeners and wrappers for +// * the event name specified as `type`. +// * @param {string | symbol} type +// * @returns {Function[]} +// */ +// rawListeners(type) { +// return _listeners(this, type, false); +// }; +// +// +// /** +// * Returns the number of listeners listening to event name +// * specified as `type`. +// * @param {string | symbol} type +// * @param {Function} listener +// * @returns {number} +// */ +// listenerCount(type, listener) { +// // TODO - change this back to prototype +// return listenerCount.call(this, type, listener); +// } +// +// /** +// * Returns an array listing the events for which +// * the emitter has registered listeners. +// * @returns {any[]} +// */ +// eventNames() { +// return this._eventsCount > 0 ? ReflectOwnKeys(this._events) : []; +// }; +// +// [kSwitchToSlowPath]() { +// if(!this[kIsFastPath]) { +// return; +// } +// +// this[kIsFastPath] = false; +// this[kImpl] = SlowEventEmitter.fromFastEventEmitter(this[kImpl]); +// } +// } +// +// // The default for captureRejections is false +// ObjectDefineProperty(EventEmitter.prototype, kCapture, { +// __proto__: null, +// get() { +// return this[kCaptureValue]; +// }, +// set(value) { +// this[kCaptureValue] = value; +// +// if (value) { +// this[kSwitchToSlowPath](); +// } +// }, +// writable: true, +// enumerable: false, +// }); +// From a7b0563b3c9339fd782679fba8ddaf060d977cae Mon Sep 17 00:00:00 2001 From: Raz Luvaton <16746759+rluvaton@users.noreply.github.com> Date: Sun, 28 Apr 2024 14:22:25 +0300 Subject: [PATCH 02/69] events: remove duplicate functions --- lib/events.js | 43 ------------------------------------------- 1 file changed, 43 deletions(-) diff --git a/lib/events.js b/lib/events.js index 7fbe8e2ecdd69d..ed81587012163e 100644 --- a/lib/events.js +++ b/lib/events.js @@ -1760,50 +1760,7 @@ function throwErrorOnMissingErrorHandler(args) { } -// TODO - move this -function enhanceStackTrace(err, own) { - let ctorInfo = ''; - try { - const { name } = this.constructor; - if (name !== 'EventEmitter') - ctorInfo = ` on ${name} instance`; - } catch { - // Continue regardless of error. - } - const sep = `\nEmitted 'error' event${ctorInfo} at:\n`; - - const errStack = ArrayPrototypeSlice( - StringPrototypeSplit(err.stack, '\n'), 1); - const ownStack = ArrayPrototypeSlice( - StringPrototypeSplit(own.stack, '\n'), 1); - const { len, offset } = identicalSequenceRange(ownStack, errStack); - if (len > 0) { - ArrayPrototypeSplice(ownStack, offset + 1, len - 2, - ' [... lines matching original stack trace ...]'); - } - - return err.stack + sep + ArrayPrototypeJoin(ownStack, '\n'); -} - - -function arrayClone(arr) { - // At least since V8 8.3, this implementation is faster than the previous - // which always used a simple for-loop - switch (arr.length) { - case 2: - return [arr[0], arr[1]]; - case 3: - return [arr[0], arr[1], arr[2]]; - case 4: - return [arr[0], arr[1], arr[2], arr[3]]; - case 5: - return [arr[0], arr[1], arr[2], arr[3], arr[4]]; - case 6: - return [arr[0], arr[1], arr[2], arr[3], arr[4], arr[5]]; - } - return ArrayPrototypeSlice(arr); -} // --------- Current From 5c7a50d507c4c5451bce27cc52f9394308464d32 Mon Sep 17 00:00:00 2001 From: Raz Luvaton <16746759+rluvaton@users.noreply.github.com> Date: Sun, 28 Apr 2024 14:34:38 +0300 Subject: [PATCH 03/69] events: remove debugger and cleanup --- lib/events.js | 133 +++++++++++++++++++------------------------------- 1 file changed, 49 insertions(+), 84 deletions(-) diff --git a/lib/events.js b/lib/events.js index ed81587012163e..c7f036cbe8778a 100644 --- a/lib/events.js +++ b/lib/events.js @@ -36,6 +36,7 @@ const { NumberMAX_SAFE_INTEGER, ObjectDefineProperties, ObjectDefineProperty, + ObjectEntries, ObjectGetPrototypeOf, ObjectSetPrototypeOf, Promise, @@ -314,12 +315,16 @@ ObjectDefineProperties(EventEmitter.prototype, { }, set: function(arg) { // TODO - remove optional chaining - if(!this[kImpl]) { + if (!this[kImpl]) { // TODO(rluvaton): find a better way and also support array with length 1? // TODO(rluvaton): if have newListener and removeListener events, switch to slow path - const shouldBeFastPath = arg === undefined || Object.entries(arg).every(([key, value]) => !isEventUnsupportedForFastPath(key) && typeof value === 'undefined' || typeof value === 'function'); + const shouldBeFastPath = arg === undefined || + ObjectEntries(arg).some(({ 0: key, 1: value }) => + isEventUnsupportedForFastPath(key) || (typeof value !== 'undefined' && typeof value !== 'function')); + // TODO(rluvaton): remove this eslint ignore + // eslint-disable-next-line no-use-before-define this[kImpl] = shouldBeFastPath ? new FastEventEmitter(this) : new SlowEventEmitter(this); this[kIsFastPath] = shouldBeFastPath; // return; @@ -339,8 +344,11 @@ ObjectDefineProperties(EventEmitter.prototype, { }, set: function(arg) { // TODO - remove optional chaining - if(!this[kImpl]) { + if (!this[kImpl]) { // TODO - don't use slow by default here + + // TODO(rluvaton): remove this eslint ignore + // eslint-disable-next-line no-use-before-define this[kImpl] = new SlowEventEmitter(this); this[kIsFastPath] = false; // return; @@ -436,14 +444,19 @@ EventEmitter.init = function(opts) { this[kCapture] = EventEmitter.prototype[kCapture]; } - if(this[kCapture]) { - if(this[kIsFastPath]) { + if (this[kCapture]) { + if (this[kIsFastPath]) { this[kSwitchToSlowPath](); - } else if(!this[kImpl]) { + } else if (!this[kImpl]) { + + // TODO(rluvaton): remove this eslint ignore + // eslint-disable-next-line no-use-before-define this[kImpl] = new SlowEventEmitter(this); this[kIsFastPath] = false; } } + // TODO(rluvaton): remove this eslint ignore + // eslint-disable-next-line no-use-before-define this[kImpl] ??= new FastEventEmitter(this, opts); this[kIsFastPath] ??= true; @@ -461,61 +474,21 @@ EventEmitter.init = function(opts) { }; EventEmitter.prototype[kSwitchToSlowPath] = function() { - if(!this[kIsFastPath]) { + if (!this[kIsFastPath]) { return; } this[kIsFastPath] = false; + + // TODO(rluvaton): remove this eslint ignore + // eslint-disable-next-line no-use-before-define this[kImpl] = SlowEventEmitter.fromFastEventEmitter(this[kImpl]); -} +}; EventEmitter.prototype[kContinueAddEventListenerInSlowMode] = function(type, listener, prepend) { // TODO - this is in case while adding listener another listener added in the middle using the newListener event // TODO - maybe for simplicity we can just switch to slow mode if have newListener event -} - -function addCatch(that, promise, type, args) { - if (!that[kCapture]) { - return; - } - - // Handle Promises/A+ spec, then could be a getter - // that throws on second use. - try { - const then = promise.then; - - if (typeof then === 'function') { - then.call(promise, undefined, function(err) { - // The callback is called with nextTick to avoid a follow-up - // rejection from this promise. - process.nextTick(emitUnhandledRejectionOrErr, that, err, type, args); - }); - } - } catch (err) { - that.emit('error', err); - } -} - -function emitUnhandledRejectionOrErr(ee, err, type, args) { - if (typeof ee[kRejection] === 'function') { - ee[kRejection](err, type, ...args); - } else { - // We have to disable the capture rejections mechanism, otherwise - // we might end up in an infinite loop. - const prev = ee[kCapture]; - - // If the error handler throws, it is not catchable and it - // will end up in 'uncaughtException'. We restore the previous - // value of kCapture in case the uncaughtException is present - // and the exception is handled. - try { - ee[kCapture] = false; - ee.emit('error', err); - } finally { - ee[kCapture] = prev; - } - } -} +}; /** * Increases the max listeners of the event emitter. @@ -645,7 +618,7 @@ EventEmitter.prototype.addListener = function addListener(type, listener) { checkListener(listener); // TODO - don't handle newListener and removeListener events in fast path for complexity reasons - if(this[kIsFastPath] && (isEventUnsupportedForFastPath(type) || this[kImpl].isListenerAlreadyExists(type))) { + if (this[kIsFastPath] && (isEventUnsupportedForFastPath(type) || this[kImpl].isListenerAlreadyExists(type))) { this[kSwitchToSlowPath](); } @@ -665,7 +638,7 @@ EventEmitter.prototype.prependListener = function prependListener(type, listener) { checkListener(listener); - if(this[kIsFastPath] && (type === 'newListener' || type === 'removeListener' || this[kImpl].isListenerAlreadyExists(type))) { + if (this[kIsFastPath] && (isEventUnsupportedForFastPath(type) || this[kImpl].isListenerAlreadyExists(type))) { this[kSwitchToSlowPath](); } @@ -1197,7 +1170,7 @@ class FastEventEmitter { // TODO - have a way to support passing _events // TODO - add comment on how events are stored _events = undefined; - _eventsCount + _eventsCount; // TODO - convert to symbol and rename eventEmitterTranslationLayer = undefined; stale = false; @@ -1213,7 +1186,7 @@ class FastEventEmitter { // TODO - change the this here? this._events === ObjectGetPrototypeOf(this)._events) { // TODO - removed the __proto__ assignment - this._events = { }; + this._events = { }; this._eventsCount = 0; this[kShapeMode] = false; } else { @@ -1230,7 +1203,7 @@ class FastEventEmitter { */ emit(type, ...args) { const events = this._events; - if(type === 'error' && events?.error === undefined) { + if (type === 'error' && events?.error === undefined) { throwErrorOnMissingErrorHandler.apply(this, args); // TODO - add assertion that should not reach here; return false; @@ -1239,7 +1212,7 @@ class FastEventEmitter { // TODO(rluvaton): will it be faster to add check if events is undefined instead? const handler = events?.[type]; - if(handler === undefined) { + if (handler === undefined) { return false; } @@ -1247,7 +1220,7 @@ class FastEventEmitter { handler.apply(this.eventEmitterTranslationLayer, args); return true; - }; + } // TODO - should switch to slow mode isListenerAlreadyExists(type) { @@ -1259,7 +1232,6 @@ class FastEventEmitter { * @param {string | symbol} type * @param {Function} listener * @param {boolean} prepend not used here as we are in fast mode and only have single listener - * @returns {EventEmitter} */ addListener(type, listener, prepend = undefined) { // TODO - add validation before getting here @@ -1271,7 +1243,7 @@ class FastEventEmitter { if (events === undefined) { events = this._events = { // TODO - change this? - __proto__: null + __proto__: null, }; this._eventsCount = 0; } else { @@ -1318,7 +1290,6 @@ class FastEventEmitter { * Removes the specified `listener`. * @param {string | symbol} type * @param {Function} listener - * @returns {EventEmitter} */ removeListener(type, listener) { // TODO - add validation before getting here @@ -1338,7 +1309,7 @@ class FastEventEmitter { events[type] = undefined; } else if (this._eventsCount === 0) { // TODO - keep this? - this._events = {__proto__: null}; + this._events = { __proto__: null }; } else { delete events[type]; // TODO(rluvaton): support addListener and removeListener events @@ -1346,14 +1317,13 @@ class FastEventEmitter { // // TODO(rluvaton): use apply to pass the parent eventEmitter // this.emit('removeListener', type, list.listener || listener); } - }; + } /** * Removes all listeners from the event emitter. (Only * removes listeners for a specific event name if specified * as `type`). * @param {string | symbol} [type] - * @returns {EventEmitter} */ removeAllListeners(type) { // TODO - parent function should return `this` @@ -1396,7 +1366,7 @@ class FastEventEmitter { if (typeof listeners === 'function') { this.removeListener(type, listeners); } - }; + } } // --------------- SLOW @@ -1412,7 +1382,7 @@ class SlowEventEmitter { // TODO - have a way to support passing _events // TODO - add comment on how events are stored _events = undefined; - _eventsCount = 0 + _eventsCount = 0; // TODO - convert to symbol and rename eventEmitterTranslationLayer = undefined; @@ -1492,7 +1462,7 @@ class SlowEventEmitter { } return true; - }; + } /** * Adds a listener to the event emitter. @@ -1501,7 +1471,6 @@ class SlowEventEmitter { * @returns {EventEmitter} */ addListener(type, listener, prepend) { - debugger; const target = this.eventEmitterTranslationLayer; let m; let events; @@ -1509,7 +1478,7 @@ class SlowEventEmitter { events = this._events; if (events === undefined) { - events = this._events = {__proto__: null}; + events = this._events = { __proto__: null }; this._eventsCount = 0; } else { // To avoid recursion in the case that type === "newListener"! Before @@ -1517,7 +1486,7 @@ class SlowEventEmitter { if (events.newListener !== undefined) { // TODO - emit this to be the eventEmitterTranslationLayer this.eventEmitterTranslationLayer.emit('newListener', type, - listener.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 @@ -1550,8 +1519,8 @@ class SlowEventEmitter { // No error code for this since it is a Warning const w = genericNodeError( `Possible EventEmitter memory leak detected. ${existing.length} ${String(type)} listeners ` + - `added to ${inspect(target, {depth: -1})}. Use emitter.setMaxListeners() to increase limit`, - {name: 'MaxListenersExceededWarning', emitter: target, type: type, count: existing.length}); + `added to ${inspect(target, { depth: -1 })}. Use emitter.setMaxListeners() to increase limit`, + { name: 'MaxListenersExceededWarning', emitter: target, type: type, count: existing.length }); process.emitWarning(w); } } @@ -1580,7 +1549,7 @@ class SlowEventEmitter { if (this[kShapeMode]) { events[type] = undefined; } else if (this._eventsCount === 0) { - this._events = {__proto__: null}; + this._events = { __proto__: null }; } else { delete events[type]; if (events.removeListener) @@ -1615,7 +1584,7 @@ class SlowEventEmitter { } return undefined; - }; + } /** * Removes all listeners from the event emitter. (Only @@ -1632,11 +1601,11 @@ class SlowEventEmitter { // Not listening for removeListener, no need to emit if (events.removeListener === undefined) { if (arguments.length === 0) { - this._events = {__proto__: null}; + this._events = { __proto__: null }; this._eventsCount = 0; } else if (events[type] !== undefined) { if (--this._eventsCount === 0) - this._events = {__proto__: null}; + this._events = { __proto__: null }; else delete events[type]; } @@ -1651,7 +1620,7 @@ class SlowEventEmitter { this.removeAllListeners(key); } this.removeAllListeners('removeListener'); - this._events = {__proto__: null}; + this._events = { __proto__: null }; this._eventsCount = 0; this[kShapeMode] = false; return undefined; @@ -1669,11 +1638,10 @@ class SlowEventEmitter { } return undefined; - }; + } } - function addCatch(that, promise, type, args) { if (!that.eventEmitterTranslationLayer[kCapture]) { return; @@ -1685,7 +1653,7 @@ function addCatch(that, promise, type, args) { const then = promise.then; if (typeof then === 'function') { - then.call(promise, undefined, function (err) { + then.call(promise, undefined, function(err) { // The callback is called with nextTick to avoid a follow-up // rejection from this promise. process.nextTick(emitUnhandledRejectionOrErr, that, err, type, args); @@ -1760,12 +1728,9 @@ function throwErrorOnMissingErrorHandler(args) { } - - // --------- Current - // TODO - change this back to function prototype // class EventEmitter { // // [kImpl] = undefined; From e633fd8a597baaa1755099beac64152559f293cf Mon Sep 17 00:00:00 2001 From: Raz Luvaton <16746759+rluvaton@users.noreply.github.com> Date: Sun, 28 Apr 2024 14:38:57 +0300 Subject: [PATCH 04/69] events: fix missing handler this reference --- lib/events.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/events.js b/lib/events.js index c7f036cbe8778a..2d6474f970c605 100644 --- a/lib/events.js +++ b/lib/events.js @@ -1204,7 +1204,7 @@ class FastEventEmitter { emit(type, ...args) { const events = this._events; if (type === 'error' && events?.error === undefined) { - throwErrorOnMissingErrorHandler.apply(this, args); + throwErrorOnMissingErrorHandler.apply(this.eventEmitterTranslationLayer, args); // TODO - add assertion that should not reach here; return false; } @@ -1427,7 +1427,7 @@ class SlowEventEmitter { // If there is no 'error' event listener then throw. if (doError) { - throwErrorOnMissingErrorHandler(args); + throwErrorOnMissingErrorHandler.apply(this.eventEmitterTranslationLayer, args); } const handler = events[type]; From 9a5fc6570f507180db1eccf024f592ec08d9672a Mon Sep 17 00:00:00 2001 From: Raz Luvaton <16746759+rluvaton@users.noreply.github.com> Date: Sun, 28 Apr 2024 14:42:41 +0300 Subject: [PATCH 05/69] events: fix some more things --- lib/events.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/events.js b/lib/events.js index 2d6474f970c605..817658a381fc62 100644 --- a/lib/events.js +++ b/lib/events.js @@ -1173,7 +1173,6 @@ class FastEventEmitter { _eventsCount; // TODO - convert to symbol and rename eventEmitterTranslationLayer = undefined; - stale = false; // TODO - use opts constructor(eventEmitterTranslationLayer, opts) { @@ -1416,6 +1415,7 @@ class SlowEventEmitter { */ emit(type, ...args) { let doError = (type === 'error'); + const eventEmitterTranslationLayer = this.eventEmitterTranslationLayer; const events = this._events; if (events !== undefined) { @@ -1427,7 +1427,7 @@ class SlowEventEmitter { // If there is no 'error' event listener then throw. if (doError) { - throwErrorOnMissingErrorHandler.apply(this.eventEmitterTranslationLayer, args); + throwErrorOnMissingErrorHandler.apply(eventEmitterTranslationLayer, args); } const handler = events[type]; @@ -1436,19 +1436,19 @@ class SlowEventEmitter { return false; if (typeof handler === 'function') { - const result = handler.apply(this, args); + const result = handler.apply(eventEmitterTranslationLayer, args); // We check if result is undefined first because that // is the most common case so we do not pay any perf // penalty if (result !== undefined && result !== null) { - addCatch(this, result, type, args); + addCatch(eventEmitterTranslationLayer, result, type, args); } } else { const len = handler.length; const listeners = arrayClone(handler); for (let i = 0; i < len; ++i) { - const result = listeners[i].apply(this, args); + const result = listeners[i].apply(eventEmitterTranslationLayer, args); // We check if result is undefined first because that // is the most common case so we do not pay any perf @@ -1456,7 +1456,7 @@ class SlowEventEmitter { // This code is duplicated because extracting it away // would make it non-inlineable. if (result !== undefined && result !== null) { - addCatch(this, result, type, args); + addCatch(eventEmitterTranslationLayer, result, type, args); } } } @@ -1643,7 +1643,7 @@ class SlowEventEmitter { function addCatch(that, promise, type, args) { - if (!that.eventEmitterTranslationLayer[kCapture]) { + if (!that[kCapture]) { return; } From afd651503b1f581a218659885845fde7c16851e6 Mon Sep 17 00:00:00 2001 From: Raz Luvaton <16746759+rluvaton@users.noreply.github.com> Date: Sun, 28 Apr 2024 14:47:11 +0300 Subject: [PATCH 06/69] events: remvoe unused --- lib/events.js | 6 ------ 1 file changed, 6 deletions(-) diff --git a/lib/events.js b/lib/events.js index 817658a381fc62..00d3e117d5c05b 100644 --- a/lib/events.js +++ b/lib/events.js @@ -99,7 +99,6 @@ const kImpl = Symbol('kImpl'); const kCaptureValue = Symbol('kCaptureValue'); const kIsFastPath = Symbol('kIsFastPath'); const kSwitchToSlowPath = Symbol('kSwitchToSlowPath'); -const kContinueAddEventListenerInSlowMode = Symbol('kContinueAddEventListenerInSlowMode'); let EventEmitterAsyncResource; // The EventEmitterAsyncResource has to be initialized lazily because event.js @@ -485,11 +484,6 @@ EventEmitter.prototype[kSwitchToSlowPath] = function() { this[kImpl] = SlowEventEmitter.fromFastEventEmitter(this[kImpl]); }; -EventEmitter.prototype[kContinueAddEventListenerInSlowMode] = function(type, listener, prepend) { - // TODO - this is in case while adding listener another listener added in the middle using the newListener event - // TODO - maybe for simplicity we can just switch to slow mode if have newListener event -}; - /** * Increases the max listeners of the event emitter. * @param {number} n From 569ef1ebefefc33b5bfc760e40e9e10b4479e578 Mon Sep 17 00:00:00 2001 From: Raz Luvaton <16746759+rluvaton@users.noreply.github.com> Date: Sun, 28 Apr 2024 14:56:33 +0300 Subject: [PATCH 07/69] events: fix tests --- lib/events.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/events.js b/lib/events.js index 00d3e117d5c05b..62d8dc9d0134c0 100644 --- a/lib/events.js +++ b/lib/events.js @@ -1685,7 +1685,7 @@ function emitUnhandledRejectionOrErr(ee, err, type, args) { // TODO - move this to a different file // TODO - rename -function throwErrorOnMissingErrorHandler(args) { +function throwErrorOnMissingErrorHandler(...args) { let er; if (args.length > 0) er = args[0]; From 3d25c6c6d0fe099ade9c021fdd86ff4efbd2b24d Mon Sep 17 00:00:00 2001 From: Raz Luvaton <16746759+rluvaton@users.noreply.github.com> Date: Sun, 28 Apr 2024 15:10:04 +0300 Subject: [PATCH 08/69] events: remove cmments --- lib/events.js | 264 +------------------------------------------------- 1 file changed, 1 insertion(+), 263 deletions(-) diff --git a/lib/events.js b/lib/events.js index 62d8dc9d0134c0..227dc43afc625f 100644 --- a/lib/events.js +++ b/lib/events.js @@ -1285,8 +1285,6 @@ class FastEventEmitter { * @param {Function} listener */ removeListener(type, listener) { - // TODO - add validation before getting here - // TODO - parent function should return `this` const events = this._events; if (events === undefined) @@ -1319,7 +1317,6 @@ class FastEventEmitter { * @param {string | symbol} [type] */ removeAllListeners(type) { - // TODO - parent function should return `this` const events = this._events; if (events === undefined) return undefined; @@ -1394,10 +1391,8 @@ class SlowEventEmitter { return eventEmitter; } - // TODO - use opts - constructor(eventEmitterTranslationLayer, opts) { + constructor(eventEmitterTranslationLayer) { this.eventEmitterTranslationLayer = eventEmitterTranslationLayer; - // TODO - } /** @@ -1720,260 +1715,3 @@ function throwErrorOnMissingErrorHandler(...args) { err.context = er; throw err; // Unhandled 'error' event } - - -// --------- Current - - -// TODO - change this back to function prototype -// class EventEmitter { -// // [kImpl] = undefined; -// // [kIsFastPath] = true; -// // [kCaptureValue] = false; -// -// // _maxListeners = 0; -// // -// // // TODO - backwards compat -// // get _eventsCount() { -// // return this[kImpl]._eventsCount; -// // } -// // -// // set _eventsCount(n) { -// // // TODO - deprecate this -// // this[kImpl]._eventsCount = n; -// // } -// // -// // get _events() { -// // return this[kImpl]._events; -// // } -// // -// // set _events(events) { -// // // TODO - might need to change to slow path -// // // TODO - deprecate this -// // this[kImpl]._events = events; -// // } -// -// // TODO - add backwards compat -// // -// // constructor(opt) { -// // this[kImpl] = new FastEventEmitter(this, opt); -// // this[kIsFastPath] = true; -// // // TODO - call init -// // } -// -// /** -// * Increases the max listeners of the event emitter. -// * @param {number} n -// * @returns {EventEmitter} -// */ -// setMaxListeners(n) { -// validateNumber(n, 'setMaxListeners', 0); -// this._maxListeners = n; -// return this; -// } -// -// /** -// * Returns the current max listener value for the event emitter. -// * @returns {number} -// */ -// getMaxListeners() { -// return _getMaxListeners(this); -// } -// -// /** -// * Synchronously calls each of the listeners registered -// * for the event. -// * @param {string | symbol} type -// * @param {...any} [args] -// * @returns {boolean} -// */ -// emit(type, ...args) { -// return this[kImpl].emit(type, ...args); -// } -// -// /** -// * Adds a listener to the event emitter. -// * @param {string | symbol} type -// * @param {Function} listener -// * @returns {EventEmitter} -// */ -// addListener(type, listener) { -// checkListener(listener); -// -// if(this[kIsFastPath] && this[kImpl].isListenerAlreadyExists(type)) { -// this[kSwitchToSlowPath](); -// } -// -// return this[kImpl].addListener(type, listener); -// } -// -// /** -// * Adds a listener to the event emitter. -// * @param {string | symbol} type -// * @param {Function} listener -// * @returns {EventEmitter} -// */ -// // TODO - change to on = addListener -// on(type, listener) { -// checkListener(listener); -// -// if(this[kIsFastPath] && this[kImpl].isListenerAlreadyExists(type)) { -// this[kSwitchToSlowPath](); -// } -// -// return this[kImpl].addListener(type, listener); -// } -// -// -// /** -// * Adds the `listener` function to the beginning of -// * the listeners array. -// * @param {string | symbol} type -// * @param {Function} listener -// * @returns {EventEmitter} -// */ -// prependListener(type, listener) { -// checkListener(listener); -// -// if(this[kIsFastPath] && this[kImpl].isListenerAlreadyExists(type)) { -// this[kSwitchToSlowPath](); -// } -// -// return this[kImpl].addListener(type, listener, true); -// } -// -// -// /** -// * Adds a one-time `listener` function to the event emitter. -// * @param {string | symbol} type -// * @param {Function} listener -// * @returns {EventEmitter} -// */ -// once(type, listener) { -// checkListener(listener); -// -// this.on(type, _onceWrap(this, type, listener)); -// return this; -// }; -// -// -// /** -// * Adds a one-time `listener` function to the beginning of -// * the listeners array. -// * @param {string | symbol} type -// * @param {Function} listener -// * @returns {EventEmitter} -// */ -// prependOnceListener(type, listener) { -// checkListener(listener); -// -// this.prependListener(type, _onceWrap(this, type, listener)); -// return this; -// } -// -// -// /** -// * Removes the specified `listener` from the listeners array. -// * @param {string | symbol} type -// * @param {Function} listener -// * @returns {EventEmitter} -// */ -// removeListener(type, listener) { -// checkListener(listener); -// -// this[kImpl].removeListener(type, listener); -// -// return this; -// } -// -// // TODO - EventEmitter.prototype.off = EventEmitter.prototype.removeListener; -// off(type, listener) { -// // TODO - did remove listener had checkListener -// checkListener(listener); -// -// this[kImpl].removeListener(type, listener); -// -// return this; -// } -// -// /** -// * Removes all listeners from the event emitter. (Only -// * removes listeners for a specific event name if specified -// * as `type`). -// * @param {string | symbol} [type] -// * @returns {EventEmitter} -// */ -// removeAllListeners(type) { -// this[kImpl].removeAllListeners(type); -// return this; -// } -// -// /** -// * Returns a copy of the array of listeners for the event name -// * specified as `type`. -// * @param {string | symbol} type -// * @returns {Function[]} -// */ -// listeners(type) { -// return _listeners(this, type, true); -// }; -// -// /** -// * Returns a copy of the array of listeners and wrappers for -// * the event name specified as `type`. -// * @param {string | symbol} type -// * @returns {Function[]} -// */ -// rawListeners(type) { -// return _listeners(this, type, false); -// }; -// -// -// /** -// * Returns the number of listeners listening to event name -// * specified as `type`. -// * @param {string | symbol} type -// * @param {Function} listener -// * @returns {number} -// */ -// listenerCount(type, listener) { -// // TODO - change this back to prototype -// return listenerCount.call(this, type, listener); -// } -// -// /** -// * Returns an array listing the events for which -// * the emitter has registered listeners. -// * @returns {any[]} -// */ -// eventNames() { -// return this._eventsCount > 0 ? ReflectOwnKeys(this._events) : []; -// }; -// -// [kSwitchToSlowPath]() { -// if(!this[kIsFastPath]) { -// return; -// } -// -// this[kIsFastPath] = false; -// this[kImpl] = SlowEventEmitter.fromFastEventEmitter(this[kImpl]); -// } -// } -// -// // The default for captureRejections is false -// ObjectDefineProperty(EventEmitter.prototype, kCapture, { -// __proto__: null, -// get() { -// return this[kCaptureValue]; -// }, -// set(value) { -// this[kCaptureValue] = value; -// -// if (value) { -// this[kSwitchToSlowPath](); -// } -// }, -// writable: true, -// enumerable: false, -// }); -// From 864c23c29c53bfc17096e0a1164dd5c479baf70b Mon Sep 17 00:00:00 2001 From: Raz Luvaton <16746759+rluvaton@users.noreply.github.com> Date: Sun, 28 Apr 2024 15:10:27 +0300 Subject: [PATCH 09/69] events: pass kShape to impl --- lib/events.js | 23 +++++------------------ 1 file changed, 5 insertions(+), 18 deletions(-) diff --git a/lib/events.js b/lib/events.js index 227dc43afc625f..f0f3b30e69e770 100644 --- a/lib/events.js +++ b/lib/events.js @@ -464,9 +464,9 @@ EventEmitter.init = function(opts) { this._events === ObjectGetPrototypeOf(this)._events) { this._events = { __proto__: null }; this._eventsCount = 0; - this[kShapeMode] = false; + this[kImpl][kShapeMode] = false; } else { - this[kShapeMode] = true; + this[kImpl][kShapeMode] = true; } this._maxListeners = this._maxListeners || undefined; @@ -1167,24 +1167,10 @@ class FastEventEmitter { _eventsCount; // TODO - convert to symbol and rename eventEmitterTranslationLayer = undefined; + [kShapeMode] = undefined; - // TODO - use opts - constructor(eventEmitterTranslationLayer, opts) { + constructor(eventEmitterTranslationLayer) { this.eventEmitterTranslationLayer = eventEmitterTranslationLayer; - // TODO - check this: - // If you're updating this function definition, please also update any - // re-definitions, such as the one in the Domain module (lib/domain.js). - if (this._events === undefined || - // TODO - this is not correct - // TODO - change the this here? - this._events === ObjectGetPrototypeOf(this)._events) { - // TODO - removed the __proto__ assignment - this._events = { }; - this._eventsCount = 0; - this[kShapeMode] = false; - } else { - this[kShapeMode] = true; - } } /** @@ -1375,6 +1361,7 @@ class SlowEventEmitter { _eventsCount = 0; // TODO - convert to symbol and rename eventEmitterTranslationLayer = undefined; + [kShapeMode] = undefined; /** * From 00e599bf1dd2c0f7c0a349efd5252a22c7845b0a Mon Sep 17 00:00:00 2001 From: Raz Luvaton <16746759+rluvaton@users.noreply.github.com> Date: Sun, 28 Apr 2024 15:14:18 +0300 Subject: [PATCH 10/69] events: remove comments --- lib/events.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/lib/events.js b/lib/events.js index f0f3b30e69e770..a16ae5d730a9fe 100644 --- a/lib/events.js +++ b/lib/events.js @@ -1161,7 +1161,6 @@ function listenersController() { // 3. kErrorMonitor - undefined by default // TODO - add comment for what this is optimized for class FastEventEmitter { - // TODO - have a way to support passing _events // TODO - add comment on how events are stored _events = undefined; _eventsCount; @@ -1201,7 +1200,6 @@ class FastEventEmitter { return true; } - // TODO - should switch to slow mode isListenerAlreadyExists(type) { return this._events?.[type] !== undefined; } @@ -1213,8 +1211,6 @@ class FastEventEmitter { * @param {boolean} prepend not used here as we are in fast mode and only have single listener */ addListener(type, listener, prepend = undefined) { - // TODO - add validation before getting here - // TODO - if got here that can add without switching to slow mode let events; events = this._events; From c297ca44526625e886b980d3bc5e0ea9069fef7a Mon Sep 17 00:00:00 2001 From: Raz Luvaton <16746759+rluvaton@users.noreply.github.com> Date: Sun, 28 Apr 2024 15:49:44 +0300 Subject: [PATCH 11/69] events: improve performance of stream creation --- lib/events.js | 28 +++++++++++++++++++++++++--- lib/internal/events/symbols.js | 2 ++ lib/internal/streams/duplex.js | 4 ++++ lib/internal/streams/readable.js | 5 +++++ lib/internal/streams/writable.js | 4 ++++ 5 files changed, 40 insertions(+), 3 deletions(-) diff --git a/lib/events.js b/lib/events.js index a16ae5d730a9fe..ecfb7f2b51ada0 100644 --- a/lib/events.js +++ b/lib/events.js @@ -86,6 +86,8 @@ const { validateString, } = require('internal/validators'); const { addAbortListener } = require('internal/events/abort_listener'); +const { kInitialFastPath } = require('internal/events/symbols'); + const kCapture = Symbol('kCapture'); const kErrorMonitor = Symbol('events.errorMonitor'); @@ -281,6 +283,13 @@ ObjectDefineProperties(EventEmitter.prototype, { configurable: false, writable: true, }, + [kInitialFastPath]: { + __proto__: null, + value: undefined, + enumerable: false, + configurable: false, + writable: true, + }, [kCaptureValue]: { __proto__: null, value: false, @@ -313,8 +322,19 @@ ObjectDefineProperties(EventEmitter.prototype, { return this[kImpl]?._events; }, set: function(arg) { - // TODO - remove optional chaining - if (!this[kImpl]) { + + if (this[kImpl] === undefined && this[kInitialFastPath] === true) { + // TODO(rluvaton): remove this eslint ignore + // eslint-disable-next-line no-use-before-define + this[kImpl] = new FastEventEmitter(this); + this[kIsFastPath] = true; + this[kInitialFastPath] = undefined; + this[kImpl]._events = arg; + return; + } + + if (this[kImpl] === undefined) { + // TODO(rluvaton): find a better way and also support array with length 1? // TODO(rluvaton): if have newListener and removeListener events, switch to slow path @@ -326,8 +346,8 @@ ObjectDefineProperties(EventEmitter.prototype, { // eslint-disable-next-line no-use-before-define this[kImpl] = shouldBeFastPath ? new FastEventEmitter(this) : new SlowEventEmitter(this); this[kIsFastPath] = shouldBeFastPath; - // return; } + // TODO - might need to change to slow path // TODO - deprecate this this[kImpl]._events = arg; @@ -359,6 +379,7 @@ ObjectDefineProperties(EventEmitter.prototype, { }); EventEmitter.prototype._maxListeners = undefined; +EventEmitter.prototype[kInitialFastPath] = undefined; // By default EventEmitters will print a warning if more than 10 listeners are // added to it. This is a useful default which helps finding memory leaks. @@ -470,6 +491,7 @@ EventEmitter.init = function(opts) { } this._maxListeners = this._maxListeners || undefined; + this[kInitialFastPath] = undefined; }; EventEmitter.prototype[kSwitchToSlowPath] = function() { diff --git a/lib/internal/events/symbols.js b/lib/internal/events/symbols.js index b1b89ddb8f0a4d..c14363aa8af949 100644 --- a/lib/internal/events/symbols.js +++ b/lib/internal/events/symbols.js @@ -5,7 +5,9 @@ const { } = primordials; const kFirstEventParam = Symbol('nodejs.kFirstEventParam'); +const kInitialFastPath = Symbol('kInitialFastPath'); module.exports = { kFirstEventParam, + kInitialFastPath, }; diff --git a/lib/internal/streams/duplex.js b/lib/internal/streams/duplex.js index 6892150c49f659..ff6bfb089a128c 100644 --- a/lib/internal/streams/duplex.js +++ b/lib/internal/streams/duplex.js @@ -38,6 +38,7 @@ module.exports = Duplex; const Stream = require('internal/streams/legacy').Stream; const Readable = require('internal/streams/readable'); const Writable = require('internal/streams/writable'); +const { kInitialFastPath } = require('internal/events/symbols'); const { addAbortSignal, @@ -66,6 +67,9 @@ function Duplex(options) { if (!(this instanceof Duplex)) return new Duplex(options); + if (this._events === undefined || this._events === null) { + this[kInitialFastPath] = true; + } this._events ??= { close: undefined, error: undefined, diff --git a/lib/internal/streams/readable.js b/lib/internal/streams/readable.js index 1cba326970d4a8..f0d82b8e39a1e8 100644 --- a/lib/internal/streams/readable.js +++ b/lib/internal/streams/readable.js @@ -43,6 +43,7 @@ Readable.ReadableState = ReadableState; const EE = require('events'); const { Stream, prependListener } = require('internal/streams/legacy'); +const { kInitialFastPath } = require('internal/events/symbols'); const { Buffer } = require('buffer'); const { @@ -319,6 +320,10 @@ function Readable(options) { if (!(this instanceof Readable)) return new Readable(options); + if (this._events === undefined || this._events === null) { + this[kInitialFastPath] = true; + } + this._events ??= { close: undefined, error: undefined, diff --git a/lib/internal/streams/writable.js b/lib/internal/streams/writable.js index a039e60b16c2c2..caaaaf94d8cda6 100644 --- a/lib/internal/streams/writable.js +++ b/lib/internal/streams/writable.js @@ -44,6 +44,7 @@ const EE = require('events'); const Stream = require('internal/streams/legacy').Stream; const { Buffer } = require('buffer'); const destroyImpl = require('internal/streams/destroy'); +const { kInitialFastPath } = require('internal/events/symbols'); const { addAbortSignal, @@ -385,6 +386,9 @@ function Writable(options) { if (!(this instanceof Writable)) return new Writable(options); + if (this._events === undefined || this._events === null) { + this[kInitialFastPath] = true; + } this._events ??= { close: undefined, error: undefined, From 6d57e145c77799c6fb5251b46a6fa6de45acb155 Mon Sep 17 00:00:00 2001 From: Raz Luvaton <16746759+rluvaton@users.noreply.github.com> Date: Sun, 28 Apr 2024 16:17:43 +0300 Subject: [PATCH 12/69] events: fix switch to slow path when no impl exists --- lib/events.js | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/lib/events.js b/lib/events.js index ecfb7f2b51ada0..05fba7e4250d44 100644 --- a/lib/events.js +++ b/lib/events.js @@ -265,6 +265,8 @@ ObjectDefineProperty(EventEmitter, 'EventEmitterAsyncResource', { EventEmitter.errorMonitor = kErrorMonitor; function isEventUnsupportedForFastPath(type) { + // Not supporting newListener and removeListener events in fast path as they can add new listeners in the middle of the process + // and also not supporting errorMonitor event as it has special handling return type === 'newListener' || type === 'removeListener' || type === kErrorMonitor; } @@ -457,11 +459,11 @@ EventEmitter.init = function(opts) { if (opts?.captureRejections) { validateBoolean(opts.captureRejections, 'options.captureRejections'); - this[kCapture] = Boolean(opts.captureRejections); + this[kCaptureValue] = Boolean(opts.captureRejections); } else { // Assigning the kCapture property directly saves an expensive // prototype lookup in a very sensitive hot path. - this[kCapture] = EventEmitter.prototype[kCapture]; + this[kCaptureValue] = EventEmitter.prototype[kCapture]; } if (this[kCapture]) { @@ -495,15 +497,22 @@ EventEmitter.init = function(opts) { }; EventEmitter.prototype[kSwitchToSlowPath] = function() { - if (!this[kIsFastPath]) { + if (this[kIsFastPath] === false) { return; } + if(this[kIsFastPath] === true) { + this[kIsFastPath] = false; + + // TODO(rluvaton): remove this eslint ignore + // eslint-disable-next-line no-use-before-define + this[kImpl] = SlowEventEmitter.fromFastEventEmitter(this[kImpl]); + return; + } + + this[kImpl] = new SlowEventEmitter(this); this[kIsFastPath] = false; - // TODO(rluvaton): remove this eslint ignore - // eslint-disable-next-line no-use-before-define - this[kImpl] = SlowEventEmitter.fromFastEventEmitter(this[kImpl]); }; /** @@ -633,7 +642,7 @@ function _addListener(target, type, listener, prepend) { EventEmitter.prototype.addListener = function addListener(type, listener) { checkListener(listener); - // TODO - don't handle newListener and removeListener events in fast path for complexity reasons + // If the listener is already added, switch to slow path as the fast path optimized for single listener for each event if (this[kIsFastPath] && (isEventUnsupportedForFastPath(type) || this[kImpl].isListenerAlreadyExists(type))) { this[kSwitchToSlowPath](); } @@ -654,6 +663,7 @@ EventEmitter.prototype.prependListener = function prependListener(type, listener) { checkListener(listener); + // If the listener is already added, switch to slow path as the fast path optimized for single listener for each event if (this[kIsFastPath] && (isEventUnsupportedForFastPath(type) || this[kImpl].isListenerAlreadyExists(type))) { this[kSwitchToSlowPath](); } From c03587838a7466d872103740ffbab2c5dcdc3357 Mon Sep 17 00:00:00 2001 From: Raz Luvaton <16746759+rluvaton@users.noreply.github.com> Date: Sun, 28 Apr 2024 16:26:10 +0300 Subject: [PATCH 13/69] events: try to improve stream creation performance --- lib/events.js | 31 +++++++++++++++++-------------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/lib/events.js b/lib/events.js index 05fba7e4250d44..180d69a52cd2a5 100644 --- a/lib/events.js +++ b/lib/events.js @@ -328,10 +328,9 @@ ObjectDefineProperties(EventEmitter.prototype, { if (this[kImpl] === undefined && this[kInitialFastPath] === true) { // TODO(rluvaton): remove this eslint ignore // eslint-disable-next-line no-use-before-define - this[kImpl] = new FastEventEmitter(this); + this[kImpl] = new FastEventEmitter(this, arg); this[kIsFastPath] = true; this[kInitialFastPath] = undefined; - this[kImpl]._events = arg; return; } @@ -346,8 +345,9 @@ ObjectDefineProperties(EventEmitter.prototype, { // TODO(rluvaton): remove this eslint ignore // eslint-disable-next-line no-use-before-define - this[kImpl] = shouldBeFastPath ? new FastEventEmitter(this) : new SlowEventEmitter(this); + this[kImpl] = shouldBeFastPath ? new FastEventEmitter(this, arg) : new SlowEventEmitter(this, arg); this[kIsFastPath] = shouldBeFastPath; + return; } // TODO - might need to change to slow path @@ -466,7 +466,7 @@ EventEmitter.init = function(opts) { this[kCaptureValue] = EventEmitter.prototype[kCapture]; } - if (this[kCapture]) { + if (this[kCaptureValue]) { if (this[kIsFastPath]) { this[kSwitchToSlowPath](); } else if (!this[kImpl]) { @@ -479,21 +479,23 @@ EventEmitter.init = function(opts) { } // TODO(rluvaton): remove this eslint ignore // eslint-disable-next-line no-use-before-define - this[kImpl] ??= new FastEventEmitter(this, opts); + this[kImpl] ??= new FastEventEmitter(this); this[kIsFastPath] ??= true; + const thisEvents = this._events; + + // TODO - update this - if (this._events === undefined || - this._events === ObjectGetPrototypeOf(this)._events) { - this._events = { __proto__: null }; - this._eventsCount = 0; + if (thisEvents === undefined || + thisEvents === ObjectGetPrototypeOf(this)._events) { + this[kImpl]._events = { __proto__: null }; + this[kImpl]._eventsCount = 0; this[kImpl][kShapeMode] = false; } else { this[kImpl][kShapeMode] = true; } this._maxListeners = this._maxListeners || undefined; - this[kInitialFastPath] = undefined; }; EventEmitter.prototype[kSwitchToSlowPath] = function() { @@ -1200,8 +1202,9 @@ class FastEventEmitter { eventEmitterTranslationLayer = undefined; [kShapeMode] = undefined; - constructor(eventEmitterTranslationLayer) { + constructor(eventEmitterTranslationLayer, _events = undefined) { this.eventEmitterTranslationLayer = eventEmitterTranslationLayer; + this._events = _events; } /** @@ -1396,18 +1399,18 @@ class SlowEventEmitter { * @param {FastEventEmitter} fastEventEmitter */ static fromFastEventEmitter(fastEventEmitter) { - const eventEmitter = new SlowEventEmitter(fastEventEmitter.eventEmitterTranslationLayer); + const eventEmitter = new SlowEventEmitter(fastEventEmitter.eventEmitterTranslationLayer, fastEventEmitter._events); // TODO - add missing - eventEmitter._events = fastEventEmitter._events; eventEmitter._eventsCount = fastEventEmitter._eventsCount; eventEmitter[kShapeMode] = fastEventEmitter[kShapeMode]; return eventEmitter; } - constructor(eventEmitterTranslationLayer) { + constructor(eventEmitterTranslationLayer, events = undefined) { this.eventEmitterTranslationLayer = eventEmitterTranslationLayer; + this._events = events; } /** From de6dfd7c078773d7ca992dd7e209ec6a0ad0e879 Mon Sep 17 00:00:00 2001 From: Raz Luvaton <16746759+rluvaton@users.noreply.github.com> Date: Sun, 28 Apr 2024 16:27:03 +0300 Subject: [PATCH 14/69] events: fix lint errors --- lib/events.js | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/lib/events.js b/lib/events.js index 180d69a52cd2a5..4b22fdaec529b9 100644 --- a/lib/events.js +++ b/lib/events.js @@ -265,7 +265,8 @@ ObjectDefineProperty(EventEmitter, 'EventEmitterAsyncResource', { EventEmitter.errorMonitor = kErrorMonitor; function isEventUnsupportedForFastPath(type) { - // Not supporting newListener and removeListener events in fast path as they can add new listeners in the middle of the process + // Not supporting newListener and removeListener events in fast path + // as they can add new listeners in the middle of the process // and also not supporting errorMonitor event as it has special handling return type === 'newListener' || type === 'removeListener' || type === kErrorMonitor; } @@ -503,7 +504,7 @@ EventEmitter.prototype[kSwitchToSlowPath] = function() { return; } - if(this[kIsFastPath] === true) { + if (this[kIsFastPath] === true) { this[kIsFastPath] = false; // TODO(rluvaton): remove this eslint ignore @@ -512,6 +513,8 @@ EventEmitter.prototype[kSwitchToSlowPath] = function() { return; } + // TODO(rluvaton): remove this eslint ignore + // eslint-disable-next-line no-use-before-define this[kImpl] = new SlowEventEmitter(this); this[kIsFastPath] = false; @@ -644,7 +647,8 @@ function _addListener(target, type, listener, prepend) { EventEmitter.prototype.addListener = function addListener(type, listener) { checkListener(listener); - // If the listener is already added, switch to slow path as the fast path optimized for single listener for each event + // If the listener is already added, + // switch to slow path as the fast path optimized for single listener for each event if (this[kIsFastPath] && (isEventUnsupportedForFastPath(type) || this[kImpl].isListenerAlreadyExists(type))) { this[kSwitchToSlowPath](); } @@ -665,7 +669,8 @@ EventEmitter.prototype.prependListener = function prependListener(type, listener) { checkListener(listener); - // If the listener is already added, switch to slow path as the fast path optimized for single listener for each event + // If the listener is already added, + // switch to slow path as the fast path optimized for single listener for each event if (this[kIsFastPath] && (isEventUnsupportedForFastPath(type) || this[kImpl].isListenerAlreadyExists(type))) { this[kSwitchToSlowPath](); } From 99b9a1d69380b23f2544e571d5fd49073db6df9c Mon Sep 17 00:00:00 2001 From: Raz Luvaton <16746759+rluvaton@users.noreply.github.com> Date: Sun, 28 Apr 2024 18:20:09 +0300 Subject: [PATCH 15/69] events: add comments and remove custom handling for newListener and listener removed --- lib/events.js | 114 +++++++++++++------------------------------------- 1 file changed, 28 insertions(+), 86 deletions(-) diff --git a/lib/events.js b/lib/events.js index 4b22fdaec529b9..55c53826108711 100644 --- a/lib/events.js +++ b/lib/events.js @@ -457,7 +457,6 @@ EventEmitter.setMaxListeners = // If you're updating this function definition, please also update any // re-definitions, such as the one in the Domain module (lib/domain.js). EventEmitter.init = function(opts) { - if (opts?.captureRejections) { validateBoolean(opts.captureRejections, 'options.captureRejections'); this[kCaptureValue] = Boolean(opts.captureRejections); @@ -467,17 +466,6 @@ EventEmitter.init = function(opts) { this[kCaptureValue] = EventEmitter.prototype[kCapture]; } - if (this[kCaptureValue]) { - if (this[kIsFastPath]) { - this[kSwitchToSlowPath](); - } else if (!this[kImpl]) { - - // TODO(rluvaton): remove this eslint ignore - // eslint-disable-next-line no-use-before-define - this[kImpl] = new SlowEventEmitter(this); - this[kIsFastPath] = false; - } - } // TODO(rluvaton): remove this eslint ignore // eslint-disable-next-line no-use-before-define this[kImpl] ??= new FastEventEmitter(this); @@ -1194,13 +1182,17 @@ function listenersController() { // TODO - should have the same API as slow_event_emitter -// TODO - not supporting -// 1. kCapture - false by default -// 2. _maxListeners (but still need to save the data), should be saved on the parent -// 3. kErrorMonitor - undefined by default -// TODO - add comment for what this is optimized for +/** + * This class is optimized for the case where there is only a single listener for each event. + * it support kCapture + * but does not support event types with the following names: + * 1. 'newListener' and 'removeListener' as they can cause another event to be added and making the code less predictable thus harder to optimize + * 2. kErrorMonitor as it has special handling + */ class FastEventEmitter { - // TODO - add comment on how events are stored + /** + * The events are stored here as Record + */ _events = undefined; _eventsCount; // TODO - convert to symbol and rename @@ -1234,8 +1226,14 @@ class FastEventEmitter { return false; } - // TODO - change - handler.apply(this.eventEmitterTranslationLayer, args); + const result = handler.apply(this.eventEmitterTranslationLayer, args); + + // We check if result is undefined first because that + // is the most common case so we do not pay any perf + // penalty + if (result !== undefined && result !== null) { + addCatch(this.eventEmitterTranslationLayer, result, type, args); + } return true; } @@ -1261,44 +1259,13 @@ class FastEventEmitter { __proto__: null, }; this._eventsCount = 0; - } else { - // TODO(rluvaton): support addListener and removeListener events - // // To avoid recursion in the case that type === "newListener"! Before - // // adding it to the listeners, first emit "newListener". - // if (events.newListener !== undefined) { - // // TODO(rluvaton): use apply to pass the parent eventEmitter - // this.emit('newListener', type, - // listener.listener ?? listener); - // - // - // // TODO - here we could already have aq listener to the same value so need to switch to slow mode - // - // if(this.stale) { - // // TODO - continue in the slow mode - // this.eventEmitterTranslationLayer[kContinueInSlowMode](type, listener, prepend); - // return; - // } - // - // if(this._events[type]) { - // // TODO(rluvaton): try to find a better way - // this.stale = true; - // this.eventEmitterTranslationLayer[kContinueInSlowMode](type, listener, prepend); - // - // // TODO(rluvaton): continue in the slow mode - // return; - // } - // - // // Re-assign `events` because a newListener handler could have caused the - // // this._events to be assigned to a new object - // // TODO(rluvaton): change this - // events = this._events; - // } + + // Not emitting `newListener` here as in fast path we don't have it } // Optimize the case of one listener. Don't need the extra array object. events[type] = listener; ++this._eventsCount; - } /** @@ -1325,10 +1292,7 @@ class FastEventEmitter { this._events = { __proto__: null }; } else { delete events[type]; - // TODO(rluvaton): support addListener and removeListener events - // if (events.removeListener) - // // TODO(rluvaton): use apply to pass the parent eventEmitter - // this.emit('removeListener', type, list.listener || listener); + // Not emitting `removeListener` here as in fast path we don't have it } } @@ -1343,41 +1307,19 @@ class FastEventEmitter { if (events === undefined) return undefined; - // Not listening for removeListener, no need to emit - if (events.removeListener === undefined) { - if (arguments.length === 0) { - this._events = { __proto__: null }; - this._eventsCount = 0; - } else if (events[type] !== undefined) { - if (--this._eventsCount === 0) - this._events = { __proto__: null }; - else - delete events[type]; - } - this[kShapeMode] = false; - - return undefined; - } - - // Emit removeListener for all listeners on all events if (arguments.length === 0) { - for (const key of ReflectOwnKeys(events)) { - if (key === 'removeListener') continue; - this.removeAllListeners(key); - } - this.removeAllListeners('removeListener'); this._events = { __proto__: null }; this._eventsCount = 0; - this[kShapeMode] = false; - - return undefined; + } else if (events[type] !== undefined) { + if (--this._eventsCount === 0) + this._events = { __proto__: null }; + else + delete events[type]; } - const listeners = events[type]; + this[kShapeMode] = false; - if (typeof listeners === 'function') { - this.removeListener(type, listeners); - } + return undefined; } } From 245172dec97c57b4ecce17ffdcbac2bfe8d771c6 Mon Sep 17 00:00:00 2001 From: Raz Luvaton <16746759+rluvaton@users.noreply.github.com> Date: Sun, 28 Apr 2024 18:22:07 +0300 Subject: [PATCH 16/69] events: remove comments in code --- lib/events.js | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/lib/events.js b/lib/events.js index 55c53826108711..ce47bb3c8b71c6 100644 --- a/lib/events.js +++ b/lib/events.js @@ -1186,7 +1186,8 @@ function listenersController() { * This class is optimized for the case where there is only a single listener for each event. * it support kCapture * but does not support event types with the following names: - * 1. 'newListener' and 'removeListener' as they can cause another event to be added and making the code less predictable thus harder to optimize + * 1. 'newListener' and 'removeListener' as they can cause another event to be added + * and making the code less predictable thus harder to optimize * 2. kErrorMonitor as it has special handling */ class FastEventEmitter { @@ -1328,13 +1329,7 @@ class FastEventEmitter { // TODO - should have the same API as slow_event_emitter -// TODO - not supporting -// 2. _maxListeners (but still need to save the data), should be saved on the parent -// 3. kErrorMonitor - undefined by default -// TODO - add comment for what this is optimized for class SlowEventEmitter { - // TODO - have a way to support passing _events - // TODO - add comment on how events are stored _events = undefined; _eventsCount = 0; // TODO - convert to symbol and rename @@ -1438,7 +1433,6 @@ class SlowEventEmitter { // To avoid recursion in the case that type === "newListener"! Before // adding it to the listeners, first emit "newListener". if (events.newListener !== undefined) { - // TODO - emit this to be the eventEmitterTranslationLayer this.eventEmitterTranslationLayer.emit('newListener', type, listener.listener ?? listener); @@ -1466,7 +1460,6 @@ class SlowEventEmitter { } // Check for listener leak - // TODO - move away from this m = _getMaxListeners(target); if (m > 0 && existing.length > m && !existing.warned) { existing.warned = true; @@ -1619,7 +1612,6 @@ function addCatch(that, promise, type, args) { } function emitUnhandledRejectionOrErr(ee, err, type, args) { - // TODO - kRejection should move to parent if (typeof ee.eventEmitterTranslationLayer[kRejection] === 'function') { ee.eventEmitterTranslationLayer[kRejection](err, type, ...args); } else { From 33d58944c4a9bfaac8c011f02dae64b8c34ff670 Mon Sep 17 00:00:00 2001 From: Raz Luvaton <16746759+rluvaton@users.noreply.github.com> Date: Mon, 29 Apr 2024 23:41:56 +0300 Subject: [PATCH 17/69] events: init when calling on before event initialized --- lib/events.js | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/lib/events.js b/lib/events.js index ce47bb3c8b71c6..232bcdd46e82ea 100644 --- a/lib/events.js +++ b/lib/events.js @@ -281,7 +281,7 @@ ObjectDefineProperties(EventEmitter.prototype, { }, [kIsFastPath]: { __proto__: null, - value: true, + value: undefined, enumerable: false, configurable: false, writable: true, @@ -635,6 +635,13 @@ function _addListener(target, type, listener, prepend) { EventEmitter.prototype.addListener = function addListener(type, listener) { checkListener(listener); + // This can happen in TLSSocket, where we listen for close event before + // the EventEmitter was initiated + // TODO(rluvaton): check how it worked before? + if (!this[kImpl]) { + EventEmitter.init.apply(this); + } + // If the listener is already added, // switch to slow path as the fast path optimized for single listener for each event if (this[kIsFastPath] && (isEventUnsupportedForFastPath(type) || this[kImpl].isListenerAlreadyExists(type))) { @@ -657,6 +664,13 @@ EventEmitter.prototype.prependListener = function prependListener(type, listener) { checkListener(listener); + // This can happen in TLSSocket, where we listen for close event before + // the EventEmitter was initiated + // TODO(rluvaton): check how it worked before? + if (!this[kImpl]) { + EventEmitter.init.apply(this); + } + // If the listener is already added, // switch to slow path as the fast path optimized for single listener for each event if (this[kIsFastPath] && (isEventUnsupportedForFastPath(type) || this[kImpl].isListenerAlreadyExists(type))) { From c25e14a2a23d0dd54edf1959444280647205cf3e Mon Sep 17 00:00:00 2001 From: Raz Luvaton <16746759+rluvaton@users.noreply.github.com> Date: Mon, 29 Apr 2024 23:46:39 +0300 Subject: [PATCH 18/69] events: remove kCapture value --- lib/events.js | 49 ++++++++++++++++++++++--------------------------- 1 file changed, 22 insertions(+), 27 deletions(-) diff --git a/lib/events.js b/lib/events.js index 232bcdd46e82ea..1d7b4896e5374e 100644 --- a/lib/events.js +++ b/lib/events.js @@ -98,7 +98,6 @@ const kMaxEventTargetListenersWarned = const kWatermarkData = SymbolFor('nodejs.watermarkData'); const kImpl = Symbol('kImpl'); -const kCaptureValue = Symbol('kCaptureValue'); const kIsFastPath = Symbol('kIsFastPath'); const kSwitchToSlowPath = Symbol('kSwitchToSlowPath'); @@ -264,6 +263,14 @@ ObjectDefineProperty(EventEmitter, 'EventEmitterAsyncResource', { EventEmitter.errorMonitor = kErrorMonitor; +// The default for captureRejections is false +ObjectDefineProperty(EventEmitter.prototype, kCapture, { + __proto__: null, + value: false, + writable: true, + enumerable: false, +}); + function isEventUnsupportedForFastPath(type) { // Not supporting newListener and removeListener events in fast path // as they can add new listeners in the middle of the process @@ -293,28 +300,15 @@ ObjectDefineProperties(EventEmitter.prototype, { configurable: false, writable: true, }, - [kCaptureValue]: { - __proto__: null, - value: false, - enumerable: false, - configurable: false, - writable: true, - }, // The default for captureRejections is false + [kCapture]: { __proto__: null, - get() { - return this[kCaptureValue]; - }, - set(value) { - this[kCaptureValue] = value; - - if (value) { - this[kSwitchToSlowPath](); - } - }, + value: false, enumerable: false, + configurable: false, + writable: true, }, _events: { __proto__: null, @@ -457,15 +451,6 @@ EventEmitter.setMaxListeners = // If you're updating this function definition, please also update any // re-definitions, such as the one in the Domain module (lib/domain.js). EventEmitter.init = function(opts) { - if (opts?.captureRejections) { - validateBoolean(opts.captureRejections, 'options.captureRejections'); - this[kCaptureValue] = Boolean(opts.captureRejections); - } else { - // Assigning the kCapture property directly saves an expensive - // prototype lookup in a very sensitive hot path. - this[kCaptureValue] = EventEmitter.prototype[kCapture]; - } - // TODO(rluvaton): remove this eslint ignore // eslint-disable-next-line no-use-before-define this[kImpl] ??= new FastEventEmitter(this); @@ -485,6 +470,16 @@ EventEmitter.init = function(opts) { } this._maxListeners = this._maxListeners || undefined; + + + if (opts?.captureRejections) { + validateBoolean(opts.captureRejections, 'options.captureRejections'); + this[kCapture] = Boolean(opts.captureRejections); + } else { + // Assigning the kCapture property directly saves an expensive + // prototype lookup in a very sensitive hot path. + this[kCapture] = EventEmitter.prototype[kCapture]; + } }; EventEmitter.prototype[kSwitchToSlowPath] = function() { From 4d74562502ff08bfcbb21aab0c45bc7e386b8273 Mon Sep 17 00:00:00 2001 From: Raz Luvaton <16746759+rluvaton@users.noreply.github.com> Date: Mon, 29 Apr 2024 23:47:07 +0300 Subject: [PATCH 19/69] events: remove double init of kCapture --- lib/events.js | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/lib/events.js b/lib/events.js index 1d7b4896e5374e..190db77590fa4b 100644 --- a/lib/events.js +++ b/lib/events.js @@ -300,16 +300,6 @@ ObjectDefineProperties(EventEmitter.prototype, { configurable: false, writable: true, }, - - // The default for captureRejections is false - - [kCapture]: { - __proto__: null, - value: false, - enumerable: false, - configurable: false, - writable: true, - }, _events: { __proto__: null, enumerable: true, From d8cd35d63dbb245c6c1157cc181218c56ce23214 Mon Sep 17 00:00:00 2001 From: Raz Luvaton <16746759+rluvaton@users.noreply.github.com> Date: Wed, 1 May 2024 20:10:51 +0300 Subject: [PATCH 20/69] events: fix the performance penalty of dict object --- lib/events.js | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/lib/events.js b/lib/events.js index 190db77590fa4b..75ff95e230d2d5 100644 --- a/lib/events.js +++ b/lib/events.js @@ -452,7 +452,9 @@ EventEmitter.init = function(opts) { // TODO - update this if (thisEvents === undefined || thisEvents === ObjectGetPrototypeOf(this)._events) { - this[kImpl]._events = { __proto__: null }; + // In fast path we don't want to have __proto__: null as it will cause the object to be in dictionary mode + // and will slow down the access to the properties by a lot (around 2x) + this[kImpl]._events = this[kIsFastPath] ? {} : { __proto__: null }; this[kImpl]._eventsCount = 0; this[kImpl][kShapeMode] = false; } else { @@ -1254,10 +1256,7 @@ class FastEventEmitter { events = this._events; // TODO - simplify this if (events === undefined) { - events = this._events = { - // TODO - change this? - __proto__: null, - }; + events = this._events = {}; this._eventsCount = 0; // Not emitting `newListener` here as in fast path we don't have it @@ -1288,8 +1287,7 @@ class FastEventEmitter { if (this[kShapeMode]) { events[type] = undefined; } else if (this._eventsCount === 0) { - // TODO - keep this? - this._events = { __proto__: null }; + this._events = { }; } else { delete events[type]; // Not emitting `removeListener` here as in fast path we don't have it @@ -1308,11 +1306,11 @@ class FastEventEmitter { return undefined; if (arguments.length === 0) { - this._events = { __proto__: null }; + this._events = { }; this._eventsCount = 0; } else if (events[type] !== undefined) { if (--this._eventsCount === 0) - this._events = { __proto__: null }; + this._events = { }; else delete events[type]; } From bb34b2647467075d2c4132b3ded89e208775503b Mon Sep 17 00:00:00 2001 From: Raz Luvaton <16746759+rluvaton@users.noreply.github.com> Date: Wed, 1 May 2024 20:29:38 +0300 Subject: [PATCH 21/69] events: moved slow event emitter to own file --- lib/events.js | 432 +----------------- lib/internal/events/fast_event_emitter.js | 3 + .../events/shared_internal_event_emitter.js | 154 +++++++ lib/internal/events/slow_event_emitter.js | 282 ++++++++++++ lib/internal/events/symbols.js | 22 + 5 files changed, 483 insertions(+), 410 deletions(-) create mode 100644 lib/internal/events/fast_event_emitter.js create mode 100644 lib/internal/events/shared_internal_event_emitter.js create mode 100644 lib/internal/events/slow_event_emitter.js diff --git a/lib/events.js b/lib/events.js index 75ff95e230d2d5..fed3f8ffb73545 100644 --- a/lib/events.js +++ b/lib/events.js @@ -22,16 +22,11 @@ 'use strict'; const { - ArrayPrototypeJoin, ArrayPrototypePop, ArrayPrototypePush, - ArrayPrototypeSlice, - ArrayPrototypeSplice, ArrayPrototypeUnshift, Boolean, Error, - ErrorCaptureStackTrace, - FunctionPrototypeBind, FunctionPrototypeCall, NumberMAX_SAFE_INTEGER, ObjectDefineProperties, @@ -44,8 +39,6 @@ const { PromiseResolve, ReflectApply, ReflectOwnKeys, - String, - StringPrototypeSplit, Symbol, SymbolAsyncIterator, SymbolDispose, @@ -55,12 +48,7 @@ const kRejection = SymbolFor('nodejs.rejection'); const { kEmptyObject } = require('internal/util'); -const { - inspect, - identicalSequenceRange, -} = require('internal/util/inspect'); -let spliceOne; let FixedQueue; let kFirstEventParam; let kResistStopPropagation; @@ -70,10 +58,7 @@ const { codes: { ERR_INVALID_ARG_TYPE, ERR_INVALID_THIS, - ERR_UNHANDLED_ERROR, }, - genericNodeError, - kEnhanceStackBeforeInspector, } = require('internal/errors'); const { @@ -86,20 +71,27 @@ const { validateString, } = require('internal/validators'); const { addAbortListener } = require('internal/events/abort_listener'); -const { kInitialFastPath } = require('internal/events/symbols'); - - -const kCapture = Symbol('kCapture'); -const kErrorMonitor = Symbol('events.errorMonitor'); -const kShapeMode = Symbol('shapeMode'); -const kMaxEventTargetListeners = Symbol('events.maxEventTargetListeners'); -const kMaxEventTargetListenersWarned = - Symbol('events.maxEventTargetListenersWarned'); -const kWatermarkData = SymbolFor('nodejs.watermarkData'); +const { + kInitialFastPath, + kCapture, + kErrorMonitor, + kShapeMode, + kMaxEventTargetListeners, + kMaxEventTargetListenersWarned, + kWatermarkData, + kImpl, + kIsFastPath, + kSwitchToSlowPath, + kEvents, +} = require('internal/events/symbols'); +const { + arrayClone, + addCatch, + throwErrorOnMissingErrorHandler, +} = require('internal/events/shared_internal_event_emitter'); +const { SlowEventEmitter } = require('internal/events/slow_event_emitter'); +// Const { } = require('internal/events/fast_event_emitter'); -const kImpl = Symbol('kImpl'); -const kIsFastPath = Symbol('kIsFastPath'); -const kSwitchToSlowPath = Symbol('kSwitchToSlowPath'); let EventEmitterAsyncResource; // The EventEmitterAsyncResource has to be initialized lazily because event.js @@ -443,7 +435,7 @@ EventEmitter.setMaxListeners = EventEmitter.init = function(opts) { // TODO(rluvaton): remove this eslint ignore // eslint-disable-next-line no-use-before-define - this[kImpl] ??= new FastEventEmitter(this); + this[kImpl] ??= new FastEventEmitter(this, opts?.[kEvents]); this[kIsFastPath] ??= true; const thisEvents = this._events; @@ -520,30 +512,6 @@ EventEmitter.prototype.getMaxListeners = function getMaxListeners() { return _getMaxListeners(this); }; -function enhanceStackTrace(err, own) { - let ctorInfo = ''; - try { - const { name } = this.constructor; - if (name !== 'EventEmitter') - ctorInfo = ` on ${name} instance`; - } catch { - // Continue regardless of error. - } - const sep = `\nEmitted 'error' event${ctorInfo} at:\n`; - - const errStack = ArrayPrototypeSlice( - StringPrototypeSplit(err.stack, '\n'), 1); - const ownStack = ArrayPrototypeSlice( - StringPrototypeSplit(own.stack, '\n'), 1); - - const { len, offset } = identicalSequenceRange(ownStack, errStack); - if (len > 0) { - ArrayPrototypeSplice(ownStack, offset + 1, len - 2, - ' [... lines matching original stack trace ...]'); - } - - return err.stack + sep + ArrayPrototypeJoin(ownStack, '\n'); -} /** * Synchronously calls each of the listeners registered @@ -845,19 +813,6 @@ EventEmitter.prototype.eventNames = function eventNames() { return this._eventsCount > 0 ? ReflectOwnKeys(this._events) : []; }; -function arrayClone(arr) { - // At least since V8 8.3, this implementation is faster than the previous - // which always used a simple for-loop - switch (arr.length) { - case 2: return [arr[0], arr[1]]; - case 3: return [arr[0], arr[1], arr[2]]; - case 4: return [arr[0], arr[1], arr[2], arr[3]]; - case 5: return [arr[0], arr[1], arr[2], arr[3], arr[4]]; - case 6: return [arr[0], arr[1], arr[2], arr[3], arr[4], arr[5]]; - } - return ArrayPrototypeSlice(arr); -} - function unwrapListeners(arr) { const ret = arrayClone(arr); for (let i = 0; i < ret.length; ++i) { @@ -1323,349 +1278,6 @@ class FastEventEmitter { // --------------- SLOW - -// TODO - should have the same API as slow_event_emitter - -class SlowEventEmitter { - _events = undefined; - _eventsCount = 0; - // TODO - convert to symbol and rename - eventEmitterTranslationLayer = undefined; - [kShapeMode] = undefined; - - /** - * - * @param {FastEventEmitter} fastEventEmitter - */ - static fromFastEventEmitter(fastEventEmitter) { - const eventEmitter = new SlowEventEmitter(fastEventEmitter.eventEmitterTranslationLayer, fastEventEmitter._events); - - // TODO - add missing - eventEmitter._eventsCount = fastEventEmitter._eventsCount; - eventEmitter[kShapeMode] = fastEventEmitter[kShapeMode]; - - return eventEmitter; - } - - constructor(eventEmitterTranslationLayer, events = undefined) { - this.eventEmitterTranslationLayer = eventEmitterTranslationLayer; - this._events = events; - } - - /** - * Synchronously calls each of the listeners registered - * for the event. - * @param {string | symbol} type - * @param {...any} [args] - * @returns {boolean} - */ - emit(type, ...args) { - let doError = (type === 'error'); - const eventEmitterTranslationLayer = this.eventEmitterTranslationLayer; - - const events = this._events; - if (events !== undefined) { - if (doError && events[kErrorMonitor] !== undefined) - this.emit(kErrorMonitor, ...args); - doError = (doError && events.error === undefined); - } else if (!doError) - return false; - - // If there is no 'error' event listener then throw. - if (doError) { - throwErrorOnMissingErrorHandler.apply(eventEmitterTranslationLayer, args); - } - - const handler = events[type]; - - if (handler === undefined) - return false; - - if (typeof handler === 'function') { - const result = handler.apply(eventEmitterTranslationLayer, args); - - // We check if result is undefined first because that - // is the most common case so we do not pay any perf - // penalty - if (result !== undefined && result !== null) { - addCatch(eventEmitterTranslationLayer, result, type, args); - } - } else { - const len = handler.length; - const listeners = arrayClone(handler); - for (let i = 0; i < len; ++i) { - const result = listeners[i].apply(eventEmitterTranslationLayer, args); - - // We check if result is undefined first because that - // is the most common case so we do not pay any perf - // penalty. - // This code is duplicated because extracting it away - // would make it non-inlineable. - if (result !== undefined && result !== null) { - addCatch(eventEmitterTranslationLayer, result, type, args); - } - } - } - - return true; - } - - /** - * Adds a listener to the event emitter. - * @param {string | symbol} type - * @param {Function} listener - * @returns {EventEmitter} - */ - addListener(type, listener, prepend) { - const target = this.eventEmitterTranslationLayer; - let m; - let events; - let existing; - - events = this._events; - if (events === undefined) { - events = this._events = { __proto__: null }; - this._eventsCount = 0; - } else { - // To avoid recursion in the case that type === "newListener"! Before - // adding it to the listeners, first emit "newListener". - if (events.newListener !== undefined) { - this.eventEmitterTranslationLayer.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 = this._events; - } - existing = events[type]; - } - - if (existing === undefined) { - // Optimize the case of one listener. Don't need the extra array object. - events[type] = listener; - ++this._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); - } - - // Check for listener leak - m = _getMaxListeners(target); - if (m > 0 && existing.length > m && !existing.warned) { - existing.warned = true; - // No error code for this since it is a Warning - const w = genericNodeError( - `Possible EventEmitter memory leak detected. ${existing.length} ${String(type)} listeners ` + - `added to ${inspect(target, { depth: -1 })}. Use emitter.setMaxListeners() to increase limit`, - { name: 'MaxListenersExceededWarning', emitter: target, type: type, count: existing.length }); - process.emitWarning(w); - } - } - - return this; - } - - /** - * Removes the specified `listener`. - * @param {string | symbol} type - * @param {Function} listener - * @returns {EventEmitter} - */ - removeListener(type, listener) { - const events = this._events; - if (events === undefined) - return undefined; - - const list = events[type]; - if (list === undefined) - return undefined; - - if (list === listener || list.listener === listener) { - this._eventsCount -= 1; - - if (this[kShapeMode]) { - events[type] = undefined; - } else if (this._eventsCount === 0) { - this._events = { __proto__: null }; - } else { - delete events[type]; - if (events.removeListener) - this.emit('removeListener', type, list.listener || listener); - } - } else if (typeof list !== 'function') { - let position = -1; - - for (let i = list.length - 1; i >= 0; i--) { - if (list[i] === listener || list[i].listener === listener) { - position = i; - break; - } - } - - if (position < 0) - return this; - - if (position === 0) - list.shift(); - 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, listener); - } - - return undefined; - } - - /** - * Removes all listeners from the event emitter. (Only - * removes listeners for a specific event name if specified - * as `type`). - * @param {string | symbol} [type] - * @returns {EventEmitter} - */ - removeAllListeners(type) { - const events = this._events; - if (events === undefined) - return undefined; - - // Not listening for removeListener, no need to emit - if (events.removeListener === undefined) { - if (arguments.length === 0) { - this._events = { __proto__: null }; - this._eventsCount = 0; - } else if (events[type] !== undefined) { - if (--this._eventsCount === 0) - this._events = { __proto__: null }; - else - delete events[type]; - } - this[kShapeMode] = false; - return undefined; - } - - // Emit removeListener for all listeners on all events - if (arguments.length === 0) { - for (const key of ReflectOwnKeys(events)) { - if (key === 'removeListener') continue; - this.removeAllListeners(key); - } - this.removeAllListeners('removeListener'); - this._events = { __proto__: null }; - this._eventsCount = 0; - this[kShapeMode] = false; - return undefined; - } - - const listeners = events[type]; - - if (typeof listeners === 'function') { - this.removeListener(type, listeners); - } else if (listeners !== undefined) { - // LIFO order - for (let i = listeners.length - 1; i >= 0; i--) { - this.removeListener(type, listeners[i]); - } - } - - return undefined; - } -} - - -function addCatch(that, promise, type, args) { - if (!that[kCapture]) { - return; - } - - // Handle Promises/A+ spec, then could be a getter - // that throws on second use. - try { - const then = promise.then; - - if (typeof then === 'function') { - then.call(promise, undefined, function(err) { - // The callback is called with nextTick to avoid a follow-up - // rejection from this promise. - process.nextTick(emitUnhandledRejectionOrErr, that, err, type, args); - }); - } - } catch (err) { - that.emit('error', err); - } -} - -function emitUnhandledRejectionOrErr(ee, err, type, args) { - if (typeof ee.eventEmitterTranslationLayer[kRejection] === 'function') { - ee.eventEmitterTranslationLayer[kRejection](err, type, ...args); - } else { - // We have to disable the capture rejections mechanism, otherwise - // we might end up in an infinite loop. - const prev = ee.eventEmitterTranslationLayer[kCapture]; - - // If the error handler throws, it is not catchable and it - // will end up in 'uncaughtException'. We restore the previous - // value of kCapture in case the uncaughtException is present - // and the exception is handled. - try { - ee.eventEmitterTranslationLayer[kCapture] = false; - ee.emit('error', err); - } finally { - ee.eventEmitterTranslationLayer[kCapture] = prev; - } - } -} +// ------ todo // ------- Internal helpers - - -// TODO - move this to a different file -// TODO - rename -function throwErrorOnMissingErrorHandler(...args) { - let er; - if (args.length > 0) - er = args[0]; - - if (er instanceof Error) { - try { - const capture = {}; - ErrorCaptureStackTrace(capture, EventEmitter.prototype.emit); - ObjectDefineProperty(er, kEnhanceStackBeforeInspector, { - __proto__: null, - value: FunctionPrototypeBind(enhanceStackTrace, this, er, capture), - configurable: true, - }); - } catch { - // Continue regardless of error. - } - - // Note: The comments on the `throw` lines are intentional, they show - // up in Node's output if this results in an unhandled exception. - throw er; // Unhandled 'error' event - } - - let stringifiedEr; - try { - stringifiedEr = inspect(er); - } catch { - stringifiedEr = er; - } - - // At least give some kind of context to the user - const err = new ERR_UNHANDLED_ERROR(stringifiedEr); - err.context = er; - throw err; // Unhandled 'error' event -} diff --git a/lib/internal/events/fast_event_emitter.js b/lib/internal/events/fast_event_emitter.js new file mode 100644 index 00000000000000..8b46fbbaadf6b0 --- /dev/null +++ b/lib/internal/events/fast_event_emitter.js @@ -0,0 +1,3 @@ +'use strict'; + +module.exports = {}; diff --git a/lib/internal/events/shared_internal_event_emitter.js b/lib/internal/events/shared_internal_event_emitter.js new file mode 100644 index 00000000000000..6487ed627528e8 --- /dev/null +++ b/lib/internal/events/shared_internal_event_emitter.js @@ -0,0 +1,154 @@ +'use strict'; + +const { + ArrayPrototypeJoin, + ArrayPrototypeSlice, + ArrayPrototypeSplice, + Error, + ErrorCaptureStackTrace, + FunctionPrototypeBind, + ObjectDefineProperty, + StringPrototypeSplit, +} = primordials; + +const { kCapture, kRejection } = require('internal/events/symbols'); +const { inspect, identicalSequenceRange } = require('internal/util/inspect'); + +const { + codes: { + ERR_UNHANDLED_ERROR, + }, + kEnhanceStackBeforeInspector, +} = require('internal/errors'); + +let EventEmitter; + +function arrayClone(arr) { + // At least since V8 8.3, this implementation is faster than the previous + // which always used a simple for-loop + switch (arr.length) { + case 2: return [arr[0], arr[1]]; + case 3: return [arr[0], arr[1], arr[2]]; + case 4: return [arr[0], arr[1], arr[2], arr[3]]; + case 5: return [arr[0], arr[1], arr[2], arr[3], arr[4]]; + case 6: return [arr[0], arr[1], arr[2], arr[3], arr[4], arr[5]]; + } + return ArrayPrototypeSlice(arr); +} + + +function addCatch(that, promise, type, args) { + if (!that[kCapture]) { + return; + } + + // Handle Promises/A+ spec, then could be a getter + // that throws on second use. + try { + const then = promise.then; + + if (typeof then === 'function') { + then.call(promise, undefined, function(err) { + // The callback is called with nextTick to avoid a follow-up + // rejection from this promise. + process.nextTick(emitUnhandledRejectionOrErr, that, err, type, args); + }); + } + } catch (err) { + that.emit('error', err); + } +} + +function emitUnhandledRejectionOrErr(ee, err, type, args) { + if (typeof ee.eventEmitterTranslationLayer[kRejection] === 'function') { + ee.eventEmitterTranslationLayer[kRejection](err, type, ...args); + } else { + // We have to disable the capture rejections mechanism, otherwise + // we might end up in an infinite loop. + const prev = ee.eventEmitterTranslationLayer[kCapture]; + + // If the error handler throws, it is not catchable and it + // will end up in 'uncaughtException'. We restore the previous + // value of kCapture in case the uncaughtException is present + // and the exception is handled. + try { + ee.eventEmitterTranslationLayer[kCapture] = false; + ee.emit('error', err); + } finally { + ee.eventEmitterTranslationLayer[kCapture] = prev; + } + } +} + + +// TODO - move this to a different file +// TODO - rename +function throwErrorOnMissingErrorHandler(...args) { + let er; + if (args.length > 0) + er = args[0]; + + if (er instanceof Error) { + try { + const capture = {}; + // TODO + EventEmitter ??= require('events'); + ErrorCaptureStackTrace(capture, EventEmitter.prototype.emit); + ObjectDefineProperty(er, kEnhanceStackBeforeInspector, { + __proto__: null, + value: FunctionPrototypeBind(enhanceStackTrace, this, er, capture), + configurable: true, + }); + } catch { + // Continue regardless of error. + } + + // Note: The comments on the `throw` lines are intentional, they show + // up in Node's output if this results in an unhandled exception. + throw er; // Unhandled 'error' event + } + + let stringifiedEr; + try { + stringifiedEr = inspect(er); + } catch { + stringifiedEr = er; + } + + // At least give some kind of context to the user + const err = new ERR_UNHANDLED_ERROR(stringifiedEr); + err.context = er; + throw err; // Unhandled 'error' event +} + + +function enhanceStackTrace(err, own) { + let ctorInfo = ''; + try { + const { name } = this.constructor; + if (name !== 'EventEmitter') + ctorInfo = ` on ${name} instance`; + } catch { + // Continue regardless of error. + } + const sep = `\nEmitted 'error' event${ctorInfo} at:\n`; + + const errStack = ArrayPrototypeSlice( + StringPrototypeSplit(err.stack, '\n'), 1); + const ownStack = ArrayPrototypeSlice( + StringPrototypeSplit(own.stack, '\n'), 1); + + const { len, offset } = identicalSequenceRange(ownStack, errStack); + if (len > 0) { + ArrayPrototypeSplice(ownStack, offset + 1, len - 2, + ' [... lines matching original stack trace ...]'); + } + + return err.stack + sep + ArrayPrototypeJoin(ownStack, '\n'); +} + +module.exports = { + arrayClone, + addCatch, + throwErrorOnMissingErrorHandler, +}; diff --git a/lib/internal/events/slow_event_emitter.js b/lib/internal/events/slow_event_emitter.js new file mode 100644 index 00000000000000..cf008f76e24f3d --- /dev/null +++ b/lib/internal/events/slow_event_emitter.js @@ -0,0 +1,282 @@ +'use strict'; + +const { + ReflectOwnKeys, + String, +} = primordials; + +let spliceOne; + +const { kErrorMonitor, kShapeMode } = require('internal/events/symbols'); +const { + arrayClone, + addCatch, + throwErrorOnMissingErrorHandler, +} = require('internal/events/shared_internal_event_emitter'); +const { genericNodeError } = require('internal/errors'); +const { inspect } = require('internal/util/inspect'); + +// TODO - should have the same API as slow_event_emitter +class SlowEventEmitter { + _events = undefined; + _eventsCount = 0; + // TODO - convert to symbol and rename + eventEmitterTranslationLayer = undefined; + [kShapeMode] = undefined; + + /** + * + * @param {FastEventEmitter} fastEventEmitter + */ + static fromFastEventEmitter(fastEventEmitter) { + const eventEmitter = new SlowEventEmitter(fastEventEmitter.eventEmitterTranslationLayer, fastEventEmitter._events); + + // TODO - add missing + eventEmitter._eventsCount = fastEventEmitter._eventsCount; + eventEmitter[kShapeMode] = fastEventEmitter[kShapeMode]; + + return eventEmitter; + } + + constructor(eventEmitterTranslationLayer, events = undefined) { + this.eventEmitterTranslationLayer = eventEmitterTranslationLayer; + this._events = events; + } + + /** + * Synchronously calls each of the listeners registered + * for the event. + * @param {string | symbol} type + * @param {...any} [args] + * @returns {boolean} + */ + emit(type, ...args) { + let doError = (type === 'error'); + const eventEmitterTranslationLayer = this.eventEmitterTranslationLayer; + + const events = this._events; + if (events !== undefined) { + if (doError && events[kErrorMonitor] !== undefined) + this.emit(kErrorMonitor, ...args); + doError = (doError && events.error === undefined); + } else if (!doError) + return false; + + // If there is no 'error' event listener then throw. + if (doError) { + throwErrorOnMissingErrorHandler.apply(eventEmitterTranslationLayer, args); + } + + const handler = events[type]; + + if (handler === undefined) + return false; + + if (typeof handler === 'function') { + const result = handler.apply(eventEmitterTranslationLayer, args); + + // We check if result is undefined first because that + // is the most common case so we do not pay any perf + // penalty + if (result !== undefined && result !== null) { + addCatch(eventEmitterTranslationLayer, result, type, args); + } + } else { + const len = handler.length; + const listeners = arrayClone(handler); + for (let i = 0; i < len; ++i) { + const result = listeners[i].apply(eventEmitterTranslationLayer, args); + + // We check if result is undefined first because that + // is the most common case so we do not pay any perf + // penalty. + // This code is duplicated because extracting it away + // would make it non-inlineable. + if (result !== undefined && result !== null) { + addCatch(eventEmitterTranslationLayer, result, type, args); + } + } + } + + return true; + } + + /** + * Adds a listener to the event emitter. + * @param {string | symbol} type + * @param {Function} listener + * @returns {EventEmitter} + */ + addListener(type, listener, prepend) { + const target = this.eventEmitterTranslationLayer; + let m; + let events; + let existing; + + events = this._events; + if (events === undefined) { + events = this._events = { __proto__: null }; + this._eventsCount = 0; + } else { + // To avoid recursion in the case that type === "newListener"! Before + // adding it to the listeners, first emit "newListener". + if (events.newListener !== undefined) { + this.eventEmitterTranslationLayer.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 = this._events; + } + existing = events[type]; + } + + if (existing === undefined) { + // Optimize the case of one listener. Don't need the extra array object. + events[type] = listener; + ++this._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); + } + + // Check for listener leak + // TODO - changed from _getMaxListeners target.getMaxListeners + m = target.getMaxListeners(target); + if (m > 0 && existing.length > m && !existing.warned) { + existing.warned = true; + // No error code for this since it is a Warning + const w = genericNodeError( + `Possible EventEmitter memory leak detected. ${existing.length} ${String(type)} listeners ` + + `added to ${inspect(target, { depth: -1 })}. Use emitter.setMaxListeners() to increase limit`, + { name: 'MaxListenersExceededWarning', emitter: target, type: type, count: existing.length }); + process.emitWarning(w); + } + } + + return this; + } + + /** + * Removes the specified `listener`. + * @param {string | symbol} type + * @param {Function} listener + * @returns {EventEmitter} + */ + removeListener(type, listener) { + const events = this._events; + if (events === undefined) + return undefined; + + const list = events[type]; + if (list === undefined) + return undefined; + + if (list === listener || list.listener === listener) { + this._eventsCount -= 1; + + if (this[kShapeMode]) { + events[type] = undefined; + } else if (this._eventsCount === 0) { + this._events = { __proto__: null }; + } else { + delete events[type]; + if (events.removeListener) + this.emit('removeListener', type, list.listener || listener); + } + } else if (typeof list !== 'function') { + let position = -1; + + for (let i = list.length - 1; i >= 0; i--) { + if (list[i] === listener || list[i].listener === listener) { + position = i; + break; + } + } + + if (position < 0) + return this; + + if (position === 0) + list.shift(); + 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, listener); + } + + return undefined; + } + + /** + * Removes all listeners from the event emitter. (Only + * removes listeners for a specific event name if specified + * as `type`). + * @param {string | symbol} [type] + * @returns {EventEmitter} + */ + removeAllListeners(type) { + const events = this._events; + if (events === undefined) + return undefined; + + // Not listening for removeListener, no need to emit + if (events.removeListener === undefined) { + if (arguments.length === 0) { + this._events = { __proto__: null }; + this._eventsCount = 0; + } else if (events[type] !== undefined) { + if (--this._eventsCount === 0) + this._events = { __proto__: null }; + else + delete events[type]; + } + this[kShapeMode] = false; + return undefined; + } + + // Emit removeListener for all listeners on all events + if (arguments.length === 0) { + for (const key of ReflectOwnKeys(events)) { + if (key === 'removeListener') continue; + this.removeAllListeners(key); + } + this.removeAllListeners('removeListener'); + this._events = { __proto__: null }; + this._eventsCount = 0; + this[kShapeMode] = false; + return undefined; + } + + const listeners = events[type]; + + if (typeof listeners === 'function') { + this.removeListener(type, listeners); + } else if (listeners !== undefined) { + // LIFO order + for (let i = listeners.length - 1; i >= 0; i--) { + this.removeListener(type, listeners[i]); + } + } + + return undefined; + } +} + +module.exports = { + SlowEventEmitter, +}; diff --git a/lib/internal/events/symbols.js b/lib/internal/events/symbols.js index c14363aa8af949..f7dca5e3358d93 100644 --- a/lib/internal/events/symbols.js +++ b/lib/internal/events/symbols.js @@ -2,12 +2,34 @@ const { Symbol, + SymbolFor, } = primordials; const kFirstEventParam = Symbol('nodejs.kFirstEventParam'); const kInitialFastPath = Symbol('kInitialFastPath'); +const kCapture = Symbol('kCapture'); +const kErrorMonitor = Symbol('events.errorMonitor'); +const kShapeMode = Symbol('shapeMode'); +const kMaxEventTargetListeners = Symbol('events.maxEventTargetListeners'); +const kMaxEventTargetListenersWarned = + Symbol('events.maxEventTargetListenersWarned'); +const kWatermarkData = SymbolFor('nodejs.watermarkData'); + +const kImpl = Symbol('kImpl'); +const kIsFastPath = Symbol('kIsFastPath'); +const kSwitchToSlowPath = Symbol('kSwitchToSlowPath'); + module.exports = { kFirstEventParam, kInitialFastPath, + kCapture, + kErrorMonitor, + kShapeMode, + kMaxEventTargetListeners, + kMaxEventTargetListenersWarned, + kWatermarkData, + kImpl, + kIsFastPath, + kSwitchToSlowPath, }; From ae87cd9a18d98e2ca0c5865d09815d2d88ac9c93 Mon Sep 17 00:00:00 2001 From: Raz Luvaton <16746759+rluvaton@users.noreply.github.com> Date: Wed, 1 May 2024 20:40:26 +0300 Subject: [PATCH 22/69] events: moved fast event emitter to own file --- lib/events.js | 144 +-------------------- lib/internal/events/fast_event_emitter.js | 149 +++++++++++++++++++++- 2 files changed, 150 insertions(+), 143 deletions(-) diff --git a/lib/events.js b/lib/events.js index fed3f8ffb73545..09d71e2576ddbe 100644 --- a/lib/events.js +++ b/lib/events.js @@ -86,11 +86,9 @@ const { } = require('internal/events/symbols'); const { arrayClone, - addCatch, - throwErrorOnMissingErrorHandler, } = require('internal/events/shared_internal_event_emitter'); const { SlowEventEmitter } = require('internal/events/slow_event_emitter'); -// Const { } = require('internal/events/fast_event_emitter'); +const { FastEventEmitter } = require('internal/events/fast_event_emitter'); let EventEmitterAsyncResource; @@ -1136,145 +1134,7 @@ function listenersController() { // ---------- FAST -// TODO - should have the same API as slow_event_emitter - -/** - * This class is optimized for the case where there is only a single listener for each event. - * it support kCapture - * but does not support event types with the following names: - * 1. 'newListener' and 'removeListener' as they can cause another event to be added - * and making the code less predictable thus harder to optimize - * 2. kErrorMonitor as it has special handling - */ -class FastEventEmitter { - /** - * The events are stored here as Record - */ - _events = undefined; - _eventsCount; - // TODO - convert to symbol and rename - eventEmitterTranslationLayer = undefined; - [kShapeMode] = undefined; - - constructor(eventEmitterTranslationLayer, _events = undefined) { - this.eventEmitterTranslationLayer = eventEmitterTranslationLayer; - this._events = _events; - } - - /** - * Synchronously calls each of the listeners registered - * for the event. - * @param {string | symbol} type - * @param {...any} [args] - * @returns {boolean} - */ - emit(type, ...args) { - const events = this._events; - if (type === 'error' && events?.error === undefined) { - throwErrorOnMissingErrorHandler.apply(this.eventEmitterTranslationLayer, args); - // TODO - add assertion that should not reach here; - return false; - } - - // TODO(rluvaton): will it be faster to add check if events is undefined instead? - const handler = events?.[type]; - - if (handler === undefined) { - return false; - } - - const result = handler.apply(this.eventEmitterTranslationLayer, args); - - // We check if result is undefined first because that - // is the most common case so we do not pay any perf - // penalty - if (result !== undefined && result !== null) { - addCatch(this.eventEmitterTranslationLayer, result, type, args); - } - - return true; - } - - isListenerAlreadyExists(type) { - return this._events?.[type] !== undefined; - } - - /** - * Adds a listener to the event emitter. - * @param {string | symbol} type - * @param {Function} listener - * @param {boolean} prepend not used here as we are in fast mode and only have single listener - */ - addListener(type, listener, prepend = undefined) { - let events; - - events = this._events; - // TODO - simplify this - if (events === undefined) { - events = this._events = {}; - this._eventsCount = 0; - - // Not emitting `newListener` here as in fast path we don't have it - } - - // Optimize the case of one listener. Don't need the extra array object. - events[type] = listener; - ++this._eventsCount; - } - - /** - * Removes the specified `listener`. - * @param {string | symbol} type - * @param {Function} listener - */ - removeListener(type, listener) { - - const events = this._events; - if (events === undefined) - return undefined; - - const list = events[type]; - if (list === undefined || (list !== listener && list.listener !== listener)) - return undefined; - - this._eventsCount -= 1; - - if (this[kShapeMode]) { - events[type] = undefined; - } else if (this._eventsCount === 0) { - this._events = { }; - } else { - delete events[type]; - // Not emitting `removeListener` here as in fast path we don't have it - } - } - - /** - * Removes all listeners from the event emitter. (Only - * removes listeners for a specific event name if specified - * as `type`). - * @param {string | symbol} [type] - */ - removeAllListeners(type) { - const events = this._events; - if (events === undefined) - return undefined; - - if (arguments.length === 0) { - this._events = { }; - this._eventsCount = 0; - } else if (events[type] !== undefined) { - if (--this._eventsCount === 0) - this._events = { }; - else - delete events[type]; - } - - this[kShapeMode] = false; - - return undefined; - } -} +// ------ todo // --------------- SLOW diff --git a/lib/internal/events/fast_event_emitter.js b/lib/internal/events/fast_event_emitter.js index 8b46fbbaadf6b0..062bba7717cbb6 100644 --- a/lib/internal/events/fast_event_emitter.js +++ b/lib/internal/events/fast_event_emitter.js @@ -1,3 +1,150 @@ 'use strict'; -module.exports = {}; +const { kShapeMode } = require('internal/events/symbols'); +const { + throwErrorOnMissingErrorHandler, + addCatch, +} = require('internal/events/shared_internal_event_emitter'); + +// TODO - should have the same API as slow_event_emitter +/** + * This class is optimized for the case where there is only a single listener for each event. + * it support kCapture + * but does not support event types with the following names: + * 1. 'newListener' and 'removeListener' as they can cause another event to be added + * and making the code less predictable thus harder to optimize + * 2. kErrorMonitor as it has special handling + */ +class FastEventEmitter { + /** + * The events are stored here as Record + */ + _events = undefined; + _eventsCount; + // TODO - convert to symbol and rename + eventEmitterTranslationLayer = undefined; + [kShapeMode] = undefined; + + constructor(eventEmitterTranslationLayer, _events = undefined) { + this.eventEmitterTranslationLayer = eventEmitterTranslationLayer; + this._events = _events; + } + + /** + * Synchronously calls each of the listeners registered + * for the event. + * @param {string | symbol} type + * @param {...any} [args] + * @returns {boolean} + */ + emit(type, ...args) { + const events = this._events; + if (type === 'error' && events?.error === undefined) { + throwErrorOnMissingErrorHandler.apply(this.eventEmitterTranslationLayer, args); + // TODO - add assertion that should not reach here; + return false; + } + + // TODO(rluvaton): will it be faster to add check if events is undefined instead? + const handler = events?.[type]; + + if (handler === undefined) { + return false; + } + + const result = handler.apply(this.eventEmitterTranslationLayer, args); + + // We check if result is undefined first because that + // is the most common case so we do not pay any perf + // penalty + if (result !== undefined && result !== null) { + addCatch(this.eventEmitterTranslationLayer, result, type, args); + } + + return true; + } + + isListenerAlreadyExists(type) { + return this._events?.[type] !== undefined; + } + + /** + * Adds a listener to the event emitter. + * @param {string | symbol} type + * @param {Function} listener + * @param {boolean} prepend not used here as we are in fast mode and only have single listener + */ + addListener(type, listener, prepend = undefined) { + let events; + + events = this._events; + // TODO - simplify this + if (events === undefined) { + events = this._events = {}; + this._eventsCount = 0; + + // Not emitting `newListener` here as in fast path we don't have it + } + + // Optimize the case of one listener. Don't need the extra array object. + events[type] = listener; + ++this._eventsCount; + } + + /** + * Removes the specified `listener`. + * @param {string | symbol} type + * @param {Function} listener + */ + removeListener(type, listener) { + + const events = this._events; + if (events === undefined) + return undefined; + + const list = events[type]; + if (list === undefined || (list !== listener && list.listener !== listener)) + return undefined; + + this._eventsCount -= 1; + + if (this[kShapeMode]) { + events[type] = undefined; + } else if (this._eventsCount === 0) { + this._events = { }; + } else { + delete events[type]; + // Not emitting `removeListener` here as in fast path we don't have it + } + } + + /** + * Removes all listeners from the event emitter. (Only + * removes listeners for a specific event name if specified + * as `type`). + * @param {string | symbol} [type] + */ + removeAllListeners(type) { + const events = this._events; + if (events === undefined) + return undefined; + + if (arguments.length === 0) { + this._events = { }; + this._eventsCount = 0; + } else if (events[type] !== undefined) { + if (--this._eventsCount === 0) + this._events = { }; + else + delete events[type]; + } + + this[kShapeMode] = false; + + return undefined; + } +} + +module.exports = { + FastEventEmitter, +}; From 682b820e2f69e899cf33e587bb9e1d6c3149d96e Mon Sep 17 00:00:00 2001 From: Raz Luvaton <16746759+rluvaton@users.noreply.github.com> Date: Wed, 1 May 2024 20:44:36 +0300 Subject: [PATCH 23/69] events: remove split comments --- lib/events.js | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/lib/events.js b/lib/events.js index 09d71e2576ddbe..bb4e6e1c60c7b7 100644 --- a/lib/events.js +++ b/lib/events.js @@ -1131,13 +1131,3 @@ function listenersController() { }, }; } - -// ---------- FAST - -// ------ todo - -// --------------- SLOW - -// ------ todo - -// ------- Internal helpers From fe43317011891e97a2723e8a6c50985b8ad5f86e Mon Sep 17 00:00:00 2001 From: Raz Luvaton <16746759+rluvaton@users.noreply.github.com> Date: Wed, 1 May 2024 20:59:22 +0300 Subject: [PATCH 24/69] events: extract EventEmitter to own file --- lib/events.js | 153 +--- lib/internal/events/event_emitter.js | 719 ++++++++++++++++++ .../events/shared_internal_event_emitter.js | 2 +- 3 files changed, 723 insertions(+), 151 deletions(-) create mode 100644 lib/internal/events/event_emitter.js diff --git a/lib/events.js b/lib/events.js index bb4e6e1c60c7b7..b3d877cf047f61 100644 --- a/lib/events.js +++ b/lib/events.js @@ -24,27 +24,17 @@ const { ArrayPrototypePop, ArrayPrototypePush, - ArrayPrototypeUnshift, - Boolean, Error, - FunctionPrototypeCall, NumberMAX_SAFE_INTEGER, - ObjectDefineProperties, - ObjectDefineProperty, - ObjectEntries, ObjectGetPrototypeOf, ObjectSetPrototypeOf, Promise, PromiseReject, PromiseResolve, ReflectApply, - ReflectOwnKeys, - Symbol, SymbolAsyncIterator, SymbolDispose, - SymbolFor, } = primordials; -const kRejection = SymbolFor('nodejs.rejection'); const { kEmptyObject } = require('internal/util'); @@ -57,167 +47,30 @@ const { AbortError, codes: { ERR_INVALID_ARG_TYPE, - ERR_INVALID_THIS, }, } = require('internal/errors'); const { validateInteger, validateAbortSignal, - validateBoolean, - validateFunction, - validateNumber, validateObject, - validateString, } = require('internal/validators'); const { addAbortListener } = require('internal/events/abort_listener'); const { - kInitialFastPath, - kCapture, - kErrorMonitor, - kShapeMode, kMaxEventTargetListeners, - kMaxEventTargetListenersWarned, kWatermarkData, - kImpl, - kIsFastPath, - kSwitchToSlowPath, - kEvents, } = require('internal/events/symbols'); const { - arrayClone, -} = require('internal/events/shared_internal_event_emitter'); -const { SlowEventEmitter } = require('internal/events/slow_event_emitter'); -const { FastEventEmitter } = require('internal/events/fast_event_emitter'); + EventEmitter, + _getMaxListeners, +} = require('internal/events/event_emitter'); -let EventEmitterAsyncResource; -// The EventEmitterAsyncResource has to be initialized lazily because event.js -// is loaded so early in the bootstrap process, before async_hooks is available. -// -// This implementation was adapted straight from addaleax's -// eventemitter-asyncresource MIT-licensed userland module. -// https://github.com/addaleax/eventemitter-asyncresource -function lazyEventEmitterAsyncResource() { - if (EventEmitterAsyncResource === undefined) { - const { - AsyncResource, - } = require('async_hooks'); - - const kEventEmitter = Symbol('kEventEmitter'); - const kAsyncResource = Symbol('kAsyncResource'); - class EventEmitterReferencingAsyncResource extends AsyncResource { - /** - * @param {EventEmitter} ee - * @param {string} [type] - * @param {{ - * triggerAsyncId?: number, - * requireManualDestroy?: boolean, - * }} [options] - */ - constructor(ee, type, options) { - super(type, options); - this[kEventEmitter] = ee; - } - - /** - * @type {EventEmitter} - */ - get eventEmitter() { - if (this[kEventEmitter] === undefined) - throw new ERR_INVALID_THIS('EventEmitterReferencingAsyncResource'); - return this[kEventEmitter]; - } - } - - EventEmitterAsyncResource = - class EventEmitterAsyncResource extends EventEmitter { - /** - * @param {{ - * name?: string, - * triggerAsyncId?: number, - * requireManualDestroy?: boolean, - * }} [options] - */ - constructor(options = undefined) { - let name; - if (typeof options === 'string') { - name = options; - options = undefined; - } else { - if (new.target === EventEmitterAsyncResource) { - validateString(options?.name, 'options.name'); - } - name = options?.name || new.target.name; - } - super(options); - - this[kAsyncResource] = - new EventEmitterReferencingAsyncResource(this, name, options); - } - - /** - * @param {symbol,string} event - * @param {...any} args - * @returns {boolean} - */ - emit(event, ...args) { - if (this[kAsyncResource] === undefined) - throw new ERR_INVALID_THIS('EventEmitterAsyncResource'); - const { asyncResource } = this; - ArrayPrototypeUnshift(args, super.emit, this, event); - return ReflectApply(asyncResource.runInAsyncScope, asyncResource, - args); - } - - /** - * @returns {void} - */ - emitDestroy() { - if (this[kAsyncResource] === undefined) - throw new ERR_INVALID_THIS('EventEmitterAsyncResource'); - this.asyncResource.emitDestroy(); - } - - /** - * @type {number} - */ - get asyncId() { - if (this[kAsyncResource] === undefined) - throw new ERR_INVALID_THIS('EventEmitterAsyncResource'); - return this.asyncResource.asyncId(); - } - - /** - * @type {number} - */ - get triggerAsyncId() { - if (this[kAsyncResource] === undefined) - throw new ERR_INVALID_THIS('EventEmitterAsyncResource'); - return this.asyncResource.triggerAsyncId(); - } - - /** - * @type {EventEmitterReferencingAsyncResource} - */ - get asyncResource() { - if (this[kAsyncResource] === undefined) - throw new ERR_INVALID_THIS('EventEmitterAsyncResource'); - return this[kAsyncResource]; - } - }; - } - return EventEmitterAsyncResource; -} - /** * Creates a new `EventEmitter` instance. * @param {{ captureRejections?: boolean; }} [opts] * @constructs {EventEmitter} */ -function EventEmitter(opts) { - EventEmitter.init.call(this, opts); -} module.exports = EventEmitter; module.exports.addAbortListener = addAbortListener; module.exports.once = once; diff --git a/lib/internal/events/event_emitter.js b/lib/internal/events/event_emitter.js new file mode 100644 index 00000000000000..ec7a35e5c30a3c --- /dev/null +++ b/lib/internal/events/event_emitter.js @@ -0,0 +1,719 @@ +'use strict'; + +const { + ArrayPrototypeUnshift, + Boolean, + FunctionPrototypeCall, + ObjectDefineProperties, + ObjectDefineProperty, + ObjectEntries, + ObjectGetPrototypeOf, + ReflectApply, + ReflectOwnKeys, + Symbol, + SymbolFor, +} = primordials; +const kRejection = SymbolFor('nodejs.rejection'); + +const { + codes: { + ERR_INVALID_ARG_TYPE, + ERR_INVALID_THIS, + }, +} = require('internal/errors'); + +const { + validateBoolean, + validateFunction, + validateNumber, + validateString, +} = require('internal/validators'); +const { + kInitialFastPath, + kCapture, + kErrorMonitor, + kShapeMode, + kMaxEventTargetListeners, + kMaxEventTargetListenersWarned, + kImpl, + kIsFastPath, + kSwitchToSlowPath, + kEvents, +} = require('internal/events/symbols'); +const { + arrayClone, +} = require('internal/events/shared_internal_event_emitter'); +const { SlowEventEmitter } = require('internal/events/slow_event_emitter'); +const { FastEventEmitter } = require('internal/events/fast_event_emitter'); + + +let EventEmitterAsyncResource; +// The EventEmitterAsyncResource has to be initialized lazily because event.js +// is loaded so early in the bootstrap process, before async_hooks is available. +// +// This implementation was adapted straight from addaleax's +// eventemitter-asyncresource MIT-licensed userland module. +// https://github.com/addaleax/eventemitter-asyncresource +function lazyEventEmitterAsyncResource() { + if (EventEmitterAsyncResource === undefined) { + const { + AsyncResource, + } = require('async_hooks'); + + const kEventEmitter = Symbol('kEventEmitter'); + const kAsyncResource = Symbol('kAsyncResource'); + class EventEmitterReferencingAsyncResource extends AsyncResource { + /** + * @param {EventEmitter} ee + * @param {string} [type] + * @param {{ + * triggerAsyncId?: number, + * requireManualDestroy?: boolean, + * }} [options] + */ + constructor(ee, type, options) { + super(type, options); + this[kEventEmitter] = ee; + } + + /** + * @type {EventEmitter} + */ + get eventEmitter() { + if (this[kEventEmitter] === undefined) + throw new ERR_INVALID_THIS('EventEmitterReferencingAsyncResource'); + return this[kEventEmitter]; + } + } + + EventEmitterAsyncResource = + class EventEmitterAsyncResource extends EventEmitter { + /** + * @param {{ + * name?: string, + * triggerAsyncId?: number, + * requireManualDestroy?: boolean, + * }} [options] + */ + constructor(options = undefined) { + let name; + if (typeof options === 'string') { + name = options; + options = undefined; + } else { + if (new.target === EventEmitterAsyncResource) { + validateString(options?.name, 'options.name'); + } + name = options?.name || new.target.name; + } + super(options); + + this[kAsyncResource] = + new EventEmitterReferencingAsyncResource(this, name, options); + } + + /** + * @param {symbol,string} event + * @param {...any} args + * @returns {boolean} + */ + emit(event, ...args) { + if (this[kAsyncResource] === undefined) + throw new ERR_INVALID_THIS('EventEmitterAsyncResource'); + const { asyncResource } = this; + ArrayPrototypeUnshift(args, super.emit, this, event); + return ReflectApply(asyncResource.runInAsyncScope, asyncResource, + args); + } + + /** + * @returns {void} + */ + emitDestroy() { + if (this[kAsyncResource] === undefined) + throw new ERR_INVALID_THIS('EventEmitterAsyncResource'); + this.asyncResource.emitDestroy(); + } + + /** + * @type {number} + */ + get asyncId() { + if (this[kAsyncResource] === undefined) + throw new ERR_INVALID_THIS('EventEmitterAsyncResource'); + return this.asyncResource.asyncId(); + } + + /** + * @type {number} + */ + get triggerAsyncId() { + if (this[kAsyncResource] === undefined) + throw new ERR_INVALID_THIS('EventEmitterAsyncResource'); + return this.asyncResource.triggerAsyncId(); + } + + /** + * @type {EventEmitterReferencingAsyncResource} + */ + get asyncResource() { + if (this[kAsyncResource] === undefined) + throw new ERR_INVALID_THIS('EventEmitterAsyncResource'); + return this[kAsyncResource]; + } + }; + } + return EventEmitterAsyncResource; +} + +/** + * Creates a new `EventEmitter` instance. + * @param {{ captureRejections?: boolean; }} [opts] + * @constructs {EventEmitter} + */ +function EventEmitter(opts) { + EventEmitter.init.call(this, opts); +} + +module.exports = { + EventEmitter, + _getMaxListeners, +}; +// Backwards-compat with node 0.10.x +EventEmitter.EventEmitter = EventEmitter; + +EventEmitter.usingDomains = false; + +EventEmitter.captureRejectionSymbol = kRejection; +ObjectDefineProperty(EventEmitter, 'captureRejections', { + __proto__: null, + get() { + return EventEmitter.prototype[kCapture]; + }, + set(value) { + validateBoolean(value, 'EventEmitter.captureRejections'); + + EventEmitter.prototype[kCapture] = value; + }, + enumerable: true, +}); + +ObjectDefineProperty(EventEmitter, 'EventEmitterAsyncResource', { + __proto__: null, + enumerable: true, + get: lazyEventEmitterAsyncResource, + set: undefined, + configurable: true, +}); + +EventEmitter.errorMonitor = kErrorMonitor; + +// The default for captureRejections is false +ObjectDefineProperty(EventEmitter.prototype, kCapture, { + __proto__: null, + value: false, + writable: true, + enumerable: false, +}); + +function isEventUnsupportedForFastPath(type) { + // Not supporting newListener and removeListener events in fast path + // as they can add new listeners in the middle of the process + // and also not supporting errorMonitor event as it has special handling + return type === 'newListener' || type === 'removeListener' || type === kErrorMonitor; +} + +ObjectDefineProperties(EventEmitter.prototype, { + [kImpl]: { + __proto__: null, + value: undefined, + enumerable: false, + configurable: false, + writable: true, + }, + [kIsFastPath]: { + __proto__: null, + value: undefined, + enumerable: false, + configurable: false, + writable: true, + }, + [kInitialFastPath]: { + __proto__: null, + value: undefined, + enumerable: false, + configurable: false, + writable: true, + }, + _events: { + __proto__: null, + enumerable: true, + + get: function() { + // TODO - remove optional chaining + return this[kImpl]?._events; + }, + set: function(arg) { + + if (this[kImpl] === undefined && this[kInitialFastPath] === true) { + // TODO(rluvaton): remove this eslint ignore + // eslint-disable-next-line no-use-before-define + this[kImpl] = new FastEventEmitter(this, arg); + this[kIsFastPath] = true; + this[kInitialFastPath] = undefined; + return; + } + + if (this[kImpl] === undefined) { + + // TODO(rluvaton): find a better way and also support array with length 1? + // TODO(rluvaton): if have newListener and removeListener events, switch to slow path + + const shouldBeFastPath = arg === undefined || + ObjectEntries(arg).some(({ 0: key, 1: value }) => + isEventUnsupportedForFastPath(key) || (typeof value !== 'undefined' && typeof value !== 'function')); + + // TODO(rluvaton): remove this eslint ignore + // eslint-disable-next-line no-use-before-define + this[kImpl] = shouldBeFastPath ? new FastEventEmitter(this, arg) : new SlowEventEmitter(this, arg); + this[kIsFastPath] = shouldBeFastPath; + return; + } + + // TODO - might need to change to slow path + // TODO - deprecate this + this[kImpl]._events = arg; + }, + }, + _eventsCount: { + __proto__: null, + + enumerable: true, + + get: function() { + return this[kImpl]?._eventsCount; + }, + set: function(arg) { + // TODO - remove optional chaining + if (!this[kImpl]) { + // TODO - don't use slow by default here + + // TODO(rluvaton): remove this eslint ignore + // eslint-disable-next-line no-use-before-define + this[kImpl] = new SlowEventEmitter(this); + this[kIsFastPath] = false; + // return; + } + // TODO - deprecate this + this[kImpl]._eventsCount = arg; + }, + }, +}); + +EventEmitter.prototype._maxListeners = undefined; +EventEmitter.prototype[kInitialFastPath] = undefined; + +// By default EventEmitters will print a warning if more than 10 listeners are +// added to it. This is a useful default which helps finding memory leaks. +let defaultMaxListeners = 10; +let isEventTarget; + +function checkListener(listener) { + validateFunction(listener, 'listener'); +} + +ObjectDefineProperty(EventEmitter, 'defaultMaxListeners', { + __proto__: null, + enumerable: true, + get: function() { + return defaultMaxListeners; + }, + set: function(arg) { + validateNumber(arg, 'defaultMaxListeners', 0); + defaultMaxListeners = arg; + }, +}); + +ObjectDefineProperties(EventEmitter, { + kMaxEventTargetListeners: { + __proto__: null, + value: kMaxEventTargetListeners, + enumerable: false, + configurable: false, + writable: false, + }, + kMaxEventTargetListenersWarned: { + __proto__: null, + value: kMaxEventTargetListenersWarned, + enumerable: false, + configurable: false, + writable: false, + }, +}); + +/** + * Sets the max listeners. + * @param {number} n + * @param {EventTarget[] | EventEmitter[]} [eventTargets] + * @returns {void} + */ +EventEmitter.setMaxListeners = + function(n = defaultMaxListeners, ...eventTargets) { + validateNumber(n, 'setMaxListeners', 0); + if (eventTargets.length === 0) { + defaultMaxListeners = n; + } else { + if (isEventTarget === undefined) + isEventTarget = require('internal/event_target').isEventTarget; + + for (let i = 0; i < eventTargets.length; i++) { + const target = eventTargets[i]; + if (isEventTarget(target)) { + target[kMaxEventTargetListeners] = n; + target[kMaxEventTargetListenersWarned] = false; + } else if (typeof target.setMaxListeners === 'function') { + target.setMaxListeners(n); + } else { + throw new ERR_INVALID_ARG_TYPE( + 'eventTargets', + ['EventEmitter', 'EventTarget'], + target); + } + } + } + }; + +// If you're updating this function definition, please also update any +// re-definitions, such as the one in the Domain module (lib/domain.js). +EventEmitter.init = function(opts) { + // TODO(rluvaton): remove this eslint ignore + // eslint-disable-next-line no-use-before-define + this[kImpl] ??= new FastEventEmitter(this, opts?.[kEvents]); + this[kIsFastPath] ??= true; + + const thisEvents = this._events; + + + // TODO - update this + if (thisEvents === undefined || + thisEvents === ObjectGetPrototypeOf(this)._events) { + // In fast path we don't want to have __proto__: null as it will cause the object to be in dictionary mode + // and will slow down the access to the properties by a lot (around 2x) + this[kImpl]._events = this[kIsFastPath] ? {} : { __proto__: null }; + this[kImpl]._eventsCount = 0; + this[kImpl][kShapeMode] = false; + } else { + this[kImpl][kShapeMode] = true; + } + + this._maxListeners = this._maxListeners || undefined; + + + if (opts?.captureRejections) { + validateBoolean(opts.captureRejections, 'options.captureRejections'); + this[kCapture] = Boolean(opts.captureRejections); + } else { + // Assigning the kCapture property directly saves an expensive + // prototype lookup in a very sensitive hot path. + this[kCapture] = EventEmitter.prototype[kCapture]; + } +}; + +EventEmitter.prototype[kSwitchToSlowPath] = function() { + if (this[kIsFastPath] === false) { + return; + } + + if (this[kIsFastPath] === true) { + this[kIsFastPath] = false; + + // TODO(rluvaton): remove this eslint ignore + // eslint-disable-next-line no-use-before-define + this[kImpl] = SlowEventEmitter.fromFastEventEmitter(this[kImpl]); + return; + } + + // TODO(rluvaton): remove this eslint ignore + // eslint-disable-next-line no-use-before-define + this[kImpl] = new SlowEventEmitter(this); + this[kIsFastPath] = false; + +}; + +/** + * Increases the max listeners of the event emitter. + * @param {number} n + * @returns {EventEmitter} + */ +EventEmitter.prototype.setMaxListeners = function setMaxListeners(n) { + validateNumber(n, 'setMaxListeners', 0); + this._maxListeners = n; + return this; +}; + +function _getMaxListeners(that) { + if (that._maxListeners === undefined) + return EventEmitter.defaultMaxListeners; + return that._maxListeners; +} +/** + * Returns the current max listener value for the event emitter. + * @returns {number} + */ +EventEmitter.prototype.getMaxListeners = function getMaxListeners() { + return _getMaxListeners(this); +}; + + +/** + * Synchronously calls each of the listeners registered + * for the event. + * @param {string | symbol} type + * @param {...any} [args] + * @returns {boolean} + */ +EventEmitter.prototype.emit = function emit(type, ...args) { + return this[kImpl].emit(type, ...args); +}; + +/** + * Adds a listener to the event emitter. + * @param {string | symbol} type + * @param {Function} listener + * @returns {EventEmitter} + */ +EventEmitter.prototype.addListener = function addListener(type, listener) { + checkListener(listener); + + // This can happen in TLSSocket, where we listen for close event before + // the EventEmitter was initiated + // TODO(rluvaton): check how it worked before? + if (!this[kImpl]) { + EventEmitter.init.apply(this); + } + + // If the listener is already added, + // switch to slow path as the fast path optimized for single listener for each event + if (this[kIsFastPath] && (isEventUnsupportedForFastPath(type) || this[kImpl].isListenerAlreadyExists(type))) { + this[kSwitchToSlowPath](); + } + + return this[kImpl].addListener(type, listener); +}; + +EventEmitter.prototype.on = EventEmitter.prototype.addListener; + +/** + * Adds the `listener` function to the beginning of + * the listeners array. + * @param {string | symbol} type + * @param {Function} listener + * @returns {EventEmitter} + */ +EventEmitter.prototype.prependListener = + function prependListener(type, listener) { + checkListener(listener); + + // This can happen in TLSSocket, where we listen for close event before + // the EventEmitter was initiated + // TODO(rluvaton): check how it worked before? + if (!this[kImpl]) { + EventEmitter.init.apply(this); + } + + // If the listener is already added, + // switch to slow path as the fast path optimized for single listener for each event + if (this[kIsFastPath] && (isEventUnsupportedForFastPath(type) || this[kImpl].isListenerAlreadyExists(type))) { + this[kSwitchToSlowPath](); + } + + return this[kImpl].addListener(type, listener, true); + }; + +function onceWrapper() { + if (!this.fired) { + this.target.removeListener(this.type, this.wrapFn); + this.fired = true; + if (arguments.length === 0) + return this.listener.call(this.target); + return this.listener.apply(this.target, arguments); + } +} + +function _onceWrap(target, type, listener) { + const state = { fired: false, wrapFn: undefined, target, type, listener }; + const wrapped = onceWrapper.bind(state); + wrapped.listener = listener; + state.wrapFn = wrapped; + return wrapped; +} + +/** + * Adds a one-time `listener` function to the event emitter. + * @param {string | symbol} type + * @param {Function} listener + * @returns {EventEmitter} + */ +EventEmitter.prototype.once = function once(type, listener) { + checkListener(listener); + + this.on(type, _onceWrap(this, type, listener)); + return this; +}; + +/** + * Adds a one-time `listener` function to the beginning of + * the listeners array. + * @param {string | symbol} type + * @param {Function} listener + * @returns {EventEmitter} + */ +EventEmitter.prototype.prependOnceListener = + function prependOnceListener(type, listener) { + checkListener(listener); + + this.prependListener(type, _onceWrap(this, type, listener)); + return this; + }; + +/** + * Removes the specified `listener` from the listeners array. + * @param {string | symbol} type + * @param {Function} listener + * @returns {EventEmitter} + */ +EventEmitter.prototype.removeListener = + function removeListener(type, listener) { + checkListener(listener); + + this[kImpl].removeListener(type, listener); + + return this; + }; + +EventEmitter.prototype.off = EventEmitter.prototype.removeListener; + +/** + * Removes all listeners from the event emitter. (Only + * removes listeners for a specific event name if specified + * as `type`). + * @param {string | symbol} [type] + * @returns {EventEmitter} + */ +EventEmitter.prototype.removeAllListeners = + function removeAllListeners(type) { + this[kImpl].removeAllListeners(type); + return this; + }; + +function _listeners(target, type, unwrap) { + const events = target._events; + + if (events === undefined) + return []; + + const evlistener = events[type]; + if (evlistener === undefined) + return []; + + if (typeof evlistener === 'function') + return unwrap ? [evlistener.listener || evlistener] : [evlistener]; + + return unwrap ? + unwrapListeners(evlistener) : arrayClone(evlistener); +} + +/** + * Returns a copy of the array of listeners for the event name + * specified as `type`. + * @param {string | symbol} type + * @returns {Function[]} + */ +EventEmitter.prototype.listeners = function listeners(type) { + return _listeners(this, type, true); +}; + +/** + * Returns a copy of the array of listeners and wrappers for + * the event name specified as `type`. + * @param {string | symbol} type + * @returns {Function[]} + */ +EventEmitter.prototype.rawListeners = function rawListeners(type) { + return _listeners(this, type, false); +}; + +/** + * Returns the number of listeners listening to the event name + * specified as `type`. + * @deprecated since v3.2.0 + * @param {EventEmitter} emitter + * @param {string | symbol} type + * @returns {number} + */ +EventEmitter.listenerCount = function(emitter, type) { + if (typeof emitter.listenerCount === 'function') { + return emitter.listenerCount(type); + } + return FunctionPrototypeCall(listenerCount, emitter, type); +}; + +EventEmitter.prototype.listenerCount = listenerCount; + +/** + * Returns the number of listeners listening to event name + * specified as `type`. + * @param {string | symbol} type + * @param {Function} listener + * @returns {number} + */ +function listenerCount(type, listener) { + const events = this._events; + + if (events !== undefined) { + const evlistener = events[type]; + + if (typeof evlistener === 'function') { + if (listener != null) { + return listener === evlistener || listener === evlistener.listener ? 1 : 0; + } + + return 1; + } else if (evlistener !== undefined) { + if (listener != null) { + let matching = 0; + + for (let i = 0, l = evlistener.length; i < l; i++) { + if (evlistener[i] === listener || evlistener[i].listener === listener) { + matching++; + } + } + + return matching; + } + + return evlistener.length; + } + } + + return 0; +} + +/** + * Returns an array listing the events for which + * the emitter has registered listeners. + * @returns {any[]} + */ +EventEmitter.prototype.eventNames = function eventNames() { + return this._eventsCount > 0 ? ReflectOwnKeys(this._events) : []; +}; + +function unwrapListeners(arr) { + const ret = arrayClone(arr); + for (let i = 0; i < ret.length; ++i) { + const orig = ret[i].listener; + if (typeof orig === 'function') + ret[i] = orig; + } + return ret; +} diff --git a/lib/internal/events/shared_internal_event_emitter.js b/lib/internal/events/shared_internal_event_emitter.js index 6487ed627528e8..de40009182a588 100644 --- a/lib/internal/events/shared_internal_event_emitter.js +++ b/lib/internal/events/shared_internal_event_emitter.js @@ -92,7 +92,7 @@ function throwErrorOnMissingErrorHandler(...args) { try { const capture = {}; // TODO - EventEmitter ??= require('events'); + EventEmitter ??= require('internal/events/event_emitter').EventEmitter; ErrorCaptureStackTrace(capture, EventEmitter.prototype.emit); ObjectDefineProperty(er, kEnhanceStackBeforeInspector, { __proto__: null, From 63fc3890f8e27398ec48c7682b7c1c70a43ad251 Mon Sep 17 00:00:00 2001 From: Raz Luvaton <16746759+rluvaton@users.noreply.github.com> Date: Wed, 1 May 2024 21:01:58 +0300 Subject: [PATCH 25/69] events: remove temp code --- lib/internal/events/event_emitter.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/internal/events/event_emitter.js b/lib/internal/events/event_emitter.js index ec7a35e5c30a3c..be81698d764edc 100644 --- a/lib/internal/events/event_emitter.js +++ b/lib/internal/events/event_emitter.js @@ -38,7 +38,6 @@ const { kImpl, kIsFastPath, kSwitchToSlowPath, - kEvents, } = require('internal/events/symbols'); const { arrayClone, @@ -388,7 +387,7 @@ EventEmitter.setMaxListeners = EventEmitter.init = function(opts) { // TODO(rluvaton): remove this eslint ignore // eslint-disable-next-line no-use-before-define - this[kImpl] ??= new FastEventEmitter(this, opts?.[kEvents]); + this[kImpl] ??= new FastEventEmitter(this); this[kIsFastPath] ??= true; const thisEvents = this._events; From 8d7ad6baf6f815287265a055cedd0e2cec3e2ba4 Mon Sep 17 00:00:00 2001 From: Raz Luvaton <16746759+rluvaton@users.noreply.github.com> Date: Wed, 1 May 2024 21:04:06 +0300 Subject: [PATCH 26/69] events: remove eslint ignore comments --- lib/internal/events/event_emitter.js | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/lib/internal/events/event_emitter.js b/lib/internal/events/event_emitter.js index be81698d764edc..e512aa89d609b9 100644 --- a/lib/internal/events/event_emitter.js +++ b/lib/internal/events/event_emitter.js @@ -255,8 +255,6 @@ ObjectDefineProperties(EventEmitter.prototype, { set: function(arg) { if (this[kImpl] === undefined && this[kInitialFastPath] === true) { - // TODO(rluvaton): remove this eslint ignore - // eslint-disable-next-line no-use-before-define this[kImpl] = new FastEventEmitter(this, arg); this[kIsFastPath] = true; this[kInitialFastPath] = undefined; @@ -272,8 +270,6 @@ ObjectDefineProperties(EventEmitter.prototype, { ObjectEntries(arg).some(({ 0: key, 1: value }) => isEventUnsupportedForFastPath(key) || (typeof value !== 'undefined' && typeof value !== 'function')); - // TODO(rluvaton): remove this eslint ignore - // eslint-disable-next-line no-use-before-define this[kImpl] = shouldBeFastPath ? new FastEventEmitter(this, arg) : new SlowEventEmitter(this, arg); this[kIsFastPath] = shouldBeFastPath; return; @@ -297,8 +293,6 @@ ObjectDefineProperties(EventEmitter.prototype, { if (!this[kImpl]) { // TODO - don't use slow by default here - // TODO(rluvaton): remove this eslint ignore - // eslint-disable-next-line no-use-before-define this[kImpl] = new SlowEventEmitter(this); this[kIsFastPath] = false; // return; @@ -385,8 +379,6 @@ EventEmitter.setMaxListeners = // If you're updating this function definition, please also update any // re-definitions, such as the one in the Domain module (lib/domain.js). EventEmitter.init = function(opts) { - // TODO(rluvaton): remove this eslint ignore - // eslint-disable-next-line no-use-before-define this[kImpl] ??= new FastEventEmitter(this); this[kIsFastPath] ??= true; @@ -425,15 +417,10 @@ EventEmitter.prototype[kSwitchToSlowPath] = function() { if (this[kIsFastPath] === true) { this[kIsFastPath] = false; - - // TODO(rluvaton): remove this eslint ignore - // eslint-disable-next-line no-use-before-define this[kImpl] = SlowEventEmitter.fromFastEventEmitter(this[kImpl]); return; } - // TODO(rluvaton): remove this eslint ignore - // eslint-disable-next-line no-use-before-define this[kImpl] = new SlowEventEmitter(this); this[kIsFastPath] = false; From b2605682c19d8236dff5b5d827265e14aea29e8a Mon Sep 17 00:00:00 2001 From: Raz Luvaton <16746759+rluvaton@users.noreply.github.com> Date: Wed, 1 May 2024 21:09:01 +0300 Subject: [PATCH 27/69] events: update comment and use original code --- lib/internal/events/fast_event_emitter.js | 4 ++-- lib/internal/events/slow_event_emitter.js | 6 ++++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/lib/internal/events/fast_event_emitter.js b/lib/internal/events/fast_event_emitter.js index 062bba7717cbb6..72e923e63b8f86 100644 --- a/lib/internal/events/fast_event_emitter.js +++ b/lib/internal/events/fast_event_emitter.js @@ -9,7 +9,7 @@ const { // TODO - should have the same API as slow_event_emitter /** * This class is optimized for the case where there is only a single listener for each event. - * it support kCapture + * it supports kCapture * but does not support event types with the following names: * 1. 'newListener' and 'removeListener' as they can cause another event to be added * and making the code less predictable thus harder to optimize @@ -17,7 +17,7 @@ const { */ class FastEventEmitter { /** - * The events are stored here as Record + * The events are stored here as Record */ _events = undefined; _eventsCount; diff --git a/lib/internal/events/slow_event_emitter.js b/lib/internal/events/slow_event_emitter.js index cf008f76e24f3d..3092f7a8fe1e35 100644 --- a/lib/internal/events/slow_event_emitter.js +++ b/lib/internal/events/slow_event_emitter.js @@ -16,6 +16,8 @@ const { const { genericNodeError } = require('internal/errors'); const { inspect } = require('internal/util/inspect'); +let _getMaxListeners; + // TODO - should have the same API as slow_event_emitter class SlowEventEmitter { _events = undefined; @@ -148,8 +150,8 @@ class SlowEventEmitter { } // Check for listener leak - // TODO - changed from _getMaxListeners target.getMaxListeners - m = target.getMaxListeners(target); + _getMaxListeners ??= require('internal/events/event_emitter')._getMaxListeners; + m = _getMaxListeners(target); if (m > 0 && existing.length > m && !existing.warned) { existing.warned = true; // No error code for this since it is a Warning From 731599de8cc0f348c0f9f853468b94c8ee8b4741 Mon Sep 17 00:00:00 2001 From: Raz Luvaton <16746759+rluvaton@users.noreply.github.com> Date: Wed, 1 May 2024 21:27:48 +0300 Subject: [PATCH 28/69] events: convert FastEventEmitter class to function --- lib/internal/events/fast_event_emitter.js | 223 +++++++++++----------- 1 file changed, 111 insertions(+), 112 deletions(-) diff --git a/lib/internal/events/fast_event_emitter.js b/lib/internal/events/fast_event_emitter.js index 72e923e63b8f86..2e5558618f70ce 100644 --- a/lib/internal/events/fast_event_emitter.js +++ b/lib/internal/events/fast_event_emitter.js @@ -15,135 +15,134 @@ const { * and making the code less predictable thus harder to optimize * 2. kErrorMonitor as it has special handling */ -class FastEventEmitter { - /** - * The events are stored here as Record - */ - _events = undefined; - _eventsCount; - // TODO - convert to symbol and rename - eventEmitterTranslationLayer = undefined; - [kShapeMode] = undefined; - - constructor(eventEmitterTranslationLayer, _events = undefined) { - this.eventEmitterTranslationLayer = eventEmitterTranslationLayer; - this._events = _events; - } +function FastEventEmitter(eventEmitterTranslationLayer, _events) { + this.eventEmitterTranslationLayer = eventEmitterTranslationLayer; + this._events = _events; +} + +/** + * The events are stored here as Record + */ +FastEventEmitter.prototype._events = undefined; +FastEventEmitter.prototype[kShapeMode] = undefined; +FastEventEmitter.prototype._eventsCount = undefined; - /** - * Synchronously calls each of the listeners registered - * for the event. - * @param {string | symbol} type - * @param {...any} [args] - * @returns {boolean} - */ - emit(type, ...args) { - const events = this._events; - if (type === 'error' && events?.error === undefined) { - throwErrorOnMissingErrorHandler.apply(this.eventEmitterTranslationLayer, args); - // TODO - add assertion that should not reach here; - return false; - } - - // TODO(rluvaton): will it be faster to add check if events is undefined instead? - const handler = events?.[type]; - - if (handler === undefined) { - return false; - } - - const result = handler.apply(this.eventEmitterTranslationLayer, args); - - // We check if result is undefined first because that - // is the most common case so we do not pay any perf - // penalty - if (result !== undefined && result !== null) { - addCatch(this.eventEmitterTranslationLayer, result, type, args); - } - - return true; +// TODO - convert to symbol and rename +FastEventEmitter.prototype.eventEmitterTranslationLayer = undefined; + +/** + * Synchronously calls each of the listeners registered + * for the event. + * @param {string | symbol} type + * @param {...any} [args] + * @returns {boolean} + */ +FastEventEmitter.prototype.emit = function emit(type, ...args) { + const events = this._events; + if (type === 'error' && events?.error === undefined) { + throwErrorOnMissingErrorHandler.apply(this.eventEmitterTranslationLayer, args); + // TODO - add assertion that should not reach here; + return false; } - isListenerAlreadyExists(type) { - return this._events?.[type] !== undefined; + // TODO(rluvaton): will it be faster to add check if events is undefined instead? + const handler = events?.[type]; + + if (handler === undefined) { + return false; } - /** - * Adds a listener to the event emitter. - * @param {string | symbol} type - * @param {Function} listener - * @param {boolean} prepend not used here as we are in fast mode and only have single listener - */ - addListener(type, listener, prepend = undefined) { - let events; - - events = this._events; - // TODO - simplify this - if (events === undefined) { - events = this._events = {}; - this._eventsCount = 0; - - // Not emitting `newListener` here as in fast path we don't have it - } - - // Optimize the case of one listener. Don't need the extra array object. - events[type] = listener; - ++this._eventsCount; + const result = handler.apply(this.eventEmitterTranslationLayer, args); + + // We check if result is undefined first because that + // is the most common case so we do not pay any perf + // penalty + if (result !== undefined && result !== null) { + addCatch(this.eventEmitterTranslationLayer, result, type, args); } - /** - * Removes the specified `listener`. - * @param {string | symbol} type - * @param {Function} listener - */ - removeListener(type, listener) { + return true; +}; - const events = this._events; - if (events === undefined) - return undefined; +FastEventEmitter.prototype.isListenerAlreadyExists = function isListenerAlreadyExists(type) { + return this._events?.[type] !== undefined; +}; - const list = events[type]; - if (list === undefined || (list !== listener && list.listener !== listener)) - return undefined; +/** + * Adds a listener to the event emitter. + * @param {string | symbol} type + * @param {Function} listener + * @param {boolean} prepend not used here as we are in fast mode and only have single listener + */ +FastEventEmitter.prototype.addListener = function addListener(type, listener, prepend = undefined) { + let events; - this._eventsCount -= 1; + events = this._events; + if (events === undefined) { + events = this._events = {}; + this._eventsCount = 0; - if (this[kShapeMode]) { - events[type] = undefined; - } else if (this._eventsCount === 0) { - this._events = { }; - } else { - delete events[type]; - // Not emitting `removeListener` here as in fast path we don't have it - } + // Not emitting `newListener` here as in fast path we don't have it } - /** - * Removes all listeners from the event emitter. (Only - * removes listeners for a specific event name if specified - * as `type`). - * @param {string | symbol} [type] - */ - removeAllListeners(type) { - const events = this._events; - if (events === undefined) - return undefined; - - if (arguments.length === 0) { - this._events = { }; - this._eventsCount = 0; - } else if (events[type] !== undefined) { - if (--this._eventsCount === 0) - this._events = { }; - else - delete events[type]; - } + // Optimize the case of one listener. Don't need the extra array object. + events[type] = listener; + ++this._eventsCount; +}; - this[kShapeMode] = false; +/** + * Removes the specified `listener`. + * @param {string | symbol} type + * @param {Function} listener + */ +FastEventEmitter.prototype.removeListener = function removeListener(type, listener) { + const events = this._events; + if (events === undefined) + return undefined; + const list = events[type]; + if (list === undefined || (list !== listener && list.listener !== listener)) return undefined; + + this._eventsCount -= 1; + + if (this[kShapeMode]) { + events[type] = undefined; + } else if (this._eventsCount === 0) { + this._events = { }; + } else { + // TODO - another perf optimization can be to keep this? + delete events[type]; + // Not emitting `removeListener` here as in fast path we don't have it } -} +}; + + +/** + * Removes all listeners from the event emitter. (Only + * removes listeners for a specific event name if specified + * as `type`). + * @param {string | symbol} [type] + */ +FastEventEmitter.prototype.removeAllListeners = function removeAllListeners(type) { + const events = this._events; + if (events === undefined) + return undefined; + + if (arguments.length === 0) { + this._events = { }; + this._eventsCount = 0; + } else if (events[type] !== undefined) { + if (--this._eventsCount === 0) + this._events = { }; + else + delete events[type]; + } + + this[kShapeMode] = false; + + return undefined; +}; module.exports = { FastEventEmitter, From 603f59af4fcb0d3df4cca7c8d8e36688ca391c18 Mon Sep 17 00:00:00 2001 From: Raz Luvaton <16746759+rluvaton@users.noreply.github.com> Date: Wed, 1 May 2024 21:39:29 +0300 Subject: [PATCH 29/69] events: improve stream creation performance --- lib/internal/events/event_emitter.js | 17 ++++-------- lib/internal/events/symbols.js | 4 +-- lib/internal/streams/duplex.js | 40 ++++++++++++++-------------- lib/internal/streams/readable.js | 37 +++++++++++++------------ lib/internal/streams/writable.js | 26 +++++++++--------- 5 files changed, 58 insertions(+), 66 deletions(-) diff --git a/lib/internal/events/event_emitter.js b/lib/internal/events/event_emitter.js index e512aa89d609b9..c583c54b2d4a14 100644 --- a/lib/internal/events/event_emitter.js +++ b/lib/internal/events/event_emitter.js @@ -29,7 +29,6 @@ const { validateString, } = require('internal/validators'); const { - kInitialFastPath, kCapture, kErrorMonitor, kShapeMode, @@ -38,6 +37,7 @@ const { kImpl, kIsFastPath, kSwitchToSlowPath, + kInitialEvents, } = require('internal/events/symbols'); const { arrayClone, @@ -237,7 +237,7 @@ ObjectDefineProperties(EventEmitter.prototype, { configurable: false, writable: true, }, - [kInitialFastPath]: { + [kInitialEvents]: { __proto__: null, value: undefined, enumerable: false, @@ -253,14 +253,6 @@ ObjectDefineProperties(EventEmitter.prototype, { return this[kImpl]?._events; }, set: function(arg) { - - if (this[kImpl] === undefined && this[kInitialFastPath] === true) { - this[kImpl] = new FastEventEmitter(this, arg); - this[kIsFastPath] = true; - this[kInitialFastPath] = undefined; - return; - } - if (this[kImpl] === undefined) { // TODO(rluvaton): find a better way and also support array with length 1? @@ -304,7 +296,6 @@ ObjectDefineProperties(EventEmitter.prototype, { }); EventEmitter.prototype._maxListeners = undefined; -EventEmitter.prototype[kInitialFastPath] = undefined; // By default EventEmitters will print a warning if more than 10 listeners are // added to it. This is a useful default which helps finding memory leaks. @@ -393,7 +384,9 @@ EventEmitter.init = function(opts) { this[kImpl]._events = this[kIsFastPath] ? {} : { __proto__: null }; this[kImpl]._eventsCount = 0; this[kImpl][kShapeMode] = false; - } else { + } else if (this[kInitialEvents] !== undefined) { + this[kImpl]._events = this[kInitialEvents]; + this[kInitialEvents] = undefined; this[kImpl][kShapeMode] = true; } diff --git a/lib/internal/events/symbols.js b/lib/internal/events/symbols.js index f7dca5e3358d93..f357fa4159c666 100644 --- a/lib/internal/events/symbols.js +++ b/lib/internal/events/symbols.js @@ -6,7 +6,7 @@ const { } = primordials; const kFirstEventParam = Symbol('nodejs.kFirstEventParam'); -const kInitialFastPath = Symbol('kInitialFastPath'); +const kInitialEvents = Symbol('kInitialEvents'); const kCapture = Symbol('kCapture'); const kErrorMonitor = Symbol('events.errorMonitor'); @@ -22,7 +22,7 @@ const kSwitchToSlowPath = Symbol('kSwitchToSlowPath'); module.exports = { kFirstEventParam, - kInitialFastPath, + kInitialEvents, kCapture, kErrorMonitor, kShapeMode, diff --git a/lib/internal/streams/duplex.js b/lib/internal/streams/duplex.js index ff6bfb089a128c..3331b3f7ec01d5 100644 --- a/lib/internal/streams/duplex.js +++ b/lib/internal/streams/duplex.js @@ -38,7 +38,7 @@ module.exports = Duplex; const Stream = require('internal/streams/legacy').Stream; const Readable = require('internal/streams/readable'); const Writable = require('internal/streams/writable'); -const { kInitialFastPath } = require('internal/events/symbols'); +const { kInitialEvents } = require('internal/events/symbols'); const { addAbortSignal, @@ -67,26 +67,26 @@ function Duplex(options) { if (!(this instanceof Duplex)) return new Duplex(options); - if (this._events === undefined || this._events === null) { - this[kInitialFastPath] = true; + const thisEvents = this._events; + if (thisEvents === undefined || thisEvents === null) { + this[kInitialEvents] ??= { + close: undefined, + error: undefined, + prefinish: undefined, + finish: undefined, + drain: undefined, + data: undefined, + end: undefined, + readable: undefined, + // Skip uncommon events... + // pause: undefined, + // resume: undefined, + // pipe: undefined, + // unpipe: undefined, + // [destroyImpl.kConstruct]: undefined, + // [destroyImpl.kDestroy]: undefined, + }; } - this._events ??= { - close: undefined, - error: undefined, - prefinish: undefined, - finish: undefined, - drain: undefined, - data: undefined, - end: undefined, - readable: undefined, - // Skip uncommon events... - // pause: undefined, - // resume: undefined, - // pipe: undefined, - // unpipe: undefined, - // [destroyImpl.kConstruct]: undefined, - // [destroyImpl.kDestroy]: undefined, - }; this._readableState = new Readable.ReadableState(options, this, true); this._writableState = new Writable.WritableState(options, this, true); diff --git a/lib/internal/streams/readable.js b/lib/internal/streams/readable.js index f0d82b8e39a1e8..0ecb69e5e3de41 100644 --- a/lib/internal/streams/readable.js +++ b/lib/internal/streams/readable.js @@ -43,7 +43,7 @@ Readable.ReadableState = ReadableState; const EE = require('events'); const { Stream, prependListener } = require('internal/streams/legacy'); -const { kInitialFastPath } = require('internal/events/symbols'); +const { kInitialEvents } = require('internal/events/symbols'); const { Buffer } = require('buffer'); const { @@ -320,24 +320,23 @@ function Readable(options) { if (!(this instanceof Readable)) return new Readable(options); - if (this._events === undefined || this._events === null) { - this[kInitialFastPath] = true; - } - - this._events ??= { - close: undefined, - error: undefined, - data: undefined, - end: undefined, - readable: undefined, - // Skip uncommon events... - // pause: undefined, - // resume: undefined, - // pipe: undefined, - // unpipe: undefined, - // [destroyImpl.kConstruct]: undefined, - // [destroyImpl.kDestroy]: undefined, - }; + const thisEvents = this._events; + if (thisEvents === undefined || thisEvents === null) { + this[kInitialEvents] ??= { + close: undefined, + error: undefined, + data: undefined, + end: undefined, + readable: undefined, + // Skip uncommon events... + // pause: undefined, + // resume: undefined, + // pipe: undefined, + // unpipe: undefined, + // [destroyImpl.kConstruct]: undefined, + // [destroyImpl.kDestroy]: undefined, + }; + } this._readableState = new ReadableState(options, this, false); diff --git a/lib/internal/streams/writable.js b/lib/internal/streams/writable.js index caaaaf94d8cda6..b7f9a5f90eec6d 100644 --- a/lib/internal/streams/writable.js +++ b/lib/internal/streams/writable.js @@ -44,7 +44,7 @@ const EE = require('events'); const Stream = require('internal/streams/legacy').Stream; const { Buffer } = require('buffer'); const destroyImpl = require('internal/streams/destroy'); -const { kInitialFastPath } = require('internal/events/symbols'); +const { kInitialEvents } = require('internal/events/symbols'); const { addAbortSignal, @@ -386,19 +386,19 @@ function Writable(options) { if (!(this instanceof Writable)) return new Writable(options); - if (this._events === undefined || this._events === null) { - this[kInitialFastPath] = true; + const thisEvents = this._events; + if (thisEvents === undefined || thisEvents === null) { + this[kInitialEvents] ??= { + close: undefined, + error: undefined, + prefinish: undefined, + finish: undefined, + drain: undefined, + // Skip uncommon events... + // [destroyImpl.kConstruct]: undefined, + // [destroyImpl.kDestroy]: undefined, + }; } - this._events ??= { - close: undefined, - error: undefined, - prefinish: undefined, - finish: undefined, - drain: undefined, - // Skip uncommon events... - // [destroyImpl.kConstruct]: undefined, - // [destroyImpl.kDestroy]: undefined, - }; this._writableState = new WritableState(options, this, false); From 53fddf2f15ba9a42424b7fa53e76bfb234814a29 Mon Sep 17 00:00:00 2001 From: Raz Luvaton <16746759+rluvaton@users.noreply.github.com> Date: Wed, 1 May 2024 22:20:34 +0300 Subject: [PATCH 30/69] events: replace check for kImpl --- lib/internal/events/event_emitter.js | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/lib/internal/events/event_emitter.js b/lib/internal/events/event_emitter.js index c583c54b2d4a14..cd42f9b4e11761 100644 --- a/lib/internal/events/event_emitter.js +++ b/lib/internal/events/event_emitter.js @@ -274,7 +274,6 @@ ObjectDefineProperties(EventEmitter.prototype, { }, _eventsCount: { __proto__: null, - enumerable: true, get: function() { @@ -282,12 +281,10 @@ ObjectDefineProperties(EventEmitter.prototype, { }, set: function(arg) { // TODO - remove optional chaining - if (!this[kImpl]) { + if (this[kImpl] === undefined) { // TODO - don't use slow by default here - this[kImpl] = new SlowEventEmitter(this); this[kIsFastPath] = false; - // return; } // TODO - deprecate this this[kImpl]._eventsCount = arg; @@ -374,20 +371,20 @@ EventEmitter.init = function(opts) { this[kIsFastPath] ??= true; const thisEvents = this._events; - + const impl = this[kImpl]; // TODO - update this if (thisEvents === undefined || thisEvents === ObjectGetPrototypeOf(this)._events) { // In fast path we don't want to have __proto__: null as it will cause the object to be in dictionary mode // and will slow down the access to the properties by a lot (around 2x) - this[kImpl]._events = this[kIsFastPath] ? {} : { __proto__: null }; - this[kImpl]._eventsCount = 0; - this[kImpl][kShapeMode] = false; + impl._events = this[kIsFastPath] ? {} : { __proto__: null }; + impl._eventsCount = 0; + impl[kShapeMode] = false; } else if (this[kInitialEvents] !== undefined) { - this[kImpl]._events = this[kInitialEvents]; + impl._events = this[kInitialEvents]; this[kInitialEvents] = undefined; - this[kImpl][kShapeMode] = true; + impl[kShapeMode] = true; } this._maxListeners = this._maxListeners || undefined; @@ -467,7 +464,7 @@ EventEmitter.prototype.addListener = function addListener(type, listener) { // This can happen in TLSSocket, where we listen for close event before // the EventEmitter was initiated // TODO(rluvaton): check how it worked before? - if (!this[kImpl]) { + if (this[kImpl] === undefined) { EventEmitter.init.apply(this); } @@ -496,7 +493,7 @@ EventEmitter.prototype.prependListener = // This can happen in TLSSocket, where we listen for close event before // the EventEmitter was initiated // TODO(rluvaton): check how it worked before? - if (!this[kImpl]) { + if (this[kImpl] === undefined) { EventEmitter.init.apply(this); } From 5e777435cad81eba4a3ea65b6625ba0b1ee96634 Mon Sep 17 00:00:00 2001 From: Raz Luvaton <16746759+rluvaton@users.noreply.github.com> Date: Wed, 1 May 2024 22:21:39 +0300 Subject: [PATCH 31/69] events: remove check assert not object done this as we no longer have __proto__: null in fast paths --- test/parallel/test-event-emitter-subclass.js | 1 - 1 file changed, 1 deletion(-) diff --git a/test/parallel/test-event-emitter-subclass.js b/test/parallel/test-event-emitter-subclass.js index a6ef54e5fa7401..396faf84b64ce7 100644 --- a/test/parallel/test-event-emitter-subclass.js +++ b/test/parallel/test-event-emitter-subclass.js @@ -47,7 +47,6 @@ assert.throws(function() { }, /blerg/); process.on('exit', function() { - assert(!(myee._events instanceof Object)); assert.deepStrictEqual(Object.keys(myee._events), []); console.log('ok'); }); From 3055bcae93d98070c51f32d835a87d33d087e019 Mon Sep 17 00:00:00 2001 From: Raz Luvaton <16746759+rluvaton@users.noreply.github.com> Date: Wed, 1 May 2024 22:23:31 +0300 Subject: [PATCH 32/69] events: initiate impl if missing inside emit --- lib/internal/events/event_emitter.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/internal/events/event_emitter.js b/lib/internal/events/event_emitter.js index cd42f9b4e11761..2c817afbbef91d 100644 --- a/lib/internal/events/event_emitter.js +++ b/lib/internal/events/event_emitter.js @@ -449,6 +449,10 @@ EventEmitter.prototype.getMaxListeners = function getMaxListeners() { * @returns {boolean} */ EventEmitter.prototype.emit = function emit(type, ...args) { + // Some can call emit in the constructor before even calling super + if (this[kImpl] === undefined) { + EventEmitter.init.apply(this); + } return this[kImpl].emit(type, ...args); }; From 23ac15c00fdee76d162479e8a9e8d3947f3221fb Mon Sep 17 00:00:00 2001 From: Raz Luvaton <16746759+rluvaton@users.noreply.github.com> Date: Wed, 1 May 2024 23:29:24 +0300 Subject: [PATCH 33/69] events: fixed bug for shared _events --- lib/internal/events/event_emitter.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/lib/internal/events/event_emitter.js b/lib/internal/events/event_emitter.js index 2c817afbbef91d..3e00c0dbab0a1c 100644 --- a/lib/internal/events/event_emitter.js +++ b/lib/internal/events/event_emitter.js @@ -367,15 +367,18 @@ EventEmitter.setMaxListeners = // If you're updating this function definition, please also update any // re-definitions, such as the one in the Domain module (lib/domain.js). EventEmitter.init = function(opts) { - this[kImpl] ??= new FastEventEmitter(this); - this[kIsFastPath] ??= true; + let thisPrototype; + + if (this[kImpl] === undefined || (thisPrototype = ObjectGetPrototypeOf(this))[kImpl] === this[kImpl]) { + this[kImpl] = new FastEventEmitter(this); + this[kIsFastPath] = true; + } const thisEvents = this._events; const impl = this[kImpl]; - // TODO - update this if (thisEvents === undefined || - thisEvents === ObjectGetPrototypeOf(this)._events) { + thisEvents === (thisPrototype || ObjectGetPrototypeOf(this))._events) { // In fast path we don't want to have __proto__: null as it will cause the object to be in dictionary mode // and will slow down the access to the properties by a lot (around 2x) impl._events = this[kIsFastPath] ? {} : { __proto__: null }; From f7667521d75f5b3b49672019ca83b2f098050974 Mon Sep 17 00:00:00 2001 From: Raz Luvaton <16746759+rluvaton@users.noreply.github.com> Date: Wed, 1 May 2024 23:52:17 +0300 Subject: [PATCH 34/69] events: fixed emit and improved perf --- lib/internal/events/event_emitter.js | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/lib/internal/events/event_emitter.js b/lib/internal/events/event_emitter.js index 3e00c0dbab0a1c..caeafb9b4b73c7 100644 --- a/lib/internal/events/event_emitter.js +++ b/lib/internal/events/event_emitter.js @@ -41,6 +41,7 @@ const { } = require('internal/events/symbols'); const { arrayClone, + throwErrorOnMissingErrorHandler, } = require('internal/events/shared_internal_event_emitter'); const { SlowEventEmitter } = require('internal/events/slow_event_emitter'); const { FastEventEmitter } = require('internal/events/fast_event_emitter'); @@ -452,11 +453,15 @@ EventEmitter.prototype.getMaxListeners = function getMaxListeners() { * @returns {boolean} */ EventEmitter.prototype.emit = function emit(type, ...args) { - // Some can call emit in the constructor before even calling super - if (this[kImpl] === undefined) { - EventEmitter.init.apply(this); + // Users can call emit in the constructor before even calling super causing this[kImpl] to be undefined + const impl = this[kImpl]; + + // The order here is important as impl === undefined is slower than type === 'error' + if(type === 'error' && impl === undefined) { + throwErrorOnMissingErrorHandler.apply(this, args); } - return this[kImpl].emit(type, ...args); + + return impl !== undefined ? impl.emit(type, ...args) : false; }; /** From badf1e51cb0be787193c57ebb0c6b87a5a7a58a3 Mon Sep 17 00:00:00 2001 From: Raz Luvaton <16746759+rluvaton@users.noreply.github.com> Date: Thu, 2 May 2024 00:07:06 +0300 Subject: [PATCH 35/69] events: remove todos --- lib/internal/events/fast_event_emitter.js | 10 ++-------- lib/internal/events/slow_event_emitter.js | 3 --- 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/lib/internal/events/fast_event_emitter.js b/lib/internal/events/fast_event_emitter.js index 2e5558618f70ce..726609c42d0f42 100644 --- a/lib/internal/events/fast_event_emitter.js +++ b/lib/internal/events/fast_event_emitter.js @@ -6,7 +6,6 @@ const { addCatch, } = require('internal/events/shared_internal_event_emitter'); -// TODO - should have the same API as slow_event_emitter /** * This class is optimized for the case where there is only a single listener for each event. * it supports kCapture @@ -26,8 +25,6 @@ function FastEventEmitter(eventEmitterTranslationLayer, _events) { FastEventEmitter.prototype._events = undefined; FastEventEmitter.prototype[kShapeMode] = undefined; FastEventEmitter.prototype._eventsCount = undefined; - -// TODO - convert to symbol and rename FastEventEmitter.prototype.eventEmitterTranslationLayer = undefined; /** @@ -39,14 +36,12 @@ FastEventEmitter.prototype.eventEmitterTranslationLayer = undefined; */ FastEventEmitter.prototype.emit = function emit(type, ...args) { const events = this._events; + if (type === 'error' && events?.error === undefined) { throwErrorOnMissingErrorHandler.apply(this.eventEmitterTranslationLayer, args); - // TODO - add assertion that should not reach here; - return false; } - // TODO(rluvaton): will it be faster to add check if events is undefined instead? - const handler = events?.[type]; + const handler = events[type]; if (handler === undefined) { return false; @@ -111,7 +106,6 @@ FastEventEmitter.prototype.removeListener = function removeListener(type, listen } else if (this._eventsCount === 0) { this._events = { }; } else { - // TODO - another perf optimization can be to keep this? delete events[type]; // Not emitting `removeListener` here as in fast path we don't have it } diff --git a/lib/internal/events/slow_event_emitter.js b/lib/internal/events/slow_event_emitter.js index 3092f7a8fe1e35..d37ca771192dee 100644 --- a/lib/internal/events/slow_event_emitter.js +++ b/lib/internal/events/slow_event_emitter.js @@ -18,11 +18,9 @@ const { inspect } = require('internal/util/inspect'); let _getMaxListeners; -// TODO - should have the same API as slow_event_emitter class SlowEventEmitter { _events = undefined; _eventsCount = 0; - // TODO - convert to symbol and rename eventEmitterTranslationLayer = undefined; [kShapeMode] = undefined; @@ -33,7 +31,6 @@ class SlowEventEmitter { static fromFastEventEmitter(fastEventEmitter) { const eventEmitter = new SlowEventEmitter(fastEventEmitter.eventEmitterTranslationLayer, fastEventEmitter._events); - // TODO - add missing eventEmitter._eventsCount = fastEventEmitter._eventsCount; eventEmitter[kShapeMode] = fastEventEmitter[kShapeMode]; From 60d522c5af9ed7748468322d1073f7e3ec1f39a2 Mon Sep 17 00:00:00 2001 From: Raz Luvaton <16746759+rluvaton@users.noreply.github.com> Date: Thu, 2 May 2024 00:07:18 +0300 Subject: [PATCH 36/69] events: format --- lib/internal/events/event_emitter.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/internal/events/event_emitter.js b/lib/internal/events/event_emitter.js index caeafb9b4b73c7..9161dbb635345e 100644 --- a/lib/internal/events/event_emitter.js +++ b/lib/internal/events/event_emitter.js @@ -457,7 +457,7 @@ EventEmitter.prototype.emit = function emit(type, ...args) { const impl = this[kImpl]; // The order here is important as impl === undefined is slower than type === 'error' - if(type === 'error' && impl === undefined) { + if (type === 'error' && impl === undefined) { throwErrorOnMissingErrorHandler.apply(this, args); } From ddfeb858cb454c83ff8c668f022f31b9df47aa82 Mon Sep 17 00:00:00 2001 From: Raz Luvaton <16746759+rluvaton@users.noreply.github.com> Date: Thu, 2 May 2024 00:18:04 +0300 Subject: [PATCH 37/69] events: replace SlowEventEmitter class with function --- lib/internal/events/slow_event_emitter.js | 432 +++++++++++----------- 1 file changed, 215 insertions(+), 217 deletions(-) diff --git a/lib/internal/events/slow_event_emitter.js b/lib/internal/events/slow_event_emitter.js index d37ca771192dee..e89746a40628f7 100644 --- a/lib/internal/events/slow_event_emitter.js +++ b/lib/internal/events/slow_event_emitter.js @@ -18,263 +18,261 @@ const { inspect } = require('internal/util/inspect'); let _getMaxListeners; -class SlowEventEmitter { - _events = undefined; - _eventsCount = 0; - eventEmitterTranslationLayer = undefined; - [kShapeMode] = undefined; - - /** - * - * @param {FastEventEmitter} fastEventEmitter - */ - static fromFastEventEmitter(fastEventEmitter) { - const eventEmitter = new SlowEventEmitter(fastEventEmitter.eventEmitterTranslationLayer, fastEventEmitter._events); - - eventEmitter._eventsCount = fastEventEmitter._eventsCount; - eventEmitter[kShapeMode] = fastEventEmitter[kShapeMode]; - - return eventEmitter; - } +function SlowEventEmitter(eventEmitterTranslationLayer, events = undefined) { + this.eventEmitterTranslationLayer = eventEmitterTranslationLayer; + this._events = events; +} + +SlowEventEmitter.prototype._events = undefined; +SlowEventEmitter.prototype._eventsCount = 0; +SlowEventEmitter.prototype.eventEmitterTranslationLayer = undefined; +SlowEventEmitter.prototype[kShapeMode] = undefined; + +/** + * @param {FastEventEmitter} fastEventEmitter + */ +SlowEventEmitter.fromFastEventEmitter = function fromFastEventEmitter(fastEventEmitter) { + const eventEmitter = new SlowEventEmitter(fastEventEmitter.eventEmitterTranslationLayer, fastEventEmitter._events); - constructor(eventEmitterTranslationLayer, events = undefined) { - this.eventEmitterTranslationLayer = eventEmitterTranslationLayer; - this._events = events; + eventEmitter._eventsCount = fastEventEmitter._eventsCount; + eventEmitter[kShapeMode] = fastEventEmitter[kShapeMode]; + + return eventEmitter; +}; + +/** + * Synchronously calls each of the listeners registered + * for the event. + * @param {string | symbol} type + * @param {...any} [args] + * @returns {boolean} + */ +SlowEventEmitter.prototype.emit = function emit(type, ...args) { + let doError = (type === 'error'); + const eventEmitterTranslationLayer = this.eventEmitterTranslationLayer; + + const events = this._events; + if (events !== undefined) { + if (doError && events[kErrorMonitor] !== undefined) + this.emit(kErrorMonitor, ...args); + doError = (doError && events.error === undefined); + } else if (!doError) + return false; + + // If there is no 'error' event listener then throw. + if (doError) { + throwErrorOnMissingErrorHandler.apply(eventEmitterTranslationLayer, args); } - /** - * Synchronously calls each of the listeners registered - * for the event. - * @param {string | symbol} type - * @param {...any} [args] - * @returns {boolean} - */ - emit(type, ...args) { - let doError = (type === 'error'); - const eventEmitterTranslationLayer = this.eventEmitterTranslationLayer; - - const events = this._events; - if (events !== undefined) { - if (doError && events[kErrorMonitor] !== undefined) - this.emit(kErrorMonitor, ...args); - doError = (doError && events.error === undefined); - } else if (!doError) - return false; - - // If there is no 'error' event listener then throw. - if (doError) { - throwErrorOnMissingErrorHandler.apply(eventEmitterTranslationLayer, args); - } + const handler = events[type]; - const handler = events[type]; + if (handler === undefined) + return false; - if (handler === undefined) - return false; + if (typeof handler === 'function') { + const result = handler.apply(eventEmitterTranslationLayer, args); - if (typeof handler === 'function') { - const result = handler.apply(eventEmitterTranslationLayer, args); + // We check if result is undefined first because that + // is the most common case so we do not pay any perf + // penalty + if (result !== undefined && result !== null) { + addCatch(eventEmitterTranslationLayer, result, type, args); + } + } else { + const len = handler.length; + const listeners = arrayClone(handler); + for (let i = 0; i < len; ++i) { + const result = listeners[i].apply(eventEmitterTranslationLayer, args); // We check if result is undefined first because that // is the most common case so we do not pay any perf - // penalty + // penalty. + // This code is duplicated because extracting it away + // would make it non-inlineable. if (result !== undefined && result !== null) { addCatch(eventEmitterTranslationLayer, result, type, args); } - } else { - const len = handler.length; - const listeners = arrayClone(handler); - for (let i = 0; i < len; ++i) { - const result = listeners[i].apply(eventEmitterTranslationLayer, args); - - // We check if result is undefined first because that - // is the most common case so we do not pay any perf - // penalty. - // This code is duplicated because extracting it away - // would make it non-inlineable. - if (result !== undefined && result !== null) { - addCatch(eventEmitterTranslationLayer, result, type, args); - } - } } - - return true; } - /** - * Adds a listener to the event emitter. - * @param {string | symbol} type - * @param {Function} listener - * @returns {EventEmitter} - */ - addListener(type, listener, prepend) { - const target = this.eventEmitterTranslationLayer; - let m; - let events; - let existing; - - events = this._events; - if (events === undefined) { - events = this._events = { __proto__: null }; - this._eventsCount = 0; - } else { - // To avoid recursion in the case that type === "newListener"! Before - // adding it to the listeners, first emit "newListener". - if (events.newListener !== undefined) { - this.eventEmitterTranslationLayer.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 = this._events; - } - existing = events[type]; + return true; +}; + +/** + * Adds a listener to the event emitter. + * @param {string | symbol} type + * @param {Function} listener + * @returns {EventEmitter} + */ +SlowEventEmitter.prototype.addListener = function addListener(type, listener, prepend) { + const target = this.eventEmitterTranslationLayer; + let m; + let events; + let existing; + + events = this._events; + if (events === undefined) { + events = this._events = { __proto__: null }; + this._eventsCount = 0; + } else { + // To avoid recursion in the case that type === "newListener"! Before + // adding it to the listeners, first emit "newListener". + if (events.newListener !== undefined) { + this.eventEmitterTranslationLayer.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 = this._events; } + existing = events[type]; + } - if (existing === undefined) { - // Optimize the case of one listener. Don't need the extra array object. - events[type] = listener; - ++this._eventsCount; + if (existing === undefined) { + // Optimize the case of one listener. Don't need the extra array object. + events[type] = listener; + ++this._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 { - 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); - } - - // Check for listener leak - _getMaxListeners ??= require('internal/events/event_emitter')._getMaxListeners; - m = _getMaxListeners(target); - if (m > 0 && existing.length > m && !existing.warned) { - existing.warned = true; - // No error code for this since it is a Warning - const w = genericNodeError( - `Possible EventEmitter memory leak detected. ${existing.length} ${String(type)} listeners ` + - `added to ${inspect(target, { depth: -1 })}. Use emitter.setMaxListeners() to increase limit`, - { name: 'MaxListenersExceededWarning', emitter: target, type: type, count: existing.length }); - process.emitWarning(w); - } + existing.push(listener); } - return this; + // Check for listener leak + _getMaxListeners ??= require('internal/events/event_emitter')._getMaxListeners; + m = _getMaxListeners(target); + if (m > 0 && existing.length > m && !existing.warned) { + existing.warned = true; + // No error code for this since it is a Warning + const w = genericNodeError( + `Possible EventEmitter memory leak detected. ${existing.length} ${String(type)} listeners ` + + `added to ${inspect(target, { depth: -1 })}. Use emitter.setMaxListeners() to increase limit`, + { name: 'MaxListenersExceededWarning', emitter: target, type: type, count: existing.length }); + process.emitWarning(w); + } } - /** - * Removes the specified `listener`. - * @param {string | symbol} type - * @param {Function} listener - * @returns {EventEmitter} - */ - removeListener(type, listener) { - const events = this._events; - if (events === undefined) - return undefined; - - const list = events[type]; - if (list === undefined) - return undefined; - - if (list === listener || list.listener === listener) { - this._eventsCount -= 1; - - if (this[kShapeMode]) { - events[type] = undefined; - } else if (this._eventsCount === 0) { - this._events = { __proto__: null }; - } else { - delete events[type]; - if (events.removeListener) - this.emit('removeListener', type, list.listener || listener); - } - } else if (typeof list !== 'function') { - let position = -1; - - for (let i = list.length - 1; i >= 0; i--) { - if (list[i] === listener || list[i].listener === listener) { - position = i; - break; - } - } + return this; +}; + +/** + * Removes the specified `listener`. + * @param {string | symbol} type + * @param {Function} listener + * @returns {EventEmitter} + */ +SlowEventEmitter.prototype.removeListener = function removeListener(type, listener) { + const events = this._events; + if (events === undefined) + return undefined; + + const list = events[type]; + if (list === undefined) + return undefined; + + if (list === listener || list.listener === listener) { + this._eventsCount -= 1; - if (position < 0) - return this; + if (this[kShapeMode]) { + events[type] = undefined; + } else if (this._eventsCount === 0) { + this._events = { __proto__: null }; + } else { + delete events[type]; + if (events.removeListener) + this.emit('removeListener', type, list.listener || listener); + } + } else if (typeof list !== 'function') { + let position = -1; - if (position === 0) - list.shift(); - else { - if (spliceOne === undefined) - spliceOne = require('internal/util').spliceOne; - spliceOne(list, position); + for (let i = list.length - 1; i >= 0; i--) { + if (list[i] === listener || list[i].listener === listener) { + position = i; + break; } + } - if (list.length === 1) - events[type] = list[0]; + if (position < 0) + return this; - if (events.removeListener !== undefined) - this.emit('removeListener', type, listener); + if (position === 0) + list.shift(); + else { + if (spliceOne === undefined) + spliceOne = require('internal/util').spliceOne; + spliceOne(list, position); } - return undefined; + if (list.length === 1) + events[type] = list[0]; + + if (events.removeListener !== undefined) + this.emit('removeListener', type, listener); } - /** - * Removes all listeners from the event emitter. (Only - * removes listeners for a specific event name if specified - * as `type`). - * @param {string | symbol} [type] - * @returns {EventEmitter} - */ - removeAllListeners(type) { - const events = this._events; - if (events === undefined) - return undefined; - - // Not listening for removeListener, no need to emit - if (events.removeListener === undefined) { - if (arguments.length === 0) { - this._events = { __proto__: null }; - this._eventsCount = 0; - } else if (events[type] !== undefined) { - if (--this._eventsCount === 0) - this._events = { __proto__: null }; - else - delete events[type]; - } - this[kShapeMode] = false; - return undefined; - } + return undefined; +}; - // Emit removeListener for all listeners on all events +/** + * Removes all listeners from the event emitter. (Only + * removes listeners for a specific event name if specified + * as `type`). + * @param {string | symbol} [type] + * @returns {EventEmitter} + */ +SlowEventEmitter.prototype.removeAllListeners = function removeAllListeners(type) { + const events = this._events; + if (events === undefined) + return undefined; + + // Not listening for removeListener, no need to emit + if (events.removeListener === undefined) { if (arguments.length === 0) { - for (const key of ReflectOwnKeys(events)) { - if (key === 'removeListener') continue; - this.removeAllListeners(key); - } - this.removeAllListeners('removeListener'); this._events = { __proto__: null }; this._eventsCount = 0; - this[kShapeMode] = false; - return undefined; + } else if (events[type] !== undefined) { + if (--this._eventsCount === 0) + this._events = { __proto__: null }; + else + delete events[type]; } + this[kShapeMode] = false; + return undefined; + } - const listeners = events[type]; - - if (typeof listeners === 'function') { - this.removeListener(type, listeners); - } else if (listeners !== undefined) { - // LIFO order - for (let i = listeners.length - 1; i >= 0; i--) { - this.removeListener(type, listeners[i]); - } + // Emit removeListener for all listeners on all events + if (arguments.length === 0) { + for (const key of ReflectOwnKeys(events)) { + if (key === 'removeListener') continue; + this.removeAllListeners(key); } - + this.removeAllListeners('removeListener'); + this._events = { __proto__: null }; + this._eventsCount = 0; + this[kShapeMode] = false; return undefined; } -} + + const listeners = events[type]; + + if (typeof listeners === 'function') { + this.removeListener(type, listeners); + } else if (listeners !== undefined) { + // LIFO order + for (let i = listeners.length - 1; i >= 0; i--) { + this.removeListener(type, listeners[i]); + } + } + + return undefined; +}; + module.exports = { SlowEventEmitter, From 7132c440892647d507e468e8ec7b9853688e8569 Mon Sep 17 00:00:00 2001 From: Raz Luvaton <16746759+rluvaton@users.noreply.github.com> Date: Thu, 2 May 2024 00:18:57 +0300 Subject: [PATCH 38/69] events: switch to slow path immediately if setting the _events property --- lib/internal/events/event_emitter.js | 22 +++------------------- 1 file changed, 3 insertions(+), 19 deletions(-) diff --git a/lib/internal/events/event_emitter.js b/lib/internal/events/event_emitter.js index 9161dbb635345e..bf691ebbd0bc10 100644 --- a/lib/internal/events/event_emitter.js +++ b/lib/internal/events/event_emitter.js @@ -6,7 +6,6 @@ const { FunctionPrototypeCall, ObjectDefineProperties, ObjectDefineProperty, - ObjectEntries, ObjectGetPrototypeOf, ReflectApply, ReflectOwnKeys, @@ -248,28 +247,13 @@ ObjectDefineProperties(EventEmitter.prototype, { _events: { __proto__: null, enumerable: true, - get: function() { - // TODO - remove optional chaining return this[kImpl]?._events; }, set: function(arg) { - if (this[kImpl] === undefined) { - - // TODO(rluvaton): find a better way and also support array with length 1? - // TODO(rluvaton): if have newListener and removeListener events, switch to slow path - - const shouldBeFastPath = arg === undefined || - ObjectEntries(arg).some(({ 0: key, 1: value }) => - isEventUnsupportedForFastPath(key) || (typeof value !== 'undefined' && typeof value !== 'function')); - - this[kImpl] = shouldBeFastPath ? new FastEventEmitter(this, arg) : new SlowEventEmitter(this, arg); - this[kIsFastPath] = shouldBeFastPath; - return; - } - - // TODO - might need to change to slow path - // TODO - deprecate this + // If using the _events setter move to slow path to avoid bugs with incorrect shape or functions + // Users should not interact with _events directly + this[kSwitchToSlowPath](); this[kImpl]._events = arg; }, }, From dd03904f6835331c5625f870f157d8e1928896cb Mon Sep 17 00:00:00 2001 From: Raz Luvaton <16746759+rluvaton@users.noreply.github.com> Date: Thu, 2 May 2024 00:20:33 +0300 Subject: [PATCH 39/69] events: return 0 in _eventsCount when no implementation exists --- lib/internal/events/event_emitter.js | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/lib/internal/events/event_emitter.js b/lib/internal/events/event_emitter.js index bf691ebbd0bc10..9ac0f941d12365 100644 --- a/lib/internal/events/event_emitter.js +++ b/lib/internal/events/event_emitter.js @@ -265,13 +265,9 @@ ObjectDefineProperties(EventEmitter.prototype, { return this[kImpl]?._eventsCount; }, set: function(arg) { - // TODO - remove optional chaining if (this[kImpl] === undefined) { - // TODO - don't use slow by default here - this[kImpl] = new SlowEventEmitter(this); - this[kIsFastPath] = false; + return 0; } - // TODO - deprecate this this[kImpl]._eventsCount = arg; }, }, From 46004a230c995f819ba93d692474e8bd04fd8ada Mon Sep 17 00:00:00 2001 From: Raz Luvaton <16746759+rluvaton@users.noreply.github.com> Date: Thu, 2 May 2024 00:20:45 +0300 Subject: [PATCH 40/69] events: remove todos --- lib/internal/events/event_emitter.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/internal/events/event_emitter.js b/lib/internal/events/event_emitter.js index 9ac0f941d12365..64176ccfb4aa58 100644 --- a/lib/internal/events/event_emitter.js +++ b/lib/internal/events/event_emitter.js @@ -455,7 +455,6 @@ EventEmitter.prototype.addListener = function addListener(type, listener) { // This can happen in TLSSocket, where we listen for close event before // the EventEmitter was initiated - // TODO(rluvaton): check how it worked before? if (this[kImpl] === undefined) { EventEmitter.init.apply(this); } @@ -484,7 +483,6 @@ EventEmitter.prototype.prependListener = // This can happen in TLSSocket, where we listen for close event before // the EventEmitter was initiated - // TODO(rluvaton): check how it worked before? if (this[kImpl] === undefined) { EventEmitter.init.apply(this); } From 7b82c665a3632acc3bcd9ffd69e41cb59bba5b1a Mon Sep 17 00:00:00 2001 From: Raz Luvaton <16746759+rluvaton@users.noreply.github.com> Date: Thu, 2 May 2024 00:23:37 +0300 Subject: [PATCH 41/69] events: remove todos --- lib/internal/events/shared_internal_event_emitter.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/internal/events/shared_internal_event_emitter.js b/lib/internal/events/shared_internal_event_emitter.js index de40009182a588..15d641356521de 100644 --- a/lib/internal/events/shared_internal_event_emitter.js +++ b/lib/internal/events/shared_internal_event_emitter.js @@ -81,8 +81,6 @@ function emitUnhandledRejectionOrErr(ee, err, type, args) { } -// TODO - move this to a different file -// TODO - rename function throwErrorOnMissingErrorHandler(...args) { let er; if (args.length > 0) From c073179086b8acc1e824a68212868430cab32ace Mon Sep 17 00:00:00 2001 From: Raz Luvaton <16746759+rluvaton@users.noreply.github.com> Date: Thu, 2 May 2024 00:31:20 +0300 Subject: [PATCH 42/69] events: remove todos --- lib/internal/events/shared_internal_event_emitter.js | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/internal/events/shared_internal_event_emitter.js b/lib/internal/events/shared_internal_event_emitter.js index 15d641356521de..8e5e6021f812f2 100644 --- a/lib/internal/events/shared_internal_event_emitter.js +++ b/lib/internal/events/shared_internal_event_emitter.js @@ -89,7 +89,6 @@ function throwErrorOnMissingErrorHandler(...args) { if (er instanceof Error) { try { const capture = {}; - // TODO EventEmitter ??= require('internal/events/event_emitter').EventEmitter; ErrorCaptureStackTrace(capture, EventEmitter.prototype.emit); ObjectDefineProperty(er, kEnhanceStackBeforeInspector, { From 70692bf31b178eed93654215f9f22a1ce1ee47a2 Mon Sep 17 00:00:00 2001 From: Raz Luvaton <16746759+rluvaton@users.noreply.github.com> Date: Thu, 2 May 2024 00:45:34 +0300 Subject: [PATCH 43/69] events: fix not returning this --- lib/internal/events/event_emitter.js | 10 ++++++++-- lib/internal/events/slow_event_emitter.js | 4 +--- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/lib/internal/events/event_emitter.js b/lib/internal/events/event_emitter.js index 64176ccfb4aa58..499050e3b03961 100644 --- a/lib/internal/events/event_emitter.js +++ b/lib/internal/events/event_emitter.js @@ -465,7 +465,9 @@ EventEmitter.prototype.addListener = function addListener(type, listener) { this[kSwitchToSlowPath](); } - return this[kImpl].addListener(type, listener); + this[kImpl].addListener(type, listener); + + return this; }; EventEmitter.prototype.on = EventEmitter.prototype.addListener; @@ -493,7 +495,9 @@ EventEmitter.prototype.prependListener = this[kSwitchToSlowPath](); } - return this[kImpl].addListener(type, listener, true); + this[kImpl].addListener(type, listener, true); + + return this; }; function onceWrapper() { @@ -524,6 +528,7 @@ EventEmitter.prototype.once = function once(type, listener) { checkListener(listener); this.on(type, _onceWrap(this, type, listener)); + return this; }; @@ -539,6 +544,7 @@ EventEmitter.prototype.prependOnceListener = checkListener(listener); this.prependListener(type, _onceWrap(this, type, listener)); + return this; }; diff --git a/lib/internal/events/slow_event_emitter.js b/lib/internal/events/slow_event_emitter.js index e89746a40628f7..44c9c235427d06 100644 --- a/lib/internal/events/slow_event_emitter.js +++ b/lib/internal/events/slow_event_emitter.js @@ -157,8 +157,6 @@ SlowEventEmitter.prototype.addListener = function addListener(type, listener, pr process.emitWarning(w); } } - - return this; }; /** @@ -199,7 +197,7 @@ SlowEventEmitter.prototype.removeListener = function removeListener(type, listen } if (position < 0) - return this; + return undefined; if (position === 0) list.shift(); From 6422d22b748dc99bf769f0f048e87cb0f99cc41d Mon Sep 17 00:00:00 2001 From: Raz Luvaton <16746759+rluvaton@users.noreply.github.com> Date: Thu, 2 May 2024 00:45:55 +0300 Subject: [PATCH 44/69] events: add internal event emitter to bootstrap module --- test/parallel/test-bootstrap-modules.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/test/parallel/test-bootstrap-modules.js b/test/parallel/test-bootstrap-modules.js index 6327fbeb2e7e1b..e3d67116599141 100644 --- a/test/parallel/test-bootstrap-modules.js +++ b/test/parallel/test-bootstrap-modules.js @@ -100,6 +100,11 @@ expected.beforePreExec = new Set([ 'NativeModule internal/modules/cjs/loader', 'Internal Binding wasm_web_api', 'NativeModule internal/events/abort_listener', + 'NativeModule internal/events/event_emitter', + 'NativeModule internal/events/fast_event_emitter', + 'NativeModule internal/events/shared_internal_event_emitter', + 'NativeModule internal/events/slow_event_emitter', + 'NativeModule internal/events/symbols', ]); expected.atRunTime = new Set([ From d880794ede637ba5682bf77bff2c0f7ef7dab52b Mon Sep 17 00:00:00 2001 From: Raz Luvaton <16746759+rluvaton@users.noreply.github.com> Date: Thu, 2 May 2024 00:59:50 +0300 Subject: [PATCH 45/69] events: fix lint issues --- lib/internal/events/slow_event_emitter.js | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/internal/events/slow_event_emitter.js b/lib/internal/events/slow_event_emitter.js index 44c9c235427d06..a2f84a795d8640 100644 --- a/lib/internal/events/slow_event_emitter.js +++ b/lib/internal/events/slow_event_emitter.js @@ -102,7 +102,6 @@ SlowEventEmitter.prototype.emit = function emit(type, ...args) { * Adds a listener to the event emitter. * @param {string | symbol} type * @param {Function} listener - * @returns {EventEmitter} */ SlowEventEmitter.prototype.addListener = function addListener(type, listener, prepend) { const target = this.eventEmitterTranslationLayer; From c7444a5248749176c959ba94669a8b5b95f8a49e Mon Sep 17 00:00:00 2001 From: Raz Luvaton <16746759+rluvaton@users.noreply.github.com> Date: Thu, 2 May 2024 01:07:51 +0300 Subject: [PATCH 46/69] events: make initial events be preferred over missing events --- lib/internal/events/event_emitter.js | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/lib/internal/events/event_emitter.js b/lib/internal/events/event_emitter.js index 499050e3b03961..5547cf05ec2e67 100644 --- a/lib/internal/events/event_emitter.js +++ b/lib/internal/events/event_emitter.js @@ -357,18 +357,19 @@ EventEmitter.init = function(opts) { const thisEvents = this._events; const impl = this[kImpl]; + const missingEvents = thisEvents === undefined; - if (thisEvents === undefined || + if (missingEvents && this[kInitialEvents] !== undefined) { + impl._events = this[kInitialEvents]; + this[kInitialEvents] = undefined; + impl[kShapeMode] = true; + } else if (missingEvents || thisEvents === (thisPrototype || ObjectGetPrototypeOf(this))._events) { // In fast path we don't want to have __proto__: null as it will cause the object to be in dictionary mode // and will slow down the access to the properties by a lot (around 2x) impl._events = this[kIsFastPath] ? {} : { __proto__: null }; impl._eventsCount = 0; impl[kShapeMode] = false; - } else if (this[kInitialEvents] !== undefined) { - impl._events = this[kInitialEvents]; - this[kInitialEvents] = undefined; - impl[kShapeMode] = true; } this._maxListeners = this._maxListeners || undefined; From 01f2e881c6d53c5749490c73f84675eed4966791 Mon Sep 17 00:00:00 2001 From: Raz Luvaton <16746759+rluvaton@users.noreply.github.com> Date: Thu, 2 May 2024 01:20:49 +0300 Subject: [PATCH 47/69] events: fix fixture after files move --- .../errors/events_unhandled_error_common_trace.snapshot | 6 +++--- .../errors/events_unhandled_error_nexttick.snapshot | 6 +++--- .../errors/events_unhandled_error_sameline.snapshot | 6 +++--- .../errors/events_unhandled_error_subclass.snapshot | 6 +++--- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/test/fixtures/errors/events_unhandled_error_common_trace.snapshot b/test/fixtures/errors/events_unhandled_error_common_trace.snapshot index a482c105b75e48..9e1e47f86d6ec8 100644 --- a/test/fixtures/errors/events_unhandled_error_common_trace.snapshot +++ b/test/fixtures/errors/events_unhandled_error_common_trace.snapshot @@ -1,6 +1,6 @@ -node:events:* - throw er; * Unhandled 'error' event - ^ +node:internal*events*shared_internal_event_emitter:* + throw er; * Unhandled 'error' event + ^ Error: foo:bar at bar (*events_unhandled_error_common_trace.js:*:*) diff --git a/test/fixtures/errors/events_unhandled_error_nexttick.snapshot b/test/fixtures/errors/events_unhandled_error_nexttick.snapshot index 450d4910a385b5..abcdb9da0d7224 100644 --- a/test/fixtures/errors/events_unhandled_error_nexttick.snapshot +++ b/test/fixtures/errors/events_unhandled_error_nexttick.snapshot @@ -1,6 +1,6 @@ -node:events:* - throw er; * Unhandled 'error' event - ^ +node:internal*events*shared_internal_event_emitter:* + throw er; * Unhandled 'error' event + ^ Error at Object. (*events_unhandled_error_nexttick.js:*:*) diff --git a/test/fixtures/errors/events_unhandled_error_sameline.snapshot b/test/fixtures/errors/events_unhandled_error_sameline.snapshot index 520601e617083c..6c7a7d78b8b44c 100644 --- a/test/fixtures/errors/events_unhandled_error_sameline.snapshot +++ b/test/fixtures/errors/events_unhandled_error_sameline.snapshot @@ -1,6 +1,6 @@ -node:events:* - throw er; * Unhandled 'error' event - ^ +node:internal*events*shared_internal_event_emitter:* + throw er; * Unhandled 'error' event + ^ Error at Object. (*events_unhandled_error_sameline.js:*:*) diff --git a/test/fixtures/errors/events_unhandled_error_subclass.snapshot b/test/fixtures/errors/events_unhandled_error_subclass.snapshot index 6a9cfd4a1a0b21..a5c6e9ce829f6a 100644 --- a/test/fixtures/errors/events_unhandled_error_subclass.snapshot +++ b/test/fixtures/errors/events_unhandled_error_subclass.snapshot @@ -1,6 +1,6 @@ -node:events:* - throw er; * Unhandled 'error' event - ^ +node:internal*events*shared_internal_event_emitter:* + throw er; * Unhandled 'error' event + ^ Error at Object. (*events_unhandled_error_subclass.js:*:*) From 09b92d5947e228ffdff87cdb654cf1ceaef59330 Mon Sep 17 00:00:00 2001 From: Raz Luvaton <16746759+rluvaton@users.noreply.github.com> Date: Thu, 2 May 2024 01:29:06 +0300 Subject: [PATCH 48/69] events: allow calling using different instance --- lib/internal/events/event_emitter.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/internal/events/event_emitter.js b/lib/internal/events/event_emitter.js index 5547cf05ec2e67..b1149f0253843d 100644 --- a/lib/internal/events/event_emitter.js +++ b/lib/internal/events/event_emitter.js @@ -253,7 +253,7 @@ ObjectDefineProperties(EventEmitter.prototype, { set: function(arg) { // If using the _events setter move to slow path to avoid bugs with incorrect shape or functions // Users should not interact with _events directly - this[kSwitchToSlowPath](); + EventEmitter.prototype[kSwitchToSlowPath].call(this); this[kImpl]._events = arg; }, }, @@ -463,7 +463,7 @@ EventEmitter.prototype.addListener = function addListener(type, listener) { // If the listener is already added, // switch to slow path as the fast path optimized for single listener for each event if (this[kIsFastPath] && (isEventUnsupportedForFastPath(type) || this[kImpl].isListenerAlreadyExists(type))) { - this[kSwitchToSlowPath](); + EventEmitter.prototype[kSwitchToSlowPath].call(this); } this[kImpl].addListener(type, listener); @@ -493,7 +493,7 @@ EventEmitter.prototype.prependListener = // If the listener is already added, // switch to slow path as the fast path optimized for single listener for each event if (this[kIsFastPath] && (isEventUnsupportedForFastPath(type) || this[kImpl].isListenerAlreadyExists(type))) { - this[kSwitchToSlowPath](); + EventEmitter.prototype[kSwitchToSlowPath].call(this); } this[kImpl].addListener(type, listener, true); From 3db33d68467188af5fafd413cd7adf5251c57ebf Mon Sep 17 00:00:00 2001 From: Raz Luvaton <16746759+rluvaton@users.noreply.github.com> Date: Thu, 2 May 2024 01:31:19 +0300 Subject: [PATCH 49/69] events: fix missing symbol --- lib/internal/events/event_emitter.js | 3 +-- lib/internal/events/symbols.js | 2 ++ 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/internal/events/event_emitter.js b/lib/internal/events/event_emitter.js index b1149f0253843d..f20c876734f346 100644 --- a/lib/internal/events/event_emitter.js +++ b/lib/internal/events/event_emitter.js @@ -10,9 +10,7 @@ const { ReflectApply, ReflectOwnKeys, Symbol, - SymbolFor, } = primordials; -const kRejection = SymbolFor('nodejs.rejection'); const { codes: { @@ -37,6 +35,7 @@ const { kIsFastPath, kSwitchToSlowPath, kInitialEvents, + kRejection, } = require('internal/events/symbols'); const { arrayClone, diff --git a/lib/internal/events/symbols.js b/lib/internal/events/symbols.js index f357fa4159c666..9f15f874cb5223 100644 --- a/lib/internal/events/symbols.js +++ b/lib/internal/events/symbols.js @@ -19,6 +19,7 @@ const kWatermarkData = SymbolFor('nodejs.watermarkData'); const kImpl = Symbol('kImpl'); const kIsFastPath = Symbol('kIsFastPath'); const kSwitchToSlowPath = Symbol('kSwitchToSlowPath'); +const kRejection = SymbolFor('nodejs.rejection'); module.exports = { kFirstEventParam, @@ -32,4 +33,5 @@ module.exports = { kImpl, kIsFastPath, kSwitchToSlowPath, + kRejection, }; From fb9f74c9e6048ac5bf72a0ecaac31d2f781ead22 Mon Sep 17 00:00:00 2001 From: Raz Luvaton <16746759+rluvaton@users.noreply.github.com> Date: Thu, 2 May 2024 01:32:43 +0300 Subject: [PATCH 50/69] events: fix not using the right target --- lib/internal/events/shared_internal_event_emitter.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/internal/events/shared_internal_event_emitter.js b/lib/internal/events/shared_internal_event_emitter.js index 8e5e6021f812f2..b22c0157c92b7a 100644 --- a/lib/internal/events/shared_internal_event_emitter.js +++ b/lib/internal/events/shared_internal_event_emitter.js @@ -60,22 +60,22 @@ function addCatch(that, promise, type, args) { } function emitUnhandledRejectionOrErr(ee, err, type, args) { - if (typeof ee.eventEmitterTranslationLayer[kRejection] === 'function') { - ee.eventEmitterTranslationLayer[kRejection](err, type, ...args); + if (typeof ee[kRejection] === 'function') { + ee[kRejection](err, type, ...args); } else { // We have to disable the capture rejections mechanism, otherwise // we might end up in an infinite loop. - const prev = ee.eventEmitterTranslationLayer[kCapture]; + const prev = ee[kCapture]; // If the error handler throws, it is not catchable and it // will end up in 'uncaughtException'. We restore the previous // value of kCapture in case the uncaughtException is present // and the exception is handled. try { - ee.eventEmitterTranslationLayer[kCapture] = false; + ee[kCapture] = false; ee.emit('error', err); } finally { - ee.eventEmitterTranslationLayer[kCapture] = prev; + ee[kCapture] = prev; } } } From d395895a67cf3e6f9fc7a7234b7c40eaa1c2e1a9 Mon Sep 17 00:00:00 2001 From: Raz Luvaton <16746759+rluvaton@users.noreply.github.com> Date: Thu, 2 May 2024 01:42:36 +0300 Subject: [PATCH 51/69] events: fix fail to spread --- lib/internal/events/event_emitter.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/internal/events/event_emitter.js b/lib/internal/events/event_emitter.js index f20c876734f346..4970b9e2882e28 100644 --- a/lib/internal/events/event_emitter.js +++ b/lib/internal/events/event_emitter.js @@ -441,7 +441,7 @@ EventEmitter.prototype.emit = function emit(type, ...args) { throwErrorOnMissingErrorHandler.apply(this, args); } - return impl !== undefined ? impl.emit(type, ...args) : false; + return impl !== undefined ? impl.emit.apply(impl, arguments) : false; }; /** From d08b36cc9a7de05358482a2945fd70ca7c77e5e8 Mon Sep 17 00:00:00 2001 From: Raz Luvaton <16746759+rluvaton@users.noreply.github.com> Date: Thu, 2 May 2024 12:10:45 +0300 Subject: [PATCH 52/69] events: update --- lib/internal/events/event_emitter.js | 2 +- lib/internal/events/slow_event_emitter.js | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/internal/events/event_emitter.js b/lib/internal/events/event_emitter.js index 4970b9e2882e28..b365a417b471d8 100644 --- a/lib/internal/events/event_emitter.js +++ b/lib/internal/events/event_emitter.js @@ -574,7 +574,7 @@ EventEmitter.prototype.off = EventEmitter.prototype.removeListener; */ EventEmitter.prototype.removeAllListeners = function removeAllListeners(type) { - this[kImpl].removeAllListeners(type); + this[kImpl].removeAllListeners(arguments); return this; }; diff --git a/lib/internal/events/slow_event_emitter.js b/lib/internal/events/slow_event_emitter.js index a2f84a795d8640..ecf774dd8d2efc 100644 --- a/lib/internal/events/slow_event_emitter.js +++ b/lib/internal/events/slow_event_emitter.js @@ -247,9 +247,9 @@ SlowEventEmitter.prototype.removeAllListeners = function removeAllListeners(type if (arguments.length === 0) { for (const key of ReflectOwnKeys(events)) { if (key === 'removeListener') continue; - this.removeAllListeners(key); + this.eventEmitterTranslationLayer.removeAllListeners(key); } - this.removeAllListeners('removeListener'); + this.eventEmitterTranslationLayer.removeAllListeners('removeListener'); this._events = { __proto__: null }; this._eventsCount = 0; this[kShapeMode] = false; @@ -259,11 +259,11 @@ SlowEventEmitter.prototype.removeAllListeners = function removeAllListeners(type const listeners = events[type]; if (typeof listeners === 'function') { - this.removeListener(type, listeners); + this.eventEmitterTranslationLayer.removeListener(type, listeners); } else if (listeners !== undefined) { // LIFO order for (let i = listeners.length - 1; i >= 0; i--) { - this.removeListener(type, listeners[i]); + this.eventEmitterTranslationLayer.removeListener(type, listeners[i]); } } From 3a88c1638e411a4041e3cb1d3e3e4db4477168e4 Mon Sep 17 00:00:00 2001 From: Raz Luvaton <16746759+rluvaton@users.noreply.github.com> Date: Thu, 2 May 2024 12:34:12 +0300 Subject: [PATCH 53/69] events: fix failing to remove all listeners --- lib/internal/events/event_emitter.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/internal/events/event_emitter.js b/lib/internal/events/event_emitter.js index b365a417b471d8..3ddcd06929409f 100644 --- a/lib/internal/events/event_emitter.js +++ b/lib/internal/events/event_emitter.js @@ -574,7 +574,7 @@ EventEmitter.prototype.off = EventEmitter.prototype.removeListener; */ EventEmitter.prototype.removeAllListeners = function removeAllListeners(type) { - this[kImpl].removeAllListeners(arguments); + this[kImpl].removeAllListeners.apply(this[kImpl], arguments); return this; }; From eb9784fb490b6ce4b95aea7c1d8d244618b3c575 Mon Sep 17 00:00:00 2001 From: Raz Luvaton <16746759+rluvaton@users.noreply.github.com> Date: Thu, 2 May 2024 15:08:02 +0300 Subject: [PATCH 54/69] events: add support for object prototype keys as event names --- lib/internal/events/event_emitter.js | 27 ++++++++++++++++--- lib/internal/events/slow_event_emitter.js | 3 +++ .../test-event-emitter-special-event-names.js | 3 --- 3 files changed, 26 insertions(+), 7 deletions(-) diff --git a/lib/internal/events/event_emitter.js b/lib/internal/events/event_emitter.js index 3ddcd06929409f..1101805414f53d 100644 --- a/lib/internal/events/event_emitter.js +++ b/lib/internal/events/event_emitter.js @@ -215,10 +215,29 @@ ObjectDefineProperty(EventEmitter.prototype, kCapture, { }); function isEventUnsupportedForFastPath(type) { - // Not supporting newListener and removeListener events in fast path - // as they can add new listeners in the middle of the process - // and also not supporting errorMonitor event as it has special handling - return type === 'newListener' || type === 'removeListener' || type === kErrorMonitor; + return ( + // Not supporting newListener and removeListener events in fast path + // as they can add new listeners in the middle of the process + type === 'newListener' || + type === 'removeListener' || + + // Not supporting errorMonitor event as it has special handling + type === kErrorMonitor + + // Not supporting Object prototype keys as fast path object is not an object with null prototype + // type === 'constructor' || + // type === '__defineGetter__' || + // type === '__defineSetter__' || + // type === 'hasOwnProperty' || + // type === '__lookupGetter__' || + // type === '__lookupSetter__' || + // type === 'isPrototypeOf' || + // type === 'propertyIsEnumerable' || + // type === 'toString' || + // type === 'valueOf' || + // type === '__proto__' || + // type === 'toLocaleString' + ); } ObjectDefineProperties(EventEmitter.prototype, { diff --git a/lib/internal/events/slow_event_emitter.js b/lib/internal/events/slow_event_emitter.js index ecf774dd8d2efc..e1e61bc2232cd3 100644 --- a/lib/internal/events/slow_event_emitter.js +++ b/lib/internal/events/slow_event_emitter.js @@ -32,6 +32,9 @@ SlowEventEmitter.prototype[kShapeMode] = undefined; * @param {FastEventEmitter} fastEventEmitter */ SlowEventEmitter.fromFastEventEmitter = function fromFastEventEmitter(fastEventEmitter) { + // To support weird cases where the event name is object keys such as __defineGetter__ + // eslint-disable-next-line no-proto + fastEventEmitter._events.__proto__ = null; const eventEmitter = new SlowEventEmitter(fastEventEmitter.eventEmitterTranslationLayer, fastEventEmitter._events); eventEmitter._eventsCount = fastEventEmitter._eventsCount; diff --git a/test/parallel/test-event-emitter-special-event-names.js b/test/parallel/test-event-emitter-special-event-names.js index f34faba9468cc2..256dc794ccbbf9 100644 --- a/test/parallel/test-event-emitter-special-event-names.js +++ b/test/parallel/test-event-emitter-special-event-names.js @@ -9,9 +9,6 @@ const handler = () => {}; assert.deepStrictEqual(ee.eventNames(), []); -assert.strictEqual(ee._events.hasOwnProperty, undefined); -assert.strictEqual(ee._events.toString, undefined); - ee.on('__proto__', handler); ee.on('__defineGetter__', handler); ee.on('toString', handler); From 3928044aff77b31a1dde8747496e9e90a6576bb1 Mon Sep 17 00:00:00 2001 From: Raz Luvaton <16746759+rluvaton@users.noreply.github.com> Date: Thu, 2 May 2024 15:08:51 +0300 Subject: [PATCH 55/69] events: remove check for events should not be an object --- test/parallel/test-event-emitter-listeners-side-effects.js | 1 - .../test-event-emitter-set-max-listeners-side-effects.js | 1 - 2 files changed, 2 deletions(-) diff --git a/test/parallel/test-event-emitter-listeners-side-effects.js b/test/parallel/test-event-emitter-listeners-side-effects.js index 3e427c4c284ea2..c11b48d733f167 100644 --- a/test/parallel/test-event-emitter-listeners-side-effects.js +++ b/test/parallel/test-event-emitter-listeners-side-effects.js @@ -32,7 +32,6 @@ let fl; // foo listeners fl = e.listeners('foo'); assert(Array.isArray(fl)); assert.strictEqual(fl.length, 0); -assert(!(e._events instanceof Object)); assert.deepStrictEqual(Object.keys(e._events), []); e.on('foo', assert.fail); diff --git a/test/parallel/test-event-emitter-set-max-listeners-side-effects.js b/test/parallel/test-event-emitter-set-max-listeners-side-effects.js index 8e66e999a54cab..79d90b322de94d 100644 --- a/test/parallel/test-event-emitter-set-max-listeners-side-effects.js +++ b/test/parallel/test-event-emitter-set-max-listeners-side-effects.js @@ -26,7 +26,6 @@ const events = require('events'); const e = new events.EventEmitter(); -assert(!(e._events instanceof Object)); assert.deepStrictEqual(Object.keys(e._events), []); e.setMaxListeners(5); assert.deepStrictEqual(Object.keys(e._events), []); From 0632bd7518930a746fab2a0c352072403900b06b Mon Sep 17 00:00:00 2001 From: Raz Luvaton <16746759+rluvaton@users.noreply.github.com> Date: Thu, 2 May 2024 15:12:19 +0300 Subject: [PATCH 56/69] events: add support for object prototype keys as event names --- lib/internal/events/event_emitter.js | 30 ++++++++++++++-------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/lib/internal/events/event_emitter.js b/lib/internal/events/event_emitter.js index 1101805414f53d..ab6f00ad665aa7 100644 --- a/lib/internal/events/event_emitter.js +++ b/lib/internal/events/event_emitter.js @@ -222,21 +222,21 @@ function isEventUnsupportedForFastPath(type) { type === 'removeListener' || // Not supporting errorMonitor event as it has special handling - type === kErrorMonitor - - // Not supporting Object prototype keys as fast path object is not an object with null prototype - // type === 'constructor' || - // type === '__defineGetter__' || - // type === '__defineSetter__' || - // type === 'hasOwnProperty' || - // type === '__lookupGetter__' || - // type === '__lookupSetter__' || - // type === 'isPrototypeOf' || - // type === 'propertyIsEnumerable' || - // type === 'toString' || - // type === 'valueOf' || - // type === '__proto__' || - // type === 'toLocaleString' + type === kErrorMonitor || + + // Not supporting Object prototype keys as fast path object is not an object with null prototype + type === 'constructor' || + type === '__defineGetter__' || + type === '__defineSetter__' || + type === 'hasOwnProperty' || + type === '__lookupGetter__' || + type === '__lookupSetter__' || + type === 'isPrototypeOf' || + type === 'propertyIsEnumerable' || + type === 'toString' || + type === 'valueOf' || + type === '__proto__' || + type === 'toLocaleString' ); } From 486e89945a732d4367bbd34bcc0c65ae2c0019fe Mon Sep 17 00:00:00 2001 From: Raz Luvaton <16746759+rluvaton@users.noreply.github.com> Date: Thu, 2 May 2024 15:22:14 +0300 Subject: [PATCH 57/69] events: fix emit event name from object prototype keys --- lib/internal/events/event_emitter.js | 6 ++++++ test/parallel/test-event-emitter-special-event-names.js | 8 ++++++++ 2 files changed, 14 insertions(+) diff --git a/lib/internal/events/event_emitter.js b/lib/internal/events/event_emitter.js index ab6f00ad665aa7..37f93206826284 100644 --- a/lib/internal/events/event_emitter.js +++ b/lib/internal/events/event_emitter.js @@ -460,6 +460,12 @@ EventEmitter.prototype.emit = function emit(type, ...args) { throwErrorOnMissingErrorHandler.apply(this, args); } + // This can happen when calling emit for function that was not listened to and we are still in the fast path + // this is to avoid calling object prototype functions + if(this[kIsFastPath] === true && isEventUnsupportedForFastPath(type) === true) { + return false; + } + return impl !== undefined ? impl.emit.apply(impl, arguments) : false; }; diff --git a/test/parallel/test-event-emitter-special-event-names.js b/test/parallel/test-event-emitter-special-event-names.js index 256dc794ccbbf9..d6d0c0c5310c37 100644 --- a/test/parallel/test-event-emitter-special-event-names.js +++ b/test/parallel/test-event-emitter-special-event-names.js @@ -32,3 +32,11 @@ process.on('__proto__', common.mustCall(function(val) { assert.strictEqual(val, 1); })); process.emit('__proto__', 1); + +const objectPrototypeKeys = Object.getOwnPropertyNames(Object.getPrototypeOf({})); + +assert.notDeepStrictEqual(objectPrototypeKeys, []); +for (const key of objectPrototypeKeys) { + const ee2 = new EventEmitter(); + ee2.emit(key, 1); +} From 4d5ee43c240d629d0e221b532a1a4edcb4f64c14 Mon Sep 17 00:00:00 2001 From: Raz Luvaton <16746759+rluvaton@users.noreply.github.com> Date: Thu, 2 May 2024 15:22:42 +0300 Subject: [PATCH 58/69] events: format --- lib/internal/events/event_emitter.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/internal/events/event_emitter.js b/lib/internal/events/event_emitter.js index 37f93206826284..2a11a2a9702346 100644 --- a/lib/internal/events/event_emitter.js +++ b/lib/internal/events/event_emitter.js @@ -462,7 +462,7 @@ EventEmitter.prototype.emit = function emit(type, ...args) { // This can happen when calling emit for function that was not listened to and we are still in the fast path // this is to avoid calling object prototype functions - if(this[kIsFastPath] === true && isEventUnsupportedForFastPath(type) === true) { + if (this[kIsFastPath] === true && isEventUnsupportedForFastPath(type) === true) { return false; } From 62c7a014efae222f95c7f6c282c67a7758b036b4 Mon Sep 17 00:00:00 2001 From: Raz Luvaton <16746759+rluvaton@users.noreply.github.com> Date: Thu, 2 May 2024 15:33:48 +0300 Subject: [PATCH 59/69] events: move isEventUnsupportedForFastPath to fast event emitter file --- lib/internal/events/event_emitter.js | 28 +----------------- lib/internal/events/fast_event_emitter.js | 36 +++++++++++++++++++---- 2 files changed, 32 insertions(+), 32 deletions(-) diff --git a/lib/internal/events/event_emitter.js b/lib/internal/events/event_emitter.js index 2a11a2a9702346..cb2869216d1ebd 100644 --- a/lib/internal/events/event_emitter.js +++ b/lib/internal/events/event_emitter.js @@ -42,7 +42,7 @@ const { throwErrorOnMissingErrorHandler, } = require('internal/events/shared_internal_event_emitter'); const { SlowEventEmitter } = require('internal/events/slow_event_emitter'); -const { FastEventEmitter } = require('internal/events/fast_event_emitter'); +const { FastEventEmitter, isEventUnsupportedForFastPath } = require('internal/events/fast_event_emitter'); let EventEmitterAsyncResource; @@ -214,32 +214,6 @@ ObjectDefineProperty(EventEmitter.prototype, kCapture, { enumerable: false, }); -function isEventUnsupportedForFastPath(type) { - return ( - // Not supporting newListener and removeListener events in fast path - // as they can add new listeners in the middle of the process - type === 'newListener' || - type === 'removeListener' || - - // Not supporting errorMonitor event as it has special handling - type === kErrorMonitor || - - // Not supporting Object prototype keys as fast path object is not an object with null prototype - type === 'constructor' || - type === '__defineGetter__' || - type === '__defineSetter__' || - type === 'hasOwnProperty' || - type === '__lookupGetter__' || - type === '__lookupSetter__' || - type === 'isPrototypeOf' || - type === 'propertyIsEnumerable' || - type === 'toString' || - type === 'valueOf' || - type === '__proto__' || - type === 'toLocaleString' - ); -} - ObjectDefineProperties(EventEmitter.prototype, { [kImpl]: { __proto__: null, diff --git a/lib/internal/events/fast_event_emitter.js b/lib/internal/events/fast_event_emitter.js index 726609c42d0f42..0190f7bb148e99 100644 --- a/lib/internal/events/fast_event_emitter.js +++ b/lib/internal/events/fast_event_emitter.js @@ -1,6 +1,6 @@ 'use strict'; -const { kShapeMode } = require('internal/events/symbols'); +const { kShapeMode, kErrorMonitor } = require('internal/events/symbols'); const { throwErrorOnMissingErrorHandler, addCatch, @@ -9,10 +9,7 @@ const { /** * This class is optimized for the case where there is only a single listener for each event. * it supports kCapture - * but does not support event types with the following names: - * 1. 'newListener' and 'removeListener' as they can cause another event to be added - * and making the code less predictable thus harder to optimize - * 2. kErrorMonitor as it has special handling + * but does not support event types that {@link isEventUnsupportedForFastPath} returns false for */ function FastEventEmitter(eventEmitterTranslationLayer, _events) { this.eventEmitterTranslationLayer = eventEmitterTranslationLayer; @@ -138,6 +135,35 @@ FastEventEmitter.prototype.removeAllListeners = function removeAllListeners(type return undefined; }; + +function isEventUnsupportedForFastPath(type) { + return ( + // Not supporting newListener and removeListener events in fast path + // as they can add new listeners in the middle of the process + type === 'newListener' || + type === 'removeListener' || + + // Not supporting errorMonitor event as it has special handling + type === kErrorMonitor || + + // Not supporting Object prototype keys as fast path object is not an object with null prototype + type === 'constructor' || + type === '__defineGetter__' || + type === '__defineSetter__' || + type === 'hasOwnProperty' || + type === '__lookupGetter__' || + type === '__lookupSetter__' || + type === 'isPrototypeOf' || + type === 'propertyIsEnumerable' || + type === 'toString' || + type === 'valueOf' || + type === '__proto__' || + type === 'toLocaleString' + ); +} + + module.exports = { FastEventEmitter, + isEventUnsupportedForFastPath, }; From d3f8345335fc0f79e6e0f84f46929759af7cd098 Mon Sep 17 00:00:00 2001 From: Raz Luvaton <16746759+rluvaton@users.noreply.github.com> Date: Thu, 2 May 2024 15:55:06 +0300 Subject: [PATCH 60/69] events: fix test-disable-proto-delete.js test --- lib/internal/events/slow_event_emitter.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/internal/events/slow_event_emitter.js b/lib/internal/events/slow_event_emitter.js index e1e61bc2232cd3..8e89cf80444eef 100644 --- a/lib/internal/events/slow_event_emitter.js +++ b/lib/internal/events/slow_event_emitter.js @@ -69,7 +69,7 @@ SlowEventEmitter.prototype.emit = function emit(type, ...args) { const handler = events[type]; - if (handler === undefined) + if (handler === undefined || handler === null) return false; if (typeof handler === 'function') { @@ -130,7 +130,7 @@ SlowEventEmitter.prototype.addListener = function addListener(type, listener, pr existing = events[type]; } - if (existing === undefined) { + if (existing === undefined || existing === null) { // Optimize the case of one listener. Don't need the extra array object. events[type] = listener; ++this._eventsCount; @@ -173,7 +173,7 @@ SlowEventEmitter.prototype.removeListener = function removeListener(type, listen return undefined; const list = events[type]; - if (list === undefined) + if (list === undefined || list === null) return undefined; if (list === listener || list.listener === listener) { @@ -263,7 +263,7 @@ SlowEventEmitter.prototype.removeAllListeners = function removeAllListeners(type if (typeof listeners === 'function') { this.eventEmitterTranslationLayer.removeListener(type, listeners); - } else if (listeners !== undefined) { + } else if (listeners !== undefined && listeners !== null) { // LIFO order for (let i = listeners.length - 1; i >= 0; i--) { this.eventEmitterTranslationLayer.removeListener(type, listeners[i]); From 94447a49b275a5744627daa477dda6331dca6f59 Mon Sep 17 00:00:00 2001 From: Raz Luvaton <16746759+rluvaton@users.noreply.github.com> Date: Thu, 2 May 2024 15:55:28 +0300 Subject: [PATCH 61/69] events: update jsdoc --- lib/internal/events/slow_event_emitter.js | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/internal/events/slow_event_emitter.js b/lib/internal/events/slow_event_emitter.js index 8e89cf80444eef..77f0d282571dd1 100644 --- a/lib/internal/events/slow_event_emitter.js +++ b/lib/internal/events/slow_event_emitter.js @@ -105,6 +105,7 @@ SlowEventEmitter.prototype.emit = function emit(type, ...args) { * Adds a listener to the event emitter. * @param {string | symbol} type * @param {Function} listener + * @param prepend */ SlowEventEmitter.prototype.addListener = function addListener(type, listener, prepend) { const target = this.eventEmitterTranslationLayer; From 0a9177e377fa063e18cdd519894bc6ef79201495 Mon Sep 17 00:00:00 2001 From: Raz Luvaton <16746759+rluvaton@users.noreply.github.com> Date: Thu, 2 May 2024 16:16:02 +0300 Subject: [PATCH 62/69] events: add support for --disable-proto=throw and --disable-proto=delete --- lib/internal/events/slow_event_emitter.js | 36 ++++++++++++++++++----- 1 file changed, 29 insertions(+), 7 deletions(-) diff --git a/lib/internal/events/slow_event_emitter.js b/lib/internal/events/slow_event_emitter.js index 77f0d282571dd1..5b0429fabf3ca1 100644 --- a/lib/internal/events/slow_event_emitter.js +++ b/lib/internal/events/slow_event_emitter.js @@ -1,6 +1,9 @@ 'use strict'; const { + ObjectAssign, + ObjectPrototype, + ObjectPrototypeHasOwnProperty, ReflectOwnKeys, String, } = primordials; @@ -17,6 +20,20 @@ const { genericNodeError } = require('internal/errors'); const { inspect } = require('internal/util/inspect'); let _getMaxListeners; +let protoExists = true; + +try { + // Can't use getOptionValue('--disable-proto') here as it's not available yet + + // Checking whether --disable-proto is equal 'throw' + // eslint-disable-next-line no-proto, no-unused-expressions + ({}).__proto__; + + // Checking whether --disable-proto is equal 'delete' + protoExists = ObjectPrototypeHasOwnProperty(ObjectPrototype, '__proto__'); +} catch { + protoExists = false; +} function SlowEventEmitter(eventEmitterTranslationLayer, events = undefined) { this.eventEmitterTranslationLayer = eventEmitterTranslationLayer; @@ -32,10 +49,15 @@ SlowEventEmitter.prototype[kShapeMode] = undefined; * @param {FastEventEmitter} fastEventEmitter */ SlowEventEmitter.fromFastEventEmitter = function fromFastEventEmitter(fastEventEmitter) { + let events = fastEventEmitter._events; // To support weird cases where the event name is object keys such as __defineGetter__ + if (protoExists) { // eslint-disable-next-line no-proto - fastEventEmitter._events.__proto__ = null; - const eventEmitter = new SlowEventEmitter(fastEventEmitter.eventEmitterTranslationLayer, fastEventEmitter._events); + events.__proto__ = null; + } else { + events = ObjectAssign({ __proto__: null }, events); + } + const eventEmitter = new SlowEventEmitter(fastEventEmitter.eventEmitterTranslationLayer, events); eventEmitter._eventsCount = fastEventEmitter._eventsCount; eventEmitter[kShapeMode] = fastEventEmitter[kShapeMode]; @@ -69,7 +91,7 @@ SlowEventEmitter.prototype.emit = function emit(type, ...args) { const handler = events[type]; - if (handler === undefined || handler === null) + if (handler === undefined) return false; if (typeof handler === 'function') { @@ -105,7 +127,7 @@ SlowEventEmitter.prototype.emit = function emit(type, ...args) { * Adds a listener to the event emitter. * @param {string | symbol} type * @param {Function} listener - * @param prepend + * @param {boolean} prepend */ SlowEventEmitter.prototype.addListener = function addListener(type, listener, prepend) { const target = this.eventEmitterTranslationLayer; @@ -131,7 +153,7 @@ SlowEventEmitter.prototype.addListener = function addListener(type, listener, pr existing = events[type]; } - if (existing === undefined || existing === null) { + if (existing === undefined) { // Optimize the case of one listener. Don't need the extra array object. events[type] = listener; ++this._eventsCount; @@ -174,7 +196,7 @@ SlowEventEmitter.prototype.removeListener = function removeListener(type, listen return undefined; const list = events[type]; - if (list === undefined || list === null) + if (list === undefined) return undefined; if (list === listener || list.listener === listener) { @@ -264,7 +286,7 @@ SlowEventEmitter.prototype.removeAllListeners = function removeAllListeners(type if (typeof listeners === 'function') { this.eventEmitterTranslationLayer.removeListener(type, listeners); - } else if (listeners !== undefined && listeners !== null) { + } else if (listeners !== undefined) { // LIFO order for (let i = listeners.length - 1; i >= 0; i--) { this.eventEmitterTranslationLayer.removeListener(type, listeners[i]); From 0fe12549e76d13b42930da077a8da56a5371edf0 Mon Sep 17 00:00:00 2001 From: Raz Luvaton <16746759+rluvaton@users.noreply.github.com> Date: Thu, 2 May 2024 16:58:26 +0300 Subject: [PATCH 63/69] events: try to fix throw not working in the tests --- lib/internal/events/slow_event_emitter.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/internal/events/slow_event_emitter.js b/lib/internal/events/slow_event_emitter.js index 5b0429fabf3ca1..dc815c83267137 100644 --- a/lib/internal/events/slow_event_emitter.js +++ b/lib/internal/events/slow_event_emitter.js @@ -26,8 +26,8 @@ try { // Can't use getOptionValue('--disable-proto') here as it's not available yet // Checking whether --disable-proto is equal 'throw' - // eslint-disable-next-line no-proto, no-unused-expressions - ({}).__proto__; + // eslint-disable-next-line no-proto + protoExists = !!({}).__proto__; // Checking whether --disable-proto is equal 'delete' protoExists = ObjectPrototypeHasOwnProperty(ObjectPrototype, '__proto__'); From b59447c13ae7cdfd861498499879905102f5f3c3 Mon Sep 17 00:00:00 2001 From: Raz Luvaton <16746759+rluvaton@users.noreply.github.com> Date: Thu, 2 May 2024 18:43:07 +0300 Subject: [PATCH 64/69] events: fix events count --- lib/internal/events/event_emitter.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/lib/internal/events/event_emitter.js b/lib/internal/events/event_emitter.js index cb2869216d1ebd..d983ca83430785 100644 --- a/lib/internal/events/event_emitter.js +++ b/lib/internal/events/event_emitter.js @@ -254,12 +254,17 @@ ObjectDefineProperties(EventEmitter.prototype, { enumerable: true, get: function() { - return this[kImpl]?._eventsCount; + if (this[kImpl] === undefined) { + return 0; + } + + return this[kImpl]._eventsCount; }, set: function(arg) { if (this[kImpl] === undefined) { - return 0; + return; } + this[kImpl]._eventsCount = arg; }, }, From e7873539ccf2420c6ddc21588fa3abbd9ae27618 Mon Sep 17 00:00:00 2001 From: Raz Luvaton <16746759+rluvaton@users.noreply.github.com> Date: Sun, 12 May 2024 22:50:20 +0300 Subject: [PATCH 65/69] events: try to improve stream creation perf --- lib/internal/events/event_emitter.js | 42 +++++++++++++++++++--------- lib/internal/events/symbols.js | 2 ++ lib/internal/streams/readable.js | 15 ++++++---- 3 files changed, 41 insertions(+), 18 deletions(-) diff --git a/lib/internal/events/event_emitter.js b/lib/internal/events/event_emitter.js index d983ca83430785..9709c0c5176e1d 100644 --- a/lib/internal/events/event_emitter.js +++ b/lib/internal/events/event_emitter.js @@ -32,6 +32,7 @@ const { kMaxEventTargetListeners, kMaxEventTargetListenersWarned, kImpl, + kPreInitiated, kIsFastPath, kSwitchToSlowPath, kInitialEvents, @@ -222,6 +223,13 @@ ObjectDefineProperties(EventEmitter.prototype, { configurable: false, writable: true, }, + [kPreInitiated]: { + __proto__: null, + value: false, + enumerable: false, + configurable: false, + writable: true, + }, [kIsFastPath]: { __proto__: null, value: undefined, @@ -240,7 +248,10 @@ ObjectDefineProperties(EventEmitter.prototype, { __proto__: null, enumerable: true, get: function() { - return this[kImpl]?._events; + if(this[kImpl] === undefined) { + return undefined; + } + return this[kImpl]._events; }, set: function(arg) { // If using the _events setter move to slow path to avoid bugs with incorrect shape or functions @@ -347,6 +358,23 @@ EventEmitter.setMaxListeners = EventEmitter.init = function(opts) { let thisPrototype; + this._maxListeners = this._maxListeners || undefined; + + + if (opts?.captureRejections) { + validateBoolean(opts.captureRejections, 'options.captureRejections'); + this[kCapture] = Boolean(opts.captureRejections); + } else { + // Assigning the kCapture property directly saves an expensive + // prototype lookup in a very sensitive hot path. + this[kCapture] = EventEmitter.prototype[kCapture]; + } + + if(this[kPreInitiated] === true) { + this[kPreInitiated] = false; + return; + } + if (this[kImpl] === undefined || (thisPrototype = ObjectGetPrototypeOf(this))[kImpl] === this[kImpl]) { this[kImpl] = new FastEventEmitter(this); this[kIsFastPath] = true; @@ -368,18 +396,6 @@ EventEmitter.init = function(opts) { impl._eventsCount = 0; impl[kShapeMode] = false; } - - this._maxListeners = this._maxListeners || undefined; - - - if (opts?.captureRejections) { - validateBoolean(opts.captureRejections, 'options.captureRejections'); - this[kCapture] = Boolean(opts.captureRejections); - } else { - // Assigning the kCapture property directly saves an expensive - // prototype lookup in a very sensitive hot path. - this[kCapture] = EventEmitter.prototype[kCapture]; - } }; EventEmitter.prototype[kSwitchToSlowPath] = function() { diff --git a/lib/internal/events/symbols.js b/lib/internal/events/symbols.js index 9f15f874cb5223..53980855996d2a 100644 --- a/lib/internal/events/symbols.js +++ b/lib/internal/events/symbols.js @@ -17,6 +17,7 @@ const kMaxEventTargetListenersWarned = const kWatermarkData = SymbolFor('nodejs.watermarkData'); const kImpl = Symbol('kImpl'); +const kPreInitiated = Symbol('kPreInitiated'); const kIsFastPath = Symbol('kIsFastPath'); const kSwitchToSlowPath = Symbol('kSwitchToSlowPath'); const kRejection = SymbolFor('nodejs.rejection'); @@ -31,6 +32,7 @@ module.exports = { kMaxEventTargetListenersWarned, kWatermarkData, kImpl, + kPreInitiated, kIsFastPath, kSwitchToSlowPath, kRejection, diff --git a/lib/internal/streams/readable.js b/lib/internal/streams/readable.js index 0ecb69e5e3de41..bb90cc18145ae7 100644 --- a/lib/internal/streams/readable.js +++ b/lib/internal/streams/readable.js @@ -43,7 +43,7 @@ Readable.ReadableState = ReadableState; const EE = require('events'); const { Stream, prependListener } = require('internal/streams/legacy'); -const { kInitialEvents } = require('internal/events/symbols'); +const { kInitialEvents, kImpl, kShapeMode, kIsFastPath, kPreInitiated} = require('internal/events/symbols'); const { Buffer } = require('buffer'); const { @@ -92,6 +92,7 @@ const FastBuffer = Buffer[SymbolSpecies]; const { StringDecoder } = require('string_decoder'); const from = require('internal/streams/from'); +const {FastEventEmitter} = require("internal/events/fast_event_emitter"); ObjectSetPrototypeOf(Readable.prototype, Stream.prototype); ObjectSetPrototypeOf(Readable, Stream); @@ -320,9 +321,9 @@ function Readable(options) { if (!(this instanceof Readable)) return new Readable(options); - const thisEvents = this._events; - if (thisEvents === undefined || thisEvents === null) { - this[kInitialEvents] ??= { + if (this[kImpl] === undefined && this._events === undefined) { + this[kIsFastPath] = true; + this[kImpl] = new FastEventEmitter(this, this[kInitialEvents] !== undefined ? this[kInitialEvents] :{ close: undefined, error: undefined, data: undefined, @@ -335,7 +336,10 @@ function Readable(options) { // unpipe: undefined, // [destroyImpl.kConstruct]: undefined, // [destroyImpl.kDestroy]: undefined, - }; + }); + this[kImpl][kShapeMode] = true; + } else { + this[kPreInitiated] = false; } this._readableState = new ReadableState(options, this, false); @@ -363,6 +367,7 @@ function Readable(options) { } } +Readable.prototype[kPreInitiated] = true; Readable.prototype.destroy = destroyImpl.destroy; Readable.prototype._undestroy = destroyImpl.undestroy; Readable.prototype._destroy = function(err, cb) { From acf96afb12a447bb786851d5be86d0877da4dfe9 Mon Sep 17 00:00:00 2001 From: Raz Luvaton <16746759+rluvaton@users.noreply.github.com> Date: Sun, 12 May 2024 22:55:26 +0300 Subject: [PATCH 66/69] events: add back changes from c0a6320 --- lib/internal/events/slow_event_emitter.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/internal/events/slow_event_emitter.js b/lib/internal/events/slow_event_emitter.js index dc815c83267137..c7a2b9136fd016 100644 --- a/lib/internal/events/slow_event_emitter.js +++ b/lib/internal/events/slow_event_emitter.js @@ -177,7 +177,7 @@ SlowEventEmitter.prototype.addListener = function addListener(type, listener, pr // No error code for this since it is a Warning const w = genericNodeError( `Possible EventEmitter memory leak detected. ${existing.length} ${String(type)} listeners ` + - `added to ${inspect(target, { depth: -1 })}. Use emitter.setMaxListeners() to increase limit`, + `added to ${inspect(target, { depth: -1 })}. MaxListeners is ${m}. Use emitter.setMaxListeners() to increase limit`, { name: 'MaxListenersExceededWarning', emitter: target, type: type, count: existing.length }); process.emitWarning(w); } From 8758ff2b3a974491d29706d0b7bc7a8eb178979c Mon Sep 17 00:00:00 2001 From: Raz Luvaton <16746759+rluvaton@users.noreply.github.com> Date: Sun, 12 May 2024 23:23:43 +0300 Subject: [PATCH 67/69] events: remove event emitter rebase issue --- lib/events.js | 602 -------------------------------------------------- 1 file changed, 602 deletions(-) diff --git a/lib/events.js b/lib/events.js index b3d877cf047f61..f9bec9a8acf4f3 100644 --- a/lib/events.js +++ b/lib/events.js @@ -65,614 +65,12 @@ const { _getMaxListeners, } = require('internal/events/event_emitter'); - -/** - * Creates a new `EventEmitter` instance. - * @param {{ captureRejections?: boolean; }} [opts] - * @constructs {EventEmitter} - */ module.exports = EventEmitter; module.exports.addAbortListener = addAbortListener; module.exports.once = once; module.exports.on = on; module.exports.getEventListeners = getEventListeners; module.exports.getMaxListeners = getMaxListeners; -// Backwards-compat with node 0.10.x -EventEmitter.EventEmitter = EventEmitter; - -EventEmitter.usingDomains = false; - -EventEmitter.captureRejectionSymbol = kRejection; -ObjectDefineProperty(EventEmitter, 'captureRejections', { - __proto__: null, - get() { - return EventEmitter.prototype[kCapture]; - }, - set(value) { - validateBoolean(value, 'EventEmitter.captureRejections'); - - EventEmitter.prototype[kCapture] = value; - }, - enumerable: true, -}); - -ObjectDefineProperty(EventEmitter, 'EventEmitterAsyncResource', { - __proto__: null, - enumerable: true, - get: lazyEventEmitterAsyncResource, - set: undefined, - configurable: true, -}); - -EventEmitter.errorMonitor = kErrorMonitor; - -// The default for captureRejections is false -ObjectDefineProperty(EventEmitter.prototype, kCapture, { - __proto__: null, - value: false, - writable: true, - enumerable: false, -}); - -function isEventUnsupportedForFastPath(type) { - // Not supporting newListener and removeListener events in fast path - // as they can add new listeners in the middle of the process - // and also not supporting errorMonitor event as it has special handling - return type === 'newListener' || type === 'removeListener' || type === kErrorMonitor; -} - -ObjectDefineProperties(EventEmitter.prototype, { - [kImpl]: { - __proto__: null, - value: undefined, - enumerable: false, - configurable: false, - writable: true, - }, - [kIsFastPath]: { - __proto__: null, - value: undefined, - enumerable: false, - configurable: false, - writable: true, - }, - [kInitialFastPath]: { - __proto__: null, - value: undefined, - enumerable: false, - configurable: false, - writable: true, - }, - _events: { - __proto__: null, - enumerable: true, - - get: function() { - // TODO - remove optional chaining - return this[kImpl]?._events; - }, - set: function(arg) { - - if (this[kImpl] === undefined && this[kInitialFastPath] === true) { - // TODO(rluvaton): remove this eslint ignore - // eslint-disable-next-line no-use-before-define - this[kImpl] = new FastEventEmitter(this, arg); - this[kIsFastPath] = true; - this[kInitialFastPath] = undefined; - return; - } - - if (this[kImpl] === undefined) { - - // TODO(rluvaton): find a better way and also support array with length 1? - // TODO(rluvaton): if have newListener and removeListener events, switch to slow path - - const shouldBeFastPath = arg === undefined || - ObjectEntries(arg).some(({ 0: key, 1: value }) => - isEventUnsupportedForFastPath(key) || (typeof value !== 'undefined' && typeof value !== 'function')); - - // TODO(rluvaton): remove this eslint ignore - // eslint-disable-next-line no-use-before-define - this[kImpl] = shouldBeFastPath ? new FastEventEmitter(this, arg) : new SlowEventEmitter(this, arg); - this[kIsFastPath] = shouldBeFastPath; - return; - } - - // TODO - might need to change to slow path - // TODO - deprecate this - this[kImpl]._events = arg; - }, - }, - _eventsCount: { - __proto__: null, - - enumerable: true, - - get: function() { - return this[kImpl]?._eventsCount; - }, - set: function(arg) { - // TODO - remove optional chaining - if (!this[kImpl]) { - // TODO - don't use slow by default here - - // TODO(rluvaton): remove this eslint ignore - // eslint-disable-next-line no-use-before-define - this[kImpl] = new SlowEventEmitter(this); - this[kIsFastPath] = false; - // return; - } - // TODO - deprecate this - this[kImpl]._eventsCount = arg; - }, - }, -}); - -EventEmitter.prototype._maxListeners = undefined; -EventEmitter.prototype[kInitialFastPath] = undefined; - -// By default EventEmitters will print a warning if more than 10 listeners are -// added to it. This is a useful default which helps finding memory leaks. -let defaultMaxListeners = 10; -let isEventTarget; - -function checkListener(listener) { - validateFunction(listener, 'listener'); -} - -ObjectDefineProperty(EventEmitter, 'defaultMaxListeners', { - __proto__: null, - enumerable: true, - get: function() { - return defaultMaxListeners; - }, - set: function(arg) { - validateNumber(arg, 'defaultMaxListeners', 0); - defaultMaxListeners = arg; - }, -}); - -ObjectDefineProperties(EventEmitter, { - kMaxEventTargetListeners: { - __proto__: null, - value: kMaxEventTargetListeners, - enumerable: false, - configurable: false, - writable: false, - }, - kMaxEventTargetListenersWarned: { - __proto__: null, - value: kMaxEventTargetListenersWarned, - enumerable: false, - configurable: false, - writable: false, - }, -}); - -/** - * Sets the max listeners. - * @param {number} n - * @param {EventTarget[] | EventEmitter[]} [eventTargets] - * @returns {void} - */ -EventEmitter.setMaxListeners = - function(n = defaultMaxListeners, ...eventTargets) { - validateNumber(n, 'setMaxListeners', 0); - if (eventTargets.length === 0) { - defaultMaxListeners = n; - } else { - if (isEventTarget === undefined) - isEventTarget = require('internal/event_target').isEventTarget; - - for (let i = 0; i < eventTargets.length; i++) { - const target = eventTargets[i]; - if (isEventTarget(target)) { - target[kMaxEventTargetListeners] = n; - target[kMaxEventTargetListenersWarned] = false; - } else if (typeof target.setMaxListeners === 'function') { - target.setMaxListeners(n); - } else { - throw new ERR_INVALID_ARG_TYPE( - 'eventTargets', - ['EventEmitter', 'EventTarget'], - target); - } - } - } - }; - -// If you're updating this function definition, please also update any -// re-definitions, such as the one in the Domain module (lib/domain.js). -EventEmitter.init = function(opts) { - // TODO(rluvaton): remove this eslint ignore - // eslint-disable-next-line no-use-before-define - this[kImpl] ??= new FastEventEmitter(this, opts?.[kEvents]); - this[kIsFastPath] ??= true; - - const thisEvents = this._events; - - - // TODO - update this - if (thisEvents === undefined || - thisEvents === ObjectGetPrototypeOf(this)._events) { - // In fast path we don't want to have __proto__: null as it will cause the object to be in dictionary mode - // and will slow down the access to the properties by a lot (around 2x) - this[kImpl]._events = this[kIsFastPath] ? {} : { __proto__: null }; - this[kImpl]._eventsCount = 0; - this[kImpl][kShapeMode] = false; - } else { - this[kImpl][kShapeMode] = true; - } - - this._maxListeners = this._maxListeners || undefined; - - - if (opts?.captureRejections) { - validateBoolean(opts.captureRejections, 'options.captureRejections'); - this[kCapture] = Boolean(opts.captureRejections); - } else { - // Assigning the kCapture property directly saves an expensive - // prototype lookup in a very sensitive hot path. - this[kCapture] = EventEmitter.prototype[kCapture]; - } -}; - -EventEmitter.prototype[kSwitchToSlowPath] = function() { - if (this[kIsFastPath] === false) { - return; - } - - if (this[kIsFastPath] === true) { - this[kIsFastPath] = false; - - // TODO(rluvaton): remove this eslint ignore - // eslint-disable-next-line no-use-before-define - this[kImpl] = SlowEventEmitter.fromFastEventEmitter(this[kImpl]); - return; - } - - // TODO(rluvaton): remove this eslint ignore - // eslint-disable-next-line no-use-before-define - this[kImpl] = new SlowEventEmitter(this); - this[kIsFastPath] = false; - -}; - -/** - * Increases the max listeners of the event emitter. - * @param {number} n - * @returns {EventEmitter} - */ -EventEmitter.prototype.setMaxListeners = function setMaxListeners(n) { - validateNumber(n, 'setMaxListeners', 0); - this._maxListeners = n; - return this; -}; - -function _getMaxListeners(that) { - if (that._maxListeners === undefined) - return EventEmitter.defaultMaxListeners; - return that._maxListeners; -} - -/** - * Returns the current max listener value for the event emitter. - * @returns {number} - */ -EventEmitter.prototype.getMaxListeners = function getMaxListeners() { - return _getMaxListeners(this); -}; - - -/** - * Synchronously calls each of the listeners registered - * for the event. - * @param {string | symbol} type - * @param {...any} [args] - * @returns {boolean} - */ -EventEmitter.prototype.emit = function emit(type, ...args) { - return this[kImpl].emit(type, ...args); -}; - -function _addListener(target, type, listener, prepend) { - let m; - let events; - let existing; - - checkListener(listener); - - events = target._events; - if (events === undefined) { - events = target._events = { __proto__: null }; - target._eventsCount = 0; - } else { - // 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); - - // 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]; - } - - if (existing === undefined) { - // Optimize the case of one listener. Don't need the extra array object. - 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); - } - - // Check for listener leak - m = _getMaxListeners(target); - if (m > 0 && existing.length > m && !existing.warned) { - existing.warned = true; - // No error code for this since it is a Warning - const w = genericNodeError( - `Possible EventEmitter memory leak detected. ${existing.length} ${String(type)} listeners ` + - `added to ${inspect(target, { depth: -1 })}. MaxListeners is ${m}. Use emitter.setMaxListeners() to increase limit`, - { name: 'MaxListenersExceededWarning', emitter: target, type: type, count: existing.length }); - process.emitWarning(w); - } - } - - return target; -} - -/** - * Adds a listener to the event emitter. - * @param {string | symbol} type - * @param {Function} listener - * @returns {EventEmitter} - */ -EventEmitter.prototype.addListener = function addListener(type, listener) { - checkListener(listener); - - // This can happen in TLSSocket, where we listen for close event before - // the EventEmitter was initiated - // TODO(rluvaton): check how it worked before? - if (!this[kImpl]) { - EventEmitter.init.apply(this); - } - - // If the listener is already added, - // switch to slow path as the fast path optimized for single listener for each event - if (this[kIsFastPath] && (isEventUnsupportedForFastPath(type) || this[kImpl].isListenerAlreadyExists(type))) { - this[kSwitchToSlowPath](); - } - - return this[kImpl].addListener(type, listener); -}; - -EventEmitter.prototype.on = EventEmitter.prototype.addListener; - -/** - * Adds the `listener` function to the beginning of - * the listeners array. - * @param {string | symbol} type - * @param {Function} listener - * @returns {EventEmitter} - */ -EventEmitter.prototype.prependListener = - function prependListener(type, listener) { - checkListener(listener); - - // This can happen in TLSSocket, where we listen for close event before - // the EventEmitter was initiated - // TODO(rluvaton): check how it worked before? - if (!this[kImpl]) { - EventEmitter.init.apply(this); - } - - // If the listener is already added, - // switch to slow path as the fast path optimized for single listener for each event - if (this[kIsFastPath] && (isEventUnsupportedForFastPath(type) || this[kImpl].isListenerAlreadyExists(type))) { - this[kSwitchToSlowPath](); - } - - return this[kImpl].addListener(type, listener, true); - }; - -function onceWrapper() { - if (!this.fired) { - this.target.removeListener(this.type, this.wrapFn); - this.fired = true; - if (arguments.length === 0) - return this.listener.call(this.target); - return this.listener.apply(this.target, arguments); - } -} - -function _onceWrap(target, type, listener) { - const state = { fired: false, wrapFn: undefined, target, type, listener }; - const wrapped = onceWrapper.bind(state); - wrapped.listener = listener; - state.wrapFn = wrapped; - return wrapped; -} - -/** - * Adds a one-time `listener` function to the event emitter. - * @param {string | symbol} type - * @param {Function} listener - * @returns {EventEmitter} - */ -EventEmitter.prototype.once = function once(type, listener) { - checkListener(listener); - - this.on(type, _onceWrap(this, type, listener)); - return this; -}; - -/** - * Adds a one-time `listener` function to the beginning of - * the listeners array. - * @param {string | symbol} type - * @param {Function} listener - * @returns {EventEmitter} - */ -EventEmitter.prototype.prependOnceListener = - function prependOnceListener(type, listener) { - checkListener(listener); - - this.prependListener(type, _onceWrap(this, type, listener)); - return this; - }; - -/** - * Removes the specified `listener` from the listeners array. - * @param {string | symbol} type - * @param {Function} listener - * @returns {EventEmitter} - */ -EventEmitter.prototype.removeListener = - function removeListener(type, listener) { - checkListener(listener); - - this[kImpl].removeListener(type, listener); - - return this; - }; - -EventEmitter.prototype.off = EventEmitter.prototype.removeListener; - -/** - * Removes all listeners from the event emitter. (Only - * removes listeners for a specific event name if specified - * as `type`). - * @param {string | symbol} [type] - * @returns {EventEmitter} - */ -EventEmitter.prototype.removeAllListeners = - function removeAllListeners(type) { - this[kImpl].removeAllListeners(type); - return this; - }; - -function _listeners(target, type, unwrap) { - const events = target._events; - - if (events === undefined) - return []; - - const evlistener = events[type]; - if (evlistener === undefined) - return []; - - if (typeof evlistener === 'function') - return unwrap ? [evlistener.listener || evlistener] : [evlistener]; - - return unwrap ? - unwrapListeners(evlistener) : arrayClone(evlistener); -} - -/** - * Returns a copy of the array of listeners for the event name - * specified as `type`. - * @param {string | symbol} type - * @returns {Function[]} - */ -EventEmitter.prototype.listeners = function listeners(type) { - return _listeners(this, type, true); -}; - -/** - * Returns a copy of the array of listeners and wrappers for - * the event name specified as `type`. - * @param {string | symbol} type - * @returns {Function[]} - */ -EventEmitter.prototype.rawListeners = function rawListeners(type) { - return _listeners(this, type, false); -}; - -/** - * Returns the number of listeners listening to the event name - * specified as `type`. - * @deprecated since v3.2.0 - * @param {EventEmitter} emitter - * @param {string | symbol} type - * @returns {number} - */ -EventEmitter.listenerCount = function(emitter, type) { - if (typeof emitter.listenerCount === 'function') { - return emitter.listenerCount(type); - } - return FunctionPrototypeCall(listenerCount, emitter, type); -}; - -EventEmitter.prototype.listenerCount = listenerCount; - -/** - * Returns the number of listeners listening to event name - * specified as `type`. - * @param {string | symbol} type - * @param {Function} listener - * @returns {number} - */ -function listenerCount(type, listener) { - const events = this._events; - - if (events !== undefined) { - const evlistener = events[type]; - - if (typeof evlistener === 'function') { - if (listener != null) { - return listener === evlistener || listener === evlistener.listener ? 1 : 0; - } - - return 1; - } else if (evlistener !== undefined) { - if (listener != null) { - let matching = 0; - - for (let i = 0, l = evlistener.length; i < l; i++) { - if (evlistener[i] === listener || evlistener[i].listener === listener) { - matching++; - } - } - - return matching; - } - - return evlistener.length; - } - } - - return 0; -} - -/** - * Returns an array listing the events for which - * the emitter has registered listeners. - * @returns {any[]} - */ -EventEmitter.prototype.eventNames = function eventNames() { - return this._eventsCount > 0 ? ReflectOwnKeys(this._events) : []; -}; - -function unwrapListeners(arr) { - const ret = arrayClone(arr); - for (let i = 0; i < ret.length; ++i) { - const orig = ret[i].listener; - if (typeof orig === 'function') - ret[i] = orig; - } - return ret; -} /** * Returns a copy of the array of listeners for the event name From 190cb0da9be780154c883f8207b21e7cf2769fa8 Mon Sep 17 00:00:00 2001 From: Raz Luvaton <16746759+rluvaton@users.noreply.github.com> Date: Mon, 13 May 2024 15:29:11 +0300 Subject: [PATCH 68/69] events: set initial events count to 0 --- lib/internal/events/fast_event_emitter.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/internal/events/fast_event_emitter.js b/lib/internal/events/fast_event_emitter.js index 0190f7bb148e99..909d3a79e55468 100644 --- a/lib/internal/events/fast_event_emitter.js +++ b/lib/internal/events/fast_event_emitter.js @@ -21,7 +21,7 @@ function FastEventEmitter(eventEmitterTranslationLayer, _events) { */ FastEventEmitter.prototype._events = undefined; FastEventEmitter.prototype[kShapeMode] = undefined; -FastEventEmitter.prototype._eventsCount = undefined; +FastEventEmitter.prototype._eventsCount = 0; FastEventEmitter.prototype.eventEmitterTranslationLayer = undefined; /** From 1d27fc5942b40d804dbc1d4aa65e3073aaaca62d Mon Sep 17 00:00:00 2001 From: Raz Luvaton <16746759+rluvaton@users.noreply.github.com> Date: Mon, 13 May 2024 15:31:44 +0300 Subject: [PATCH 69/69] events: try to improve perf by checking if impl already exists this will break but just for the sake of it --- lib/internal/events/event_emitter.js | 13 +------------ lib/internal/events/symbols.js | 2 -- lib/internal/streams/readable.js | 11 ++++------- 3 files changed, 5 insertions(+), 21 deletions(-) diff --git a/lib/internal/events/event_emitter.js b/lib/internal/events/event_emitter.js index 9709c0c5176e1d..8bf856cb0c0fa6 100644 --- a/lib/internal/events/event_emitter.js +++ b/lib/internal/events/event_emitter.js @@ -32,7 +32,6 @@ const { kMaxEventTargetListeners, kMaxEventTargetListenersWarned, kImpl, - kPreInitiated, kIsFastPath, kSwitchToSlowPath, kInitialEvents, @@ -223,13 +222,6 @@ ObjectDefineProperties(EventEmitter.prototype, { configurable: false, writable: true, }, - [kPreInitiated]: { - __proto__: null, - value: false, - enumerable: false, - configurable: false, - writable: true, - }, [kIsFastPath]: { __proto__: null, value: undefined, @@ -360,7 +352,6 @@ EventEmitter.init = function(opts) { this._maxListeners = this._maxListeners || undefined; - if (opts?.captureRejections) { validateBoolean(opts.captureRejections, 'options.captureRejections'); this[kCapture] = Boolean(opts.captureRejections); @@ -369,9 +360,7 @@ EventEmitter.init = function(opts) { // prototype lookup in a very sensitive hot path. this[kCapture] = EventEmitter.prototype[kCapture]; } - - if(this[kPreInitiated] === true) { - this[kPreInitiated] = false; + if(this[kImpl] !== undefined) { return; } diff --git a/lib/internal/events/symbols.js b/lib/internal/events/symbols.js index 53980855996d2a..9f15f874cb5223 100644 --- a/lib/internal/events/symbols.js +++ b/lib/internal/events/symbols.js @@ -17,7 +17,6 @@ const kMaxEventTargetListenersWarned = const kWatermarkData = SymbolFor('nodejs.watermarkData'); const kImpl = Symbol('kImpl'); -const kPreInitiated = Symbol('kPreInitiated'); const kIsFastPath = Symbol('kIsFastPath'); const kSwitchToSlowPath = Symbol('kSwitchToSlowPath'); const kRejection = SymbolFor('nodejs.rejection'); @@ -32,7 +31,6 @@ module.exports = { kMaxEventTargetListenersWarned, kWatermarkData, kImpl, - kPreInitiated, kIsFastPath, kSwitchToSlowPath, kRejection, diff --git a/lib/internal/streams/readable.js b/lib/internal/streams/readable.js index bb90cc18145ae7..c8f46deb979516 100644 --- a/lib/internal/streams/readable.js +++ b/lib/internal/streams/readable.js @@ -43,7 +43,7 @@ Readable.ReadableState = ReadableState; const EE = require('events'); const { Stream, prependListener } = require('internal/streams/legacy'); -const { kInitialEvents, kImpl, kShapeMode, kIsFastPath, kPreInitiated} = require('internal/events/symbols'); +const { kInitialEvents, kImpl, kShapeMode, kIsFastPath } = require('internal/events/symbols'); const { Buffer } = require('buffer'); const { @@ -321,9 +321,9 @@ function Readable(options) { if (!(this instanceof Readable)) return new Readable(options); - if (this[kImpl] === undefined && this._events === undefined) { + if (this[kImpl] === undefined) { this[kIsFastPath] = true; - this[kImpl] = new FastEventEmitter(this, this[kInitialEvents] !== undefined ? this[kInitialEvents] :{ + this[kImpl] = new FastEventEmitter(this, this[kInitialEvents] === undefined ? { close: undefined, error: undefined, data: undefined, @@ -336,10 +336,8 @@ function Readable(options) { // unpipe: undefined, // [destroyImpl.kConstruct]: undefined, // [destroyImpl.kDestroy]: undefined, - }); + } : this[kInitialEvents]); this[kImpl][kShapeMode] = true; - } else { - this[kPreInitiated] = false; } this._readableState = new ReadableState(options, this, false); @@ -367,7 +365,6 @@ function Readable(options) { } } -Readable.prototype[kPreInitiated] = true; Readable.prototype.destroy = destroyImpl.destroy; Readable.prototype._undestroy = destroyImpl.undestroy; Readable.prototype._destroy = function(err, cb) {