events: add max listener warning for EventTarget

Signed-off-by: James M Snell <jasnell@gmail.com>

PR-URL: https://github.com/nodejs/node/pull/36001
Fixes: https://github.com/nodejs/node/issues/35990
Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com>
Reviewed-By: Rich Trott <rtrott@gmail.com>
This commit is contained in:
James M Snell 2020-11-06 08:09:42 -08:00 committed by Node.js GitHub Bot
parent cd3134029e
commit dc79f3f37c
4 changed files with 178 additions and 30 deletions

View File

@ -383,6 +383,29 @@ Installing a listener using this symbol does not change the behavior once an
`'error'` event is emitted, therefore the process will still crash if no
regular `'error'` listener is installed.
### `EventEmitter.setMaxListeners(n[, ...eventTargets])`
<!-- YAML
added: REPLACEME
-->
* `n` {number} A non-negative number. The maximum number of listeners per
`EventTarget` event.
* `...eventsTargets` {EventTarget[]|EventEmitter[]} Zero or more {EventTarget}
or {EventEmitter} instances. If none are specified, `n` is set as the default
max for all newly created {EventTarget} and {EventEmitter} objects.
```js
const {
setMaxListeners,
EventEmitter
} = require('events');
const target = new EventTarget();
const emitter = new EventEmitter();
setMaxListeners(5, target, emitter);
```
### `emitter.addListener(eventName, listener)`
<!-- YAML
added: v0.1.26

View File

