tee() + draining both branches sustains ~2.18M source chunks/sec vs ~6.7M for a raw reader.read() loop (64 KiB chunks, 32 MiB per pass, macOS arm64, canary, steady state after JIT warmup) — ~460ns per source chunk vs ~150ns.
readableStreamDefaultTee's pull-through allocates a read request per chunk, enqueues into both branch controllers (each paying the queue-wrapper and pull-if-needed promise costs), and re-arms via microtask (readAgain handling) per chunk.
Worth investigating: synchronous re-arm when both branches have outstanding demand, cutting the per-chunk deferred, and letting the branch controllers share the chunk without redundant bookkeeping. This overlaps with the queue-wrapper and read-path allocation issues, so it should land after/with those and be re-measured.
Repro benchmark (same harness as the linked read-path issue, scenario):
const [a, b] = makeStream().tee();
const drain = async (s) => {
const r = s.getReader();
while (!(await r.read()).done);
};
await Promise.all([drain(a), drain(b)]);
Related: #35768 (read/pipe path allocations), #35769 (identity pipeThrough)
tee()+ draining both branches sustains ~2.18M source chunks/sec vs ~6.7M for a rawreader.read()loop (64 KiB chunks, 32 MiB per pass, macOS arm64, canary, steady state after JIT warmup) — ~460ns per source chunk vs ~150ns.readableStreamDefaultTee's pull-through allocates a read request per chunk, enqueues into both branch controllers (each paying the queue-wrapper and pull-if-needed promise costs), and re-arms via microtask (readAgainhandling) per chunk.Worth investigating: synchronous re-arm when both branches have outstanding demand, cutting the per-chunk deferred, and letting the branch controllers share the chunk without redundant bookkeeping. This overlaps with the queue-wrapper and read-path allocation issues, so it should land after/with those and be re-measured.
Repro benchmark (same harness as the linked read-path issue, scenario):
Related: #35768 (read/pipe path allocations), #35769 (identity pipeThrough)