Commit ea82bc4

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 f58ac82 commit ea82bc4

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)
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Commit ea82bc4

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 f58ac82 commit ea82bc4

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)
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Commit ea82bc4

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 f58ac82 commit ea82bc4

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)
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Commit ea82bc4

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 f58ac82 commit ea82bc4

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)
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Commit ea82bc4

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 f58ac82 commit ea82bc4

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)
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Commit ea82bc4

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 f58ac82 commit ea82bc4

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)
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Commit ea82bc4

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 f58ac82 commit ea82bc4

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)
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Commit ea82bc4

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 f58ac82 commit ea82bc4

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)