Skip to content

Commit 9041ad0

Browse files
mcollinaavivkeller
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 5841155 commit 9041ad0

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+
const common = require('../common.js');
3+
const readline = require('readline');
4+
const { Readable, Writable } = require('stream');
5+
6+
const bench = common.createBenchmark(main, {
7+
n: [1e5],
8+
terminal: [0, 1],
9+
});
10+
11+
function main({ n, terminal }) {
12+
bench.start();
13+
for (let i = 0; i < n; i++) {
14+
const input = new Readable({ read() {} });
15+
const output = new Writable({ write(chunk, encoding, callback) {
16+
callback();
17+
} });
18+
const rl = 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
let crlfDelay;
173174
let prompt = '> ';
174175
let signal;
176+
let historyOptions;
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 && typeof completer !== 'function') {
221226
throw new ERR_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+
const kHistoryAccessorDescriptors = {
369+
__proto__: null,
370+
history: {
371+
__proto__: null, configurable: true, enumerable: true,
372+
get() { return this.historyManager.history; },
373+
set(newHistory) { return this.historyManager.history = newHistory; },
374+
},
375+
historyIndex: {
376+
__proto__: null, configurable: true, enumerable: true,
377+
get() { return this.historyManager.index; },
378+
set(historyIndex) { return this.historyManager.index = historyIndex; },
379+
},
380+
historySize: {
381+
__proto__: null, configurable: true, enumerable: true,
382+
get() { return this.historyManager.size; },
383+
},
384+
isFlushing: {
385+
__proto__: null, configurable: true, enumerable: true,
386+
get() { return this.historyManager.isFlushing; },
387+
},
388+
};
389+
361390
class Interface extends InterfaceConstructor {
362391
get columns() {
363392
if (this.output?.columns) return this.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() { return this.historyManager.history; },
394-
set(newHistory) { return this.historyManager.history = newHistory; },
395-
});
396-
397-
ObjectDefineProperty(this, 'historyIndex', {
398-
__proto__: null, configurable: true, enumerable: true,
399-
get() { return this.historyManager.index; },
400-
set(historyIndex) { return this.historyManager.index = historyIndex; },
401-
});
402-
403-
ObjectDefineProperty(this, 'historySize', {
404-
__proto__: null, configurable: true, enumerable: true,
405-
get() { return this.historyManager.size; },
406-
});
407-
408-
ObjectDefineProperty(this, 'isFlushing', {
409-
__proto__: null, configurable: true, enumerable: true,
410-
get() { return this.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-
let newPartContainsEnding = 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-
const indexes = [0, newPartContainsEnding.index, lineEnding.lastIndex];
639-
let nextMatch;
640-
while ((nextMatch = RegExpPrototypeExec(lineEnding, string)) !== null) {
641-
ArrayPrototypePush(indexes, nextMatch.index, lineEnding.lastIndex);
642-
}
643-
const lastIndex = indexes.length - 1;
644-
// Either '' or (conceivably) the unfinished portion of the next line
645-
this[kLine_buffer] = StringPrototypeSlice(string, indexes[lastIndex]);
646-
for (let i = 1; i < lastIndex; i += 2) {
647-
this[kOnLine](StringPrototypeSlice(string, indexes[i - 1], indexes[i]));
648-
}
649-
} else if (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+
const lines =
643+
StringPrototypeIncludes(string, '\r') ||
644+
StringPrototypeIncludes(string, '\u2028') ||
645+
StringPrototypeIncludes(string, '\u2029') ?
646+
RegExpPrototypeSymbolSplit(lineEnding, string) :
647+
StringPrototypeSplit(string, '\n');
648+
const lastIndex = 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+
let first = 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 (let i = 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)