Skip to content

Commit c25b8e3

Browse files
mcollinaaduh95
authored andcommitted
readline: reduce createInterface overhead
Speed up Interface construction: - Hoist the history accessor property descriptors to module scope and define them with a single ObjectDefineProperties call, instead of allocating six closures and four descriptor objects per instance. - Stop assigning the history options onto the input stream. This avoids hidden class transitions on the user provided stream and no longer mutates it observably. - Only check process.env.TERM for a dumb terminal when the interface is in terminal mode. Reading process.env goes through the environment interceptor and is comparatively expensive, and _ttyWrite is never called when terminal is false. Signed-off-by: Matteo Collina <hello@matteocollina.com> PR-URL: #64585 Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Rafael Gonzaga <rafael.nunu@hotmail.com> Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com> Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com> Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
1 parent e018f9a commit c25b8e3

3 files changed

Lines changed: 97 additions & 53 deletions

File tree

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
'use strict';
2+
constcommon=require('../common.js');
3+
constreadline=require('readline');
4+
const{ Readable, Writable }=require('stream');
5+
6+
constbench=common.createBenchmark(main,{
7+
n: [1e5],
8+
terminal: [0,1],
9+
});
10+
11+
functionmain({ n, terminal }){
12+
bench.start();
13+
for(leti=0;i<n;i++){
14+
constinput=newReadable({read(){}});
15+
constoutput=newWritable({write(chunk,encoding,callback){
16+
callback();
17+
}});
18+
constrl=readline.createInterface({
19+
input,
20+
output,
21+
terminal: Boolean(terminal),
22+
});
23+
rl.close();
24+
}
25+
bench.end(n);
26+
}

‎lib/internal/readline/interface.js‎

