Commit 236b53ff62d for nodejs

commit 236b53ff62df569d76125d519793d7bdbbe30799
Author: Yagiz Nizipli <yagiz@nizipli.com>
Date:   Mon Sep 21 13:11:36 2026 -0400

    stream: speed up flowing pipe of buffers

    flow() pulls one already-buffered chunk and calls _read() for the
    next one on every iteration. That goes through the general read()
    path, which updates a holey buffer array and then pulls the chunk
    back out.

    While a synchronous byte-mode flow is in progress, keep that
    prefetched chunk on the readable state and emit it directly.
    _read() of the next chunk still runs before 'data', and a nested
    read() moves the chunk back onto the buffer.

    benchmark/streams/pipe.js is about 77% faster (15 runs).
    pipe-object-mode, readable-readall, and readable-bigread stay
    within noise.

    Assisted-by: a closed-source coding agent
    Signed-off-by: Yagiz Nizipli <yagiz@nizipli.com>
    PR-URL: https://github.com/nodejs/node/pull/66182
    Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
    Reviewed-By: Robert Nagy <ronagy@icloud.com>
    Reviewed-By: James M Snell <jasnell@gmail.com>
    Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
    Reviewed-By: Zeyu "Alex" Yang <himself65@outlook.com>

diff --git a/lib/internal/streams/destroy.js b/lib/internal/streams/destroy.js
index 3119de5dd9d..e488e18db9a 100644
--- a/lib/internal/streams/destroy.js
+++ b/lib/internal/streams/destroy.js
@@ -182,6 +182,7 @@ function undestroy() {
     r.errored = null;
     r.errorEmitted = false;
     r.reading = false;
+    r.fastChunk = null;
     r.ended = r.readable === false;
     r.endEmitted = r.readable === false;
   }
diff --git a/lib/internal/streams/readable.js b/lib/internal/streams/readable.js
index 1b8f9ab3adb..fea818e1397 100644
--- a/lib/internal/streams/readable.js
+++ b/lib/internal/streams/readable.js
@@ -139,6 +139,9 @@ const kPaused = 1 << 26;
 const kDataListening = 1 << 27;
 const kEndScheduled = 1 << 28;
 const kEofReadablePending = 1 << 29;