@ -30,6 +30,7 @@ const {
NumberIsNaN,
ObjectCreate,
ObjectDefineProperty,
ObjectDefineProperties,
ObjectGetPrototypeOf,
ObjectSetPrototypeOf,
Promise,
@ -67,6 +68,9 @@ const {
const kCapture = Symbol('kCapture');
const kErrorMonitor = Symbol('events.errorMonitor');
const kMaxEventTargetListeners = Symbol('events.maxEventTargetListeners');
const kMaxEventTargetListenersWarned =
Symbol('events.maxEventTargetListenersWarned');
let DOMException;
const lazyDOMException = hideStackFrames((message, name) => {
@ -120,6 +124,7 @@ EventEmitter.prototype._maxListeners = 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) {
if (typeof listener !== 'function') {
@ -142,6 +147,48 @@ ObjectDefineProperty(EventEmitter, 'defaultMaxListeners', {
}
});
ObjectDefineProperties(EventEmitter, {
kMaxEventTargetListeners: {
value: kMaxEventTargetListeners,
enumerable: false,
configurable: false,
writable: false,
},
kMaxEventTargetListenersWarned: {
value: kMaxEventTargetListenersWarned,
enumerable: false,
configurable: false,
writable: false,
}
});
EventEmitter.setMaxListeners =
function(n = defaultMaxListeners, ...eventTargets) {
if (typeof n !== 'number' || n < 0 || NumberIsNaN(n))
throw new ERR_OUT_OF_RANGE('n', 'a non-negative number', n);
if (eventTargets.length === 0) {
defaultMaxListeners = n;
} else {
if (isEventTarget === undefined)
isEventTarget = require('internal/event_target').isEventTarget;
// Performance for forEach is now comparable with regular for-loop
eventTargets.forEach((target) => {
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);
}
});
}
};
EventEmitter.init = function(opts) {
if (this._events === undefined ||

View File

@ -26,13 +26,19 @@ const {
ERR_INVALID_THIS,
}
} = require('internal/errors');
const { validateInteger, validateObject } = require('internal/validators');
const { validateObject } = require('internal/validators');
const { customInspectSymbol } = require('internal/util');
const { inspect } = require('util');
const kIsEventTarget = SymbolFor('nodejs.event_target');
const EventEmitter = require('events');
const {
kMaxEventTargetListeners,
kMaxEventTargetListenersWarned,
} = EventEmitter;
const kEvents = Symbol('kEvents');
const kStop = Symbol('kStop');
const kTarget = Symbol('kTarget');
@ -43,8 +49,6 @@ const kCreateEvent = Symbol('kCreateEvent');
const kNewListener = Symbol('kNewListener');
const kRemoveListener = Symbol('kRemoveListener');
const kIsNodeStyleListener = Symbol('kIsNodeStyleListener');
const kMaxListeners = Symbol('kMaxListeners');
const kMaxListenersWarned = Symbol('kMaxListenersWarned');
const kTrustEvent = Symbol('kTrustEvent');
// Lazy load perf_hooks to avoid the additional overhead on startup
@ -221,6 +225,8 @@ class Listener {
function initEventTarget(self) {
self[kEvents] = new SafeMap();
self[kMaxEventTargetListeners] = EventEmitter.defaultMaxListeners;
self[kMaxEventTargetListenersWarned] = false;
}
class EventTarget {
@ -233,7 +239,24 @@ class EventTarget {
initEventTarget(this);
}
[kNewListener](size, type, listener, once, capture, passive) {}
[kNewListener](size, type, listener, once, capture, passive) {
if (this[kMaxEventTargetListeners] > 0 &&
size > this[kMaxEventTargetListeners] &&
!this[kMaxEventTargetListenersWarned]) {
this[kMaxEventTargetListenersWarned] = true;
// No error code for this since it is a Warning
// eslint-disable-next-line no-restricted-syntax
const w = new Error('Possible EventTarget memory leak detected. ' +
`${size} ${type} listeners ` +
`added to ${inspect(this, { depth: -1 })}. Use ` +
'events.setMaxListeners() to increase limit');
w.name = 'MaxListenersExceededWarning';
w.target = this;
w.type = type;
w.count = size;
process.emitWarning(w);
}
}
[kRemoveListener](size, type, listener, capture) {}
addEventListener(type, listener, options = {}) {
@ -417,9 +440,6 @@ ObjectDefineProperty(EventTarget.prototype, SymbolToStringTag, {
function initNodeEventTarget(self) {
initEventTarget(self);
// eslint-disable-next-line no-use-before-define
self[kMaxListeners] = NodeEventTarget.defaultMaxListeners;
self[kMaxListenersWarned] = false;
}
class NodeEventTarget extends EventTarget {
@ -430,33 +450,12 @@ class NodeEventTarget extends EventTarget {
initNodeEventTarget(this);
}
[kNewListener](size, type, listener, once, capture, passive) {
if (this[kMaxListeners] > 0 &&
size > this[kMaxListeners] &&
!this[kMaxListenersWarned]) {
this[kMaxListenersWarned] = true;
// No error code for this since it is a Warning
// eslint-disable-next-line no-restricted-syntax
const w = new Error('Possible EventTarget memory leak detected. ' +
`${size} ${type} listeners ` +
`added to ${inspect(this, { depth: -1 })}. Use ` +
'setMaxListeners() to increase limit');
w.name = 'MaxListenersExceededWarning';
w.target = this;
w.type = type;
w.count = size;
process.emitWarning(w);
}
}
setMaxListeners(n) {
validateInteger(n, 'n', 0);
this[kMaxListeners] = n;
return this;
EventEmitter.setMaxListeners(n, this);
}
getMaxListeners() {
return this[kMaxListeners];
return this[kMaxEventTargetListeners];
}
eventNames() {

View File

@ -0,0 +1,79 @@
// Flags: --no-warnings
'use strict';
const common = require('../common');
const {
setMaxListeners,
EventEmitter
} = require('events');
const assert = require('assert');
common.expectWarning({
MaxListenersExceededWarning: [
['Possible EventTarget memory leak detected. 3 foo listeners added to ' +
'EventTarget. Use events.setMaxListeners() ' +
'to increase limit'],
['Possible EventTarget memory leak detected. 3 foo listeners added to ' +
'[MessagePort [EventTarget]]. ' +
'Use events.setMaxListeners() to increase ' +
'limit'],
['Possible EventTarget memory leak detected. 3 foo listeners added to ' +
'[MessagePort [EventTarget]]. ' +
'Use events.setMaxListeners() to increase ' +
'limit'],
['Possible EventTarget memory leak detected. 3 foo listeners added to ' +
'[AbortSignal [EventTarget]]. ' +
'Use events.setMaxListeners() to increase ' +
'limit'],
],
ExperimentalWarning: [[
'AbortController is an experimental feature. This feature could change ' +
'at any time'
]]
});
{
const et = new EventTarget();
setMaxListeners(2, et);
et.addEventListener('foo', () => {});
et.addEventListener('foo', () => {});
et.addEventListener('foo', () => {});
}
{
// No warning emitted because prior limit was only for that
// one EventTarget.
const et = new EventTarget();
et.addEventListener('foo', () => {});
et.addEventListener('foo', () => {});
et.addEventListener('foo', () => {});
}
{
const mc = new MessageChannel();
setMaxListeners(2, mc.port1);
mc.port1.addEventListener('foo', () => {});
mc.port1.addEventListener('foo', () => {});
mc.port1.addEventListener('foo', () => {});
}
{
// Set the default for newly created EventTargets
setMaxListeners(2);
const mc = new MessageChannel();
mc.port1.addEventListener('foo', () => {});
mc.port1.addEventListener('foo', () => {});
mc.port1.addEventListener('foo', () => {});
const ac = new AbortController();
ac.signal.addEventListener('foo', () => {});
ac.signal.addEventListener('foo', () => {});
ac.signal.addEventListener('foo', () => {});
}
{
// It works for EventEmitters also
const ee = new EventEmitter();
setMaxListeners(2, ee);
assert.strictEqual(ee.getMaxListeners(), 2);
}