Lines changed: 68 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,10 @@ const {
1717
MathMax,
1818
MathMaxApply,
1919
NumberIsFinite,
20-
ObjectDefineProperty,
20+
ObjectDefineProperties,
2121
ObjectSetPrototypeOf,
2222
RegExpPrototypeExec,
23+
RegExpPrototypeSymbolSplit,
2324
SafeStringIterator,
2425
StringPrototypeCodePointAt,
2526
StringPrototypeEndsWith,
@@ -172,6 +173,7 @@ function InterfaceConstructor(input, output, completer, terminal) {
172173
letcrlfDelay;
173174
letprompt='> ';
174175
letsignal;
176+
lethistoryOptions;
175177

176178
if(input?.input){
177179
// An options object was given
@@ -210,12 +212,15 @@ function InterfaceConstructor(input, output, completer, terminal) {
210212
crlfDelay=input.crlfDelay;
211213
input=input.input;
212214

213-
input.size=historySize;
214-
input.history=history;
215-
input.removeHistoryDuplicates=removeHistoryDuplicates;
215+
historyOptions={
216+
__proto__: null,
217+
size: historySize,
218+
history,
219+
removeHistoryDuplicates,
220+
};
216221
}
217222

218-
this.setupHistoryManager(input);
223+
this.setupHistoryManager(historyOptions??input);
219224

220225
if(completer!==undefined&&typeofcompleter!=='function'){
221226
thrownewERR_INVALID_ARG_VALUE('completer',completer);
@@ -358,6 +363,30 @@ function InterfaceConstructor(input, output, completer, terminal) {
358363
ObjectSetPrototypeOf(InterfaceConstructor.prototype,EventEmitter.prototype);
359364
ObjectSetPrototypeOf(InterfaceConstructor,EventEmitter);
360365

366+
// Shared descriptors for the history accessors defined on each instance.
367+
// Hoisted to avoid allocating fresh closures on every construction.
368+
constkHistoryAccessorDescriptors={
369+
__proto__: null,
370+
history: {
371+
__proto__: null,configurable: true,enumerable: true,
372+
get(){returnthis.historyManager.history;},
373+
set(newHistory){returnthis.historyManager.history=newHistory;},
374+
},
375+
historyIndex: {
376+
__proto__: null,configurable: true,enumerable: true,
377+
get(){returnthis.historyManager.index;},
378+
set(historyIndex){returnthis.historyManager.index=historyIndex;},
379+
},
380+
historySize: {
381+
__proto__: null,configurable: true,enumerable: true,
382+
get(){returnthis.historyManager.size;},
383+
},
384+
isFlushing: {
385+
__proto__: null,configurable: true,enumerable: true,
386+
get(){returnthis.historyManager.isFlushing;},
387+
},
388+
};
389+
361390
classInterfaceextendsInterfaceConstructor{
362391
getcolumns(){
363392
if(this.output?.columns)returnthis.output.columns;
@@ -388,27 +417,7 @@ class Interface extends InterfaceConstructor {
388417
this.historyManager.initialize(options.onHistoryFileLoaded);
389418
}
390419

391-
ObjectDefineProperty(this,'history',{
392-
__proto__: null,configurable: true,enumerable: true,
393-
get(){returnthis.historyManager.history;},
394-
set(newHistory){returnthis.historyManager.history=newHistory;},
395-
});
396-
397-
ObjectDefineProperty(this,'historyIndex',{
398-
__proto__: null,configurable: true,enumerable: true,
399-
get(){returnthis.historyManager.index;},
400-
set(historyIndex){returnthis.historyManager.index=historyIndex;},
401-
});
402-
403-
ObjectDefineProperty(this,'historySize',{
404-
__proto__: null,configurable: true,enumerable: true,
405-
get(){returnthis.historyManager.size;},
406-
});
407-
408-
ObjectDefineProperty(this,'isFlushing',{
409-
__proto__: null,configurable: true,enumerable: true,
410-
get(){returnthis.historyManager.isFlushing;},
411-
});
420+
ObjectDefineProperties(this,kHistoryAccessorDescriptors);
412421
}
413422

414423
[kSetRawMode](mode){
@@ -622,37 +631,44 @@ class Interface extends InterfaceConstructor {
622631
this[kSawReturnAt]=0;
623632
}
624633

625-
// Run test() on the new string chunk, not on the entire line buffer.
626-
letnewPartContainsEnding=RegExpPrototypeExec(lineEnding,string);
627-
if(newPartContainsEnding!==null){
628-
if(this[kLine_buffer]){
629-
string=this[kLine_buffer]+string;
630-
this[kLine_buffer]=null;
631-
lineEnding.lastIndex=0;// Start the search from the beginning of the string.
632-
newPartContainsEnding=RegExpPrototypeExec(lineEnding,string);
633-
}
634-
this[kSawReturnAt]=StringPrototypeEndsWith(string,'\r') ?
635-
DateNow() :
636-
0;
637-
638-
constindexes=[0,newPartContainsEnding.index,lineEnding.lastIndex];
639-
letnextMatch;
640-
while((nextMatch=RegExpPrototypeExec(lineEnding,string))!==null){
641-
ArrayPrototypePush(indexes,nextMatch.index,lineEnding.lastIndex);
642-
}
643-
constlastIndex=indexes.length-1;
644-
// Either '' or (conceivably) the unfinished portion of the next line
645-
this[kLine_buffer]=StringPrototypeSlice(string,indexes[lastIndex]);
646-
for(leti=1;i<lastIndex;i+=2){
647-
this[kOnLine](StringPrototypeSlice(string,indexes[i-1],indexes[i]));
648-
}
649-
}elseif(string){
650-
// No newlines this time, save what we have for next time
634+
if(!string){
635+
return;
636+
}
637+
638+
// Split the new string chunk, not the entire line buffer: a single
639+
// split pass avoids allocating a match object per line ending.
640+
// When the chunk contains none of the rare line endings, a plain
641+
// string split is much cheaper than the regular expression.
642+
constlines=
643+
StringPrototypeIncludes(string,'\r')||
644+
StringPrototypeIncludes(string,'\u2028')||
645+
StringPrototypeIncludes(string,'\u2029') ?
646+
RegExpPrototypeSymbolSplit(lineEnding,string) :
647+
StringPrototypeSplit(string,'\n');
648+
constlastIndex=lines.length-1;
649+
if(lastIndex===0){
650+
// No line endings this time, save what we have for next time.
651651
if(this[kLine_buffer]){
652652
this[kLine_buffer]+=string;
653653
}else{
654654
this[kLine_buffer]=string;
655655
}
656+
return;
657+
}
658+
659+
this[kSawReturnAt]=StringPrototypeEndsWith(string,'\r') ?
660+
DateNow() :
661+
0;
662+
663+
letfirst=lines[0];
664+
if(this[kLine_buffer]){
665+
first=this[kLine_buffer]+first;
666+
}
667+
// Either '' or (conceivably) the unfinished portion of the next line
668+
this[kLine_buffer]=lines[lastIndex];
669+
this[kOnLine](first);
670+
for(leti=1;i<lastIndex;i++){
671+
this[kOnLine](lines[i]);
656672
}
657673
}
658674

‎lib/readline.js‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,9 @@ function Interface(input, output, completer, terminal) {
115115
FunctionPrototypeCall(InterfaceConstructor,this,
116116
input,output,completer,terminal);
117117

118-
if(process.env.TERM==='dumb'){
118+
// Reading process.env is expensive and _ttyWrite is only used in
119+
// terminal mode, so only check for a dumb terminal when relevant.
120+
if(this.terminal&&process.env.TERM==='dumb'){
119121
this._ttyWrite=FunctionPrototypeBind(_ttyWriteDumb,this);
120122
}
121123
}

0 commit comments

Comments
 (0)