diff --git a/benchmark/readline/readline-createInterface.js b/benchmark/readline/readline-createInterface.js new file mode 100644 index 00000000000000..06cd2817545640 --- /dev/null +++ b/benchmark/readline/readline-createInterface.js @@ -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); +} diff --git a/lib/internal/readline/interface.js b/lib/internal/readline/interface.js index 08f7aaa9e3e7e8..729892ff670d08 100644 --- a/lib/internal/readline/interface.js +++ b/lib/internal/readline/interface.js @@ -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; - - 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 + if (!string) { + return; + } + + // 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]); } } diff --git a/lib/readline.js b/lib/readline.js index 1eaf35d75029e2..7fb0f9d40af37d 100644 --- a/lib/readline.js +++ b/lib/readline.js @@ -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); } }