+// Set only while flowSync() is inside _read(). push() then keeps the
+// chunk on state.fastChunk instead of the buffer array.
+const kFastPush = 1 << 30;

 // TODO(benjamingr) it is likely slower to do it this way than with free functions
 function makeBitMapDescriptor(bit) {
@@ -296,6 +299,8 @@ function ReadableState(options, stream, isDuplex) {
   this.buffer = [];
   this.bufferIndex = 0;
   this.length = 0;
+  // Chunk prefetched by flowSync(), kept off the buffer array.
+  this.fastChunk = null;
   this.pipes = [];

   // Should close be emitted on destroy. Defaults to true.
@@ -400,9 +405,29 @@ Readable.prototype[SymbolAsyncDispose] = async function() {
 // similar to how Writable.write() returns true if you should
 // write() some more.
 Readable.prototype.push = function(chunk, encoding) {
+  const state = this._readableState;
+
+  // flowSync() is inside _read() and wants a single buffer parked on
+  // fastChunk. A second push, a string, or EOF drops back to the buffer.
+  if ((state[kState] & kFastPush) !== 0 && encoding == null &&
+      state.fastChunk == null && chunk instanceof Buffer && chunk.length > 0) {
+    state.fastChunk = chunk;
+    state.length = chunk.length;
+    state[kState] &= ~kReading;
+    return chunk.length < state.highWaterMark;
+  }
+  if ((state[kState] & kFastPush) !== 0) {
+    state[kState] &= ~kFastPush;
+    if (state.fastChunk != null) {
+      const first = state.fastChunk;
+      state.fastChunk = null;
+      state.length = 0;
+      readableAddChunkPushByteMode(this, state, first);
+    }
+  }
+
   debug('push', chunk);

-  const state = this._readableState;
   return (state[kState] & kObjectMode) === 0 ?
     readableAddChunkPushByteMode(this, state, chunk, encoding) :
     readableAddChunkPushObjectMode(this, state, chunk, encoding);
@@ -601,6 +626,8 @@ Readable.prototype.isPaused = function() {
 // Backwards compatibility.
 Readable.prototype.setEncoding = function(enc) {
   const state = this._readableState;
+  if (state.fastChunk != null)
+    materializeFastChunk(state);

   const decoder = new StringDecoder(enc);
   state.decoder = decoder;
@@ -667,6 +694,11 @@ function howMuchToRead(n, state) {

 // You can override either this method, or the async _read(n) below.
 Readable.prototype.read = function(n) {
+  // A nested read() during flowSync()'s 'data' event must see the
+  // prefetched chunk. Null for every read that is not inside that loop.
+  if (this._readableState.fastChunk != null)
+    materializeFastChunk(this._readableState);
+
   debug('read', n);
   // Same as parseInt(undefined, 10), however V8 7.3 performance regressed
   // in this scenario, so we are doing it manually.
@@ -1336,9 +1368,91 @@ Readable.prototype.pause = function() {
 function flow(stream) {
   const state = stream._readableState;
   debug('flow');
+  // Byte-mode pipe sits in read() to pull one already-buffered chunk and
+  // refill. That read is most of the per-chunk cost. flowSync() keeps the
+  // same prefetch order without the buffer array or the general read path.
+  if (flowSync(stream, state))
+    return;
   while ((state[kState] & kFlowing) !== 0 && stream.read() !== null);
 }

+const kFastFlowNeed = kConstructed | kFlowing | kDataListening;
+const kFastFlowBlock = kObjectMode | kDecoder | kEnded | kDestroyed |
+  kErrored | kPaused | kReading | kSync;
+
+// Returns true when this call owned the flowing loop, including any
+// fallback to read() after the fast path stops.
+function flowSync(stream, state) {
+  const bits = state[kState];
+  if ((bits & kFastFlowNeed) !== kFastFlowNeed ||
+      (bits & kFastFlowBlock) !== 0 ||
+      !(state.highWaterMark > 0) ||
+      state.fastChunk != null) {
+    return false;
+  }
+
+  if (state.length !== 0) {
+    const buf = state.buffer;
+    const idx = state.bufferIndex;
+    const chunk = buf[idx];
+    // Only the one-chunk prefetch left by read(0) / the previous read.
+    if (buf.length !== idx + 1 || chunk == null || chunk.length !== state.length)
+      return false;
+    buf.length = 0;
+    state.bufferIndex = 0;
+    state.fastChunk = chunk;
+  }
+
+  while ((state[kState] & kFlowing) !== 0) {
+    const current = state.fastChunk;
+    if (current == null)
+      break;
+    if ((state[kState] & kFastFlowBlock) !== 0)
+      break;
+
+    // _read() of the next chunk runs before 'data', matching read().
+    state.fastChunk = null;
+    state.length = 0;
+    state[kState] |= kReading | kSync | kFastPush;
+    try {
+      stream._read(state.highWaterMark);
+    } catch (err) {
+      state[kState] &= ~(kSync | kFastPush);
+      errorOrDestroy(stream, err);
+      break;
+    }
+    state[kState] &= ~(kSync | kFastPush);
+
+    if ((state[kState] & (kErrorEmitted | kCloseEmitted)) === 0) {
+      state[kState] |= kDataEmitted;
+      stream.emit('data', current);
+    }
+
+    // Nested read() moved fastChunk into the buffer and may have refilled.
+    if (state.fastChunk == null && state.length !== 0)
+      break;
+  }
+
+  if (state.fastChunk != null)
+    materializeFastChunk(state);
+
+  while ((state[kState] & kFlowing) !== 0 && stream.read() !== null);
+  return true;
+}
+
+// Put fastChunk at the head of the buffer. state.length already counts it.
+function materializeFastChunk(state) {
+  const chunk = state.fastChunk;
+  if (chunk == null)
+    return;
+  state.fastChunk = null;
+  if (state.bufferIndex > 0) {
+    state.buffer[--state.bufferIndex] = chunk;
+  } else {
+    state.buffer.unshift(chunk);
+  }
+}
+
 // Wrap an old-style stream as the async data source.
 // This is *not* part of the readable stream interface.
 // It is an ugly unfortunate mess of history.
@@ -1724,7 +1838,16 @@ ObjectDefineProperties(Readable.prototype, {
     __proto__: null,
     enumerable: false,
     get: function() {
-      return this._readableState?.buffer;
+      const state = this._readableState;
+      if (state == null)
+        return undefined;
+      if (state.fastChunk == null)
+        return state.buffer;
+      if (state.bufferIndex === state.buffer.length)
+        return [state.fastChunk];
+      const out = state.buffer.slice(state.bufferIndex);
+      out.push(state.fastChunk);
+      return out;
     },
   },