Skip to content

Commit 2ca42c8

Browse files
authored
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 31a863f commit 2ca42c8

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
@@ -186,7 +186,35 @@ const domainSet = new SafeWeakSet();
186186
constkBufferedCommandSymbol=Symbol('bufferedCommand');
187187
constkLoadingSymbol=Symbol('loading');
188188

189-
letaddedNewListener=false;
189+
functionprocessNewListener(event,listener){
190+
if(event==='uncaughtException'&&
191+
process.domain&&
192+
listener.name!=='domainUncaughtExceptionClear'&&
193+
domainSet.has(process.domain)){
194+
// Throw an error so that the event will not be added and the current
195+
// domain takes over. That way the user is notified about the error
196+
// and the current code evaluation is stopped, just as any other code
197+
// that contains an error.
198+
thrownewERR_INVALID_REPL_INPUT(
199+
'Listeners for `uncaughtException` cannot be used in the REPL');
200+
}
201+
}
202+
203+
letprocessNewListenerUseCount=0;
204+
functionaddProcessNewListener(){
205+
if(processNewListenerUseCount++===0){
206+
// Add this listener only once and use a WeakSet that contains the REPLs
207+
// domains. Otherwise we'd have to add a single listener to each REPL
208+
// instance and that could trigger the `MaxListenersExceededWarning`.
209+
process.prependListener('newListener',processNewListener);
210+
}
211+
}
212+
213+
functionremoveProcessNewListener(){
214+
if(--processNewListenerUseCount===0){
215+
process.removeListener('newListener',processNewListener);
216+
}
217+
}
190218

191219
fixReplRequire(module);
192220

@@ -327,24 +355,9 @@ function REPLServer(prompt,
327355
// It is possible to introspect the running REPL accessing this variable
328356
// from inside the REPL. This is useful for anyone working on the REPL.
329357
module.exports.repl=this;
330-
}elseif(!addedNewListener){
331-
// Add this listener only once and use a WeakSet that contains the REPLs
332-
// domains. Otherwise we'd have to add a single listener to each REPL
333-
// instance and that could trigger the `MaxListenersExceededWarning`.
334-
process.prependListener('newListener',(event,listener)=>{
335-
if(event==='uncaughtException'&&
336-
process.domain&&
337-
listener.name!=='domainUncaughtExceptionClear'&&
338-
domainSet.has(process.domain)){
339-
// Throw an error so that the event will not be added and the current
340-
// domain takes over. That way the user is notified about the error
341-
// and the current code evaluation is stopped, just as any other code
342-
// that contains an error.
343-
thrownewERR_INVALID_REPL_INPUT(
344-
'Listeners for `uncaughtException` cannot be used in the REPL');
345-
}
346-
});
347-
addedNewListener=true;
358+
}else{
359+
addProcessNewListener();
360+
this.once('exit',removeProcessNewListener);
348361
}
349362

350363
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+
constcommon=require('../common');
3+
const{ startNewREPLServer }=require('../common/repl');
4+
constassert=require('assert');
5+
6+
constoriginalProcessNewListenerCount=process.listenerCount('newListener');
7+
const{ replServer }=startNewREPLServer();
8+
9+
constlistenerCountBeforeClose=process.listenerCount('newListener');
10+
replServer.close();
11+
replServer.once('exit',common.mustCall(()=>{
12+
setImmediate(common.mustCall(()=>{
13+
constlistenerCountAfterClose=process.listenerCount('newListener');
14+
assert.strictEqual(listenerCountAfterClose,listenerCountBeforeClose-1);
15+
assert.strictEqual(listenerCountAfterClose,originalProcessNewListenerCount);
16+
}));
17+
}));

0 commit comments

Comments
 (0)