Skip to content

Commit 384d7a4

Browse files
authored
events: optimize once() and removeListener()
Use a closure instead of a bound state object for the once() wrapper, keep the events object shape in removeListener() by storing undefined instead of deleting keys or reallocating the object, and outline the cold unhandled 'error' path from emit() and the max-listeners warning from _addListener() so both fit within V8 inlining budgets. ee-once +25%, ee-add-remove +12-22%, ee-listen-unique +7-15%, streams/pipe +13.9%. Signed-off-by: Matteo Collina <hello@matteocollina.com> PR-URL: #64373 Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
1 parent e631709 commit 384d7a4

5 files changed

Lines changed: 82 additions & 78 deletions

lib/events.js

Lines changed: 74 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,6 @@ const { addAbortListener } = require('internal/events/abort_listener');
8686

8787
const kCapture = Symbol('kCapture');
8888
const kErrorMonitor = Symbol('events.errorMonitor');
89-
const kShapeMode = Symbol('shapeMode');
9089
const kEmitting = Symbol('events.emitting');
9190
const kMaxEventTargetListeners = Symbol('events.maxEventTargetListeners');
9291
const kMaxEventTargetListenersWarned =
@@ -335,9 +334,6 @@ EventEmitter.init = function(opts) {
335334
this._events === ObjectGetPrototypeOf(this)._events) {
336335
this._events = { __proto__: null };
337336
this._eventsCount = 0;
338-
this[kShapeMode] = false;
339-
} else {
340-
this[kShapeMode] = true;
341337
}
342338

343339
this._maxListeners ||= undefined;
@@ -446,6 +442,39 @@ function enhanceStackTrace(err, own) {
446442
return err.stack + sep + ArrayPrototypeJoin(ownStack, '\n');
447443
}
448444

