Skip to content

Commit 3d6eb40

Browse files
committed
stream: fast path single-destination pipe
Avoid EventEmitter dispatch for the readable pipe data handler when it is the only data listener. Fall back to normal data event emission when additional data listeners are present to preserve listener ordering. Track the pipe data handler lazily so non-piped readable streams do not pay an extra ReadableState property initialization or data listener identity check. Signed-off-by: Kamat, Trivikram <16024985+trivikr@users.noreply.github.com> Assisted-by: openai:gpt-5.5
1 parent 4639dcb commit 3d6eb40

2 files changed

Lines changed: 65 additions & 4 deletions

File tree

lib/internal/streams/readable.js

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ const kErroredValue = Symbol('kErroredValue');
111111
const kDefaultEncodingValue = Symbol('kDefaultEncodingValue');
112112
const kDecoderValue = Symbol('kDecoderValue');
113113
const kEncodingValue = Symbol('kEncodingValue');
114+
const kPipeData = Symbol('kPipeData');
114115

115116
const kEnded = 1 << 9;
116117
const kEndEmitted = 1 << 10;
@@ -289,6 +290,7 @@ function ReadableState(options, stream, isDuplex) {
289290
this.bufferIndex = 0;
290291
this.length = 0;
291292
this.pipes = [];
293+
this[kPipeData] = null;
292294

293295
// Should close be emitted on destroy. Defaults to true.
294296
if (options && options.emitClose === false) this[kState] &= ~kEmitClose;
@@ -564,8 +566,7 @@ function addChunk(stream, state, chunk, addToFront) {
564566
state.awaitDrainWriters = null;
565567
}
566568

567-
state[kState] |= kDataEmitted;
568-
stream.emit('data', chunk);
569+
emitData(stream, state, chunk);
569570
} else {
570571
// Update the buffer info.
571572
state.length += (state[kState] & kObjectMode) !== 0 ? 1 : chunk.length;
@@ -790,13 +791,21 @@ Readable.prototype.read = function(n) {
790791
}
791792

792793
if (ret !== null && (state[kState] & (kErrorEmitted | kCloseEmitted)) === 0) {
793-
state[kState] |= kDataEmitted;
794-
this.emit('data', ret);
794+
emitData(this, state, ret);
795795
}
796796

797797
return ret;
798798
};
799799

800+
function emitData(stream, state, chunk) {
801+
state[kState] |= kDataEmitted;
802+
if (stream._events.data === state[kPipeData]) {
803+
state[kPipeData](chunk);
804+
} else {
805+
stream.emit('data', chunk);
806+
}
807+
}
808+
800809
function onEofChunk(stream, state) {
801810
debug('onEofChunk');
802811
if ((state[kState] & kEnded) !== 0) return;
@@ -976,6 +985,8 @@ Readable.prototype.pipe = function(dest, pipeOpts) {
976985
src.removeListener('end', onend);
977986
src.removeListener('end', unpipe);
978987
src.removeListener('data', ondata);
988+
if (state[kPipeData] === ondata)
989+
state[kPipeData] = null;
979990

980991
cleanedUp = true;
981992

@@ -1016,6 +1027,8 @@ Readable.prototype.pipe = function(dest, pipeOpts) {
10161027
}
10171028

10181029
src.on('data', ondata);
1030+
if (src._events.data === ondata)
1031+
state[kPipeData] = ondata;
10191032
function ondata(chunk) {
10201033
debug('ondata');
10211034
try {

test/parallel/test-stream2-basic.js

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,54 @@ class TestWriter extends EE {
146146
r.pipe(w);
147147
}
148148

149+
{
150+
// Verify data listener ordering around pipe.
151+
function makeWritable(events) {
152+
return new W({
153+
write(chunk, enc, cb) {
154+
events.push('write');
155+
cb();
156+
},
157+
});
158+
}
159+
160+
{
161+
const events = [];
162+
const r = R.from(['x']);
163+
const w = makeWritable(events);
164+
165+
r.on('data', () => events.push('data'));
166+
r.pipe(w);
167+
w.on('finish', common.mustCall(() => {
168+
assert.deepStrictEqual(events, ['data', 'write']);
169+
}));
170+
}
171+
172+
{
173+
const events = [];
174+
const r = R.from(['x']);
175+
const w = makeWritable(events);
176+
177+
r.pipe(w);
178+
r.on('data', () => events.push('data'));
179+
w.on('finish', common.mustCall(() => {
180+
assert.deepStrictEqual(events, ['write', 'data']);
181+
}));
182+
}
183+
184+
{
185+
const events = [];
186+
const r = R.from(['x']);
187+
const w = makeWritable(events);
188+
189+
r.pipe(w);
190+
r.prependListener('data', () => events.push('data'));
191+
w.on('finish', common.mustCall(() => {
192+
assert.deepStrictEqual(events, ['data', 'write']);
193+
}));
194+
}
195+
}
196+
149197

150198
[1, 2, 3, 4, 5, 6, 7, 8, 9].forEach(function(SPLIT) {
151199
// Verify unpipe

0 commit comments

Comments
 (0)