mirror of
https://github.com/nodejs/node.git
synced 2026-10-28 10:15:07 +08:00
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: https://github.com/nodejs/node/pull/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>
This commit is contained in:
1 parent
5841155d92
commit
9041ad047a
3 files changed
+96
-52
No files matched your search
@@ -0,0 +1,26 @@
|
||||
'use strict';
|
||||
const common = require('../common.js');
|
||||
const readline = require('readline');
|
||||
const { Readable, Writable } = require('stream');
|
||||
|
||||
const bench = common.createBenchmark(main, {
|
||||
n: [1e5],
|
||||
terminal: [0, 1],
|
||||
});
|
||||
|
||||
function main({ n, terminal }) {
|
||||
bench.start();
|
||||
for (let i = 0; i < n; i++) {
|
||||
const input = new Readable({ read() {} });
|
||||
const output = new Writable({ write(chunk, encoding, callback) {
|
||||
callback();
|
||||
} });
|
||||
const rl = readline.createInterface({
|
||||
input,
|
||||
output,
|
||||
terminal: Boolean(terminal),
|
||||
});
|
||||
rl.close();
|
||||
}
|
||||
bench.end(n);
|
||||
}
|
||||
@@ -17,9 +17,10 @@ const {
|
||||
MathMax,
|
||||
MathMaxApply,
|
||||
NumberIsFinite,
|
||||
ObjectDefineProperty,
|
||||
ObjectDefineProperties,
|
||||
ObjectSetPrototypeOf,
|
||||
RegExpPrototypeExec,
|
||||
RegExpPrototypeSymbolSplit,
|
||||
SafeStringIterator,
|
||||
StringPrototypeCodePointAt,
|
||||
StringPrototypeEndsWith,
|
||||
@@ -172,6 +173,7 @@ function InterfaceConstructor(input, output, completer, terminal) {
|
||||
let crlfDelay;
|
||||
let prompt = '> ';
|
||||
let signal;
|
||||
let historyOptions;
|
||||
|
||||
if (input?.input) {
|
||||
// An options object was given
|
||||
@@ -210,12 +212,15 @@ function InterfaceConstructor(input, output, completer, terminal) {
|
||||
crlfDelay = input.crlfDelay;
|
||||
input = input.input;
|
||||
|
||||
input.size = historySize;
|
||||
input.history = history;
|
||||
input.removeHistoryDuplicates = removeHistoryDuplicates;
|
||||
historyOptions = {
|
||||
__proto__: null,
|
||||
size: historySize,
|
||||
history,
|
||||
removeHistoryDuplicates,
|
||||
};
|
||||
}
|
||||
|
||||
this.setupHistoryManager(input);
|
||||
this.setupHistoryManager(historyOptions ?? input);
|
||||
|
||||
if (completer !== undefined && typeof completer !== 'function') {
|
||||
throw new ERR_INVALID_ARG_VALUE('completer', completer);
|
||||
@@ -358,6 +363,30 @@ function InterfaceConstructor(input, output, completer, terminal) {
|
||||
ObjectSetPrototypeOf(InterfaceConstructor.prototype, EventEmitter.prototype);
|
||||
ObjectSetPrototypeOf(InterfaceConstructor, EventEmitter);
|
||||
|
||||
// Shared descriptors for the history accessors defined on each instance.
|
||||
// Hoisted to avoid allocating fresh closures on every construction.
|
||||
const kHistoryAccessorDescriptors = {
|
||||
__proto__: null,
|
||||
history: {
|
||||
__proto__: null, configurable: true, enumerable: true,
|
||||
get() { return this.historyManager.history; },
|
||||
set(newHistory) { return this.historyManager.history = newHistory; },
|
||||
},
|
||||
historyIndex: {
|
||||
__proto__: null, configurable: true, enumerable: true,
|
||||
get() { return this.historyManager.index; },
|
||||
set(historyIndex) { return this.historyManager.index = historyIndex; },
|
||||
},
|
||||
historySize: {
|
||||
__proto__: null, configurable: true, enumerable: true,
|
||||
get() { return this.historyManager.size; },
|
||||
},
|
||||
isFlushing: {
|
||||
__proto__: null, configurable: true, enumerable: true,
|
||||
get() { return this.historyManager.isFlushing; },
|
||||
},
|
||||
};
|
||||
|
||||
class Interface extends InterfaceConstructor {
|
||||
get columns() {
|
||||
if (this.output?.columns) return this.output.columns;
|
||||
@@ -388,27 +417,7 @@ class Interface extends InterfaceConstructor {
|
||||
this.historyManager.initialize(options.onHistoryFileLoaded);
|
||||
}
|
||||
|
||||
ObjectDefineProperty(this, 'history', {
|
||||
__proto__: null, configurable: true, enumerable: true,
|
||||
get() { return this.historyManager.history; },
|
||||
set(newHistory) { return this.historyManager.history = newHistory; },
|
||||
});
|
||||
|
||||
ObjectDefineProperty(this, 'historyIndex', {
|
||||
__proto__: null, configurable: true, enumerable: true,
|
||||
get() { return this.historyManager.index; },
|
||||
set(historyIndex) { return this.historyManager.index = historyIndex; },
|
||||
});
|
||||
|
||||
ObjectDefineProperty(this, 'historySize', {
|
||||
__proto__: null, configurable: true, enumerable: true,
|
||||
get() { return this.historyManager.size; },
|
||||
});
|
||||
|
||||
ObjectDefineProperty(this, 'isFlushing', {
|
||||
__proto__: null, configurable: true, enumerable: true,
|
||||
get() { return this.historyManager.isFlushing; },
|
||||
});
|
||||
ObjectDefineProperties(this, kHistoryAccessorDescriptors);
|
||||
}
|
||||
|
||||
[kSetRawMode](mode) {
|
||||
@@ -622,37 +631,44 @@ class Interface extends InterfaceConstructor {
|
||||
this[kSawReturnAt] = 0;
|
||||
}
|
||||
|
||||
// Run test() on the new string chunk, not on the entire line buffer.
|
||||
let newPartContainsEnding = RegExpPrototypeExec(lineEnding, string);
|
||||
if (newPartContainsEnding !== null) {
|
||||
if (this[kLine_buffer]) {
|
||||
string = this[kLine_buffer] + string;
|
||||
this[kLine_buffer] = null;
|
||||
lineEnding.lastIndex = 0; // Start the search from the beginning of the string.
|
||||
newPartContainsEnding = RegExpPrototypeExec(lineEnding, string);
|
||||
}
|
||||
this[kSawReturnAt] = StringPrototypeEndsWith(string, '\r') ?
|
||||
DateNow() :
|
||||
0;
|
||||
if (!string) {
|
||||
return;
|
||||
}
|
||||
|
||||
const indexes = [0, newPartContainsEnding.index, lineEnding.lastIndex];
|
||||
let nextMatch;
|
||||
while ((nextMatch = RegExpPrototypeExec(lineEnding, string)) !== null) {
|
||||
ArrayPrototypePush(indexes, nextMatch.index, lineEnding.lastIndex);
|
||||
}
|
||||
const lastIndex = indexes.length - 1;
|
||||
// Either '' or (conceivably) the unfinished portion of the next line
|
||||
this[kLine_buffer] = StringPrototypeSlice(string, indexes[lastIndex]);
|
||||
for (let i = 1; i < lastIndex; i += 2) {
|
||||
this[kOnLine](StringPrototypeSlice(string, indexes[i - 1], indexes[i]));
|
||||
}
|
||||
} else if (string) {
|
||||
// No newlines this time, save what we have for next time
|
||||
// Split the new string chunk, not the entire line buffer: a single
|
||||
// split pass avoids allocating a match object per line ending.
|
||||
// When the chunk contains none of the rare line endings, a plain
|
||||
// string split is much cheaper than the regular expression.
|
||||
const lines =
|
||||
StringPrototypeIncludes(string, '\r') ||
|
||||
StringPrototypeIncludes(string, '\u2028') ||
|
||||
StringPrototypeIncludes(string, '\u2029') ?
|
||||
RegExpPrototypeSymbolSplit(lineEnding, string) :
|
||||
StringPrototypeSplit(string, '\n');
|
||||
const lastIndex = lines.length - 1;
|
||||
if (lastIndex === 0) {
|
||||
// No line endings this time, save what we have for next time.
|
||||
if (this[kLine_buffer]) {
|
||||
this[kLine_buffer] += string;
|
||||
} else {
|
||||
this[kLine_buffer] = string;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
this[kSawReturnAt] = StringPrototypeEndsWith(string, '\r') ?
|
||||
DateNow() :
|
||||
0;
|
||||
|
||||
let first = lines[0];
|
||||
if (this[kLine_buffer]) {
|
||||
first = this[kLine_buffer] + first;
|
||||
}
|
||||
// Either '' or (conceivably) the unfinished portion of the next line
|
||||
this[kLine_buffer] = lines[lastIndex];
|
||||
this[kOnLine](first);
|
||||
for (let i = 1; i < lastIndex; i++) {
|
||||
this[kOnLine](lines[i]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+3
-1
@@ -115,7 +115,9 @@ function Interface(input, output, completer, terminal) {
|
||||
FunctionPrototypeCall(InterfaceConstructor, this,
|
||||
input, output, completer, terminal);
|
||||
|
||||
if (process.env.TERM === 'dumb') {
|
||||
// Reading process.env is expensive and _ttyWrite is only used in
|
||||
// terminal mode, so only check for a dumb terminal when relevant.
|
||||
if (this.terminal && process.env.TERM === 'dumb') {
|
||||
this._ttyWrite = FunctionPrototypeBind(_ttyWriteDumb, this);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user