445+
function getUnhandledErrorException(ee, args) {
446+
let er;
447+
if (args.length > 0)
448+
er = args[0];
449+
if (er instanceof Error) {
450+
try {
451+
const capture = {};
452+
ErrorCaptureStackTrace(capture, EventEmitter.prototype.emit);
453+
ObjectDefineProperty(er, kEnhanceStackBeforeInspector, {
454+
__proto__: null,
455+
value: FunctionPrototypeBind(enhanceStackTrace, ee, er, capture),
456+
configurable: true,
457+
});
458+
} catch {
459+
// Continue regardless of error.
460+
}
461+
462+
return er;
463+
}
464+
465+
let stringifiedEr;
466+
try {
467+
stringifiedEr = inspect(er);
468+
} catch {
469+
stringifiedEr = er;
470+
}
471+
472+
// At least give some kind of context to the user
473+
const err = new ERR_UNHANDLED_ERROR(stringifiedEr);
474+
err.context = er;
475+
return err;
476+
}
477+
449478
/**
450479
* Synchronously calls each of the listeners registered
451480
* for the event.
@@ -466,38 +495,10 @@ EventEmitter.prototype.emit = function emit(type, ...args) {
466495

467496
// If there is no 'error' event listener then throw.
468497
if (doError) {
469-
let er;
470-
if (args.length > 0)
471-
er = args[0];
472-
if (er instanceof Error) {
473-
try {
474-
const capture = {};
475-
ErrorCaptureStackTrace(capture, EventEmitter.prototype.emit);
476-
ObjectDefineProperty(er, kEnhanceStackBeforeInspector, {
477-
__proto__: null,
478-
value: FunctionPrototypeBind(enhanceStackTrace, this, er, capture),
479-
configurable: true,
480-
});
481-
} catch {
482-
// Continue regardless of error.
483-
}
484-
485-
// Note: The comments on the `throw` lines are intentional, they show
486-
// up in Node's output if this results in an unhandled exception.
487-
throw er; // Unhandled 'error' event
488-
}
489-
490-
let stringifiedEr;
491-
try {
492-
stringifiedEr = inspect(er);
493-
} catch {
494-
stringifiedEr = er;
495-
}
496-
497-
// At least give some kind of context to the user
498-
const err = new ERR_UNHANDLED_ERROR(stringifiedEr);
499-
err.context = er;
500-
throw err; // Unhandled 'error' event
498+
const er = getUnhandledErrorException(this, args);
499+
// Note: The comments on the `throw` lines are intentional, they show
500+
// up in Node's output if this results in an unhandled exception.
501+
throw er; // Unhandled 'error' event
501502
}
502503

503504
const handler = events[type];
@@ -584,20 +585,23 @@ function _addListener(target, type, listener, prepend) {
584585

585586
// Check for listener leak
586587
m = _getMaxListeners(target);
587-
if (m > 0 && existing.length > m && !existing.warned) {
588-
existing.warned = true;
589-
// No error code for this since it is a Warning
590-
const w = genericNodeError(
591-
`Possible EventEmitter memory leak detected. ${existing.length} ${String(type)} listeners ` +
592-
`added to ${inspect(target, { depth: -1 })}. MaxListeners is ${m}. Use emitter.setMaxListeners() to increase limit`,
593-
{ name: 'MaxListenersExceededWarning', emitter: target, type: type, count: existing.length });
594-
process.emitWarning(w);
595-
}
588+
if (m > 0 && existing.length > m && !existing.warned)
589+
warnMaxListenersExceeded(target, type, existing, m);
596590
}
597591

598592
return target;
599593
}
600594

595+
function warnMaxListenersExceeded(target, type, existing, m) {
596+
existing.warned = true;
597+
// No error code for this since it is a Warning
598+
const w = genericNodeError(
599+
`Possible EventEmitter memory leak detected. ${existing.length} ${String(type)} listeners ` +
600+
`added to ${inspect(target, { depth: -1 })}. MaxListeners is ${m}. Use emitter.setMaxListeners() to increase limit`,
601+
{ name: 'MaxListenersExceededWarning', emitter: target, type: type, count: existing.length });
602+
process.emitWarning(w);
603+
}
604+
601605
/**
602606
* Adds a listener to the event emitter.
603607
* @param {string | symbol} type
@@ -622,22 +626,16 @@ EventEmitter.prototype.prependListener =
622626
return _addListener(this, type, listener, true);
623627
};
624628

625-
function onceWrapper() {
626-
if (!this.fired) {
627-
this.target.removeListener(this.type, this.wrapFn);
628-
this.fired = true;
629-
if (arguments.length === 0)
630-
return this.listener.call(this.target);
631-
return ReflectApply(this.listener, this.target, arguments);
632-
}
633-
}
634-
635629
function _onceWrap(target, type, listener) {
636-
const state = { fired: false, wrapFn: undefined, target, type, listener };
637-
const wrapped = onceWrapper.bind(state);
638-
wrapped.listener = listener;
639-
state.wrapFn = wrapped;
640-
return wrapped;
630+
let fired = false;
631+
function wrapper(...args) {
632+
if (fired) return;
633+
fired = true;
634+
target.removeListener(type, wrapper);
635+
return ReflectApply(listener, target, args);
636+
}
637+
wrapper.listener = listener;
638+
return wrapper;
641639
}
642640

643641
/**
@@ -689,13 +687,12 @@ EventEmitter.prototype.removeListener =
689687
if (list === listener || list.listener === listener) {
690688
this._eventsCount -= 1;
691689

692-
if (this[kShapeMode]) {
693-
events[type] = undefined;
694-
} else if (this._eventsCount === 0) {
695-
this._events = { __proto__: null };
696-
} else {
697-
delete events[type];
698-
}
690+
// Leave the key in place with an `undefined` value: repeatedly
691+
// adding and removing a listener for the same event this way keeps
692+
// the `events` object in the same shape and avoids both a `delete`
693+
// (which would put the object into dictionary mode) and allocating
694+
// a fresh object when the last listener is removed.
695+
events[type] = undefined;
699696

700697
if (events.removeListener !== undefined)
701698
this.emit('removeListener', type, list.listener || listener);
@@ -755,7 +752,6 @@ EventEmitter.prototype.removeAllListeners =
755752
else
756753
delete events[type];
757754
}
758-
this[kShapeMode] = false;
759755
return this;
760756
}
761757

@@ -768,7 +764,6 @@ EventEmitter.prototype.removeAllListeners =
768764
this.removeAllListeners('removeListener');
769765
this._events = { __proto__: null };
770766
this._eventsCount = 0;
771-
this[kShapeMode] = false;
772767
return this;
773768
}
774769

@@ -868,7 +863,16 @@ EventEmitter.prototype.listenerCount = function listenerCount(type, listener) {
868863
* @returns {(string | symbol)[]}
869864
*/
870865
EventEmitter.prototype.eventNames = function eventNames() {
871-
return this._eventsCount > 0 ? ReflectOwnKeys(this._events) : [];
866+
if (this._eventsCount === 0)
867+
return [];
868+
const events = this._events;
869+
const names = [];
870+
for (const key of ReflectOwnKeys(events)) {
871+
// Removed listeners leave the key in place with an `undefined` value.
872+
if (events[key] !== undefined)
873+
ArrayPrototypePush(names, key);
874+
}
875+
return names;
872876
};
873877

874878
function arrayClone(arr) {

test/fixtures/errors/events_unhandled_error_common_trace.snapshot

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
node:events:<line>
2-
throw er; // Unhandled 'error' event
3-
^
2+
throw er; // Unhandled 'error' event
3+
^
44

55
Error: foo:bar
66
at bar (<project-root>/test/fixtures/errors/events_unhandled_error_common_trace.js:9:12)

test/fixtures/errors/events_unhandled_error_nexttick.snapshot

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
node:events:<line>
2-
throw er; // Unhandled 'error' event
3-
^
2+
throw er; // Unhandled 'error' event
3+
^
44

55
Error
66
at Object.<anonymous> (<project-root>/test/fixtures/errors/events_unhandled_error_nexttick.js:6:12)

test/fixtures/errors/events_unhandled_error_sameline.snapshot

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
node:events:<line>
2-
throw er; // Unhandled 'error' event
3-
^
2+
throw er; // Unhandled 'error' event
3+
^
44

55
Error
66
at Object.<anonymous> (<project-root>/test/fixtures/errors/events_unhandled_error_sameline.js:6:34)

test/fixtures/errors/events_unhandled_error_subclass.snapshot

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
node:events:<line>
2-
throw er; // Unhandled 'error' event
3-
^
2+
throw er; // Unhandled 'error' event
3+
^
44

55
Error
66
at Object.<anonymous> (<project-root>/test/fixtures/errors/events_unhandled_error_subclass.js:7:25)

0 commit comments

Comments
 (0)