Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions benchmark/readline/readline-createInterface.js
Original file line number Diff line number Diff line change
@@ -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);
}
120 changes: 68 additions & 52 deletions lib/internal/readline/interface.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,10 @@ const {
MathMax,
MathMaxApply,
NumberIsFinite,
ObjectDefineProperty,
ObjectDefineProperties,
ObjectSetPrototypeOf,
RegExpPrototypeExec,
RegExpPrototypeSymbolSplit,
SafeStringIterator,
StringPrototypeCodePointAt,
StringPrototypeEndsWith,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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]);
}
}

Expand Down
4 changes: 3 additions & 1 deletion lib/readline.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
Expand Down
Loading