Skip to content

Commit 6242697

Browse files
addaleaxjuanarbol
authored andcommitted
repl: keep reference count for process.on('newListener')
When investigating a memory leak in one of our applications, we discovered that this listener holds on to a `REPLServer` instance and all heap objects transitively kept alive by it by capturing as part of its closure. It's cleaner to declare the listener outside of the `REPLServer` class and to actually clean it up properly when it is no longer required or meaningful, which is easily achieved through keeping a reference count. PR-URL: #61895 Backport-PR-URL: #63194 Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
1 parent 3b97a61 commit 6242697

2 files changed

Lines changed: 49 additions & 19 deletions

File tree

lib/repl.js

Lines changed: 32 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,35 @@ const kBufferedCommandSymbol = Symbol('bufferedCommand');
213213
const kContextId = Symbol('contextId');
214214
const kLoadingSymbol = Symbol('loading');
215215

216-
let addedNewListener = false;
216+
function processNewListener(event, listener) {
217+
if (event === 'uncaughtException' &&
218+
process.domain &&
219+
listener.name !== 'domainUncaughtExceptionClear' &&
220+
domainSet.has(process.domain)) {
221+
// Throw an error so that the event will not be added and the current
222+
// domain takes over. That way the user is notified about the error
223+
// and the current code evaluation is stopped, just as any other code
224+
// that contains an error.
225+
throw new ERR_INVALID_REPL_INPUT(
226+
'Listeners for `uncaughtException` cannot be used in the REPL');
227+
}
228+
}
229+
230+
let processNewListenerUseCount = 0;
231+
function addProcessNewListener() {
232+
if (processNewListenerUseCount++ === 0) {
233+
// Add this listener only once and use a WeakSet that contains the REPLs
234+
// domains. Otherwise we'd have to add a single listener to each REPL
235+
// instance and that could trigger the `MaxListenersExceededWarning`.
236+
process.prependListener('newListener', processNewListener);
237+
}
238+
}
239+
240+
function removeProcessNewListener() {
241+
if (--processNewListenerUseCount === 0) {
242+
process.removeListener('newListener', processNewListener);
243+
}
244+
}
217245

218246
try {
219247
// Hack for require.resolve("./relative") to work properly.
@@ -370,24 +398,9 @@ function REPLServer(prompt,
370398
// It is possible to introspect the running REPL accessing this variable
371399
// from inside the REPL. This is useful for anyone working on the REPL.
372400
module.exports.repl = this;
373-
} else if (!addedNewListener) {
374-
// Add this listener only once and use a WeakSet that contains the REPLs
375-
// domains. Otherwise we'd have to add a single listener to each REPL
376-
// instance and that could trigger the `MaxListenersExceededWarning`.
377-
process.prependListener('newListener', (event, listener) => {
378-
if (event === 'uncaughtException' &&
379-
process.domain &&
380-
listener.name !== 'domainUncaughtExceptionClear' &&
381-
domainSet.has(process.domain)) {
382-
// Throw an error so that the event will not be added and the current
383-
// domain takes over. That way the user is notified about the error
384-
// and the current code evaluation is stopped, just as any other code
385-
// that contains an error.
386-
throw new ERR_INVALID_REPL_INPUT(
387-
'Listeners for `uncaughtException` cannot be used in the REPL');
388-
}
389-
});
390-
addedNewListener = true;
401+
} else {
402+
addProcessNewListener();
403+
this.once('exit', removeProcessNewListener);
391404
}
392405

393406
domainSet.add(this._domain);
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
'use strict';
2+
const common = require('../common');
3+
const { startNewREPLServer } = require('../common/repl');
4+
const assert = require('assert');
5+
6+
const originalProcessNewListenerCount = process.listenerCount('newListener');
7+
const { replServer } = startNewREPLServer();
8+
9+
const listenerCountBeforeClose = process.listenerCount('newListener');
10+
replServer.close();
11+
replServer.once('exit', common.mustCall(() => {
12+
setImmediate(common.mustCall(() => {
13+
const listenerCountAfterClose = process.listenerCount('newListener');
14+
assert.strictEqual(listenerCountAfterClose, listenerCountBeforeClose - 1);
15+
assert.strictEqual(listenerCountAfterClose, originalProcessNewListenerCount);
16+
}));
17+
}));

0 commit comments

Comments
 (0)