node/benchmark/streams/iter-throughput-transform.js
James M Snell e78ccd8017 benchmark: add benchmarks for experimental stream/iter
Signed-off-by: James M Snell <jasnell@gmail.com>
Assisted-By: Claude/Opus 4.6
PR-URL: https://github.com/nodejs/node/pull/62066
Reviewed-By: Robert Nagy <ronagy@icloud.com>
Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com>
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
2026-03-27 19:55:35 -07:00

146 lines
4 KiB
JavaScript

// Throughput benchmark: data flow through a single stateless transform.
// Uses buffer copy (allocate + memcpy) so pipeline overhead is measurable.
'use strict';
const common = require('../common.js');
const { Readable, Transform, Writable, pipeline } = require('stream');
const bench = common.createBenchmark(main, {
api: ['classic', 'webstream', 'iter', 'iter-sync'],
datasize: [1024 * 1024, 16 * 1024 * 1024, 64 * 1024 * 1024],
n: [5],
}, {
flags: ['--experimental-stream-iter'],
});
const CHUNK_SIZE = 64 * 1024;
// Buffer copy transform: allocate + memcpy. Cheap enough that pipeline
// overhead is a measurable fraction of total time, but non-trivial (new
// buffer per chunk, so it's a real transform that produces new data).
function copyBuf(buf) {
return Buffer.copyBytesFrom(buf);
}
function main({ api, datasize, n }) {
const chunk = Buffer.alloc(CHUNK_SIZE, 'abcdefghij');
const totalOps = (datasize * n) / (1024 * 1024);
switch (api) {
case 'classic':
return benchClassic(chunk, datasize, n, totalOps);
case 'webstream':
return benchWebStream(chunk, datasize, n, totalOps);
case 'iter':
return benchIter(chunk, datasize, n, totalOps);
case 'iter-sync':
return benchIterSync(chunk, datasize, n, totalOps);
}
}
function benchClassic(chunk, datasize, n, totalOps) {
function run(cb) {
let remaining = datasize;
const r = new Readable({
read() {
if (remaining <= 0) { this.push(null); return; }
const size = Math.min(remaining, chunk.length);
remaining -= size;
this.push(size === chunk.length ? chunk : chunk.subarray(0, size));
},
});
const t = new Transform({
transform(data, enc, cb) {
cb(null, copyBuf(data));
},
});
const w = new Writable({ write(data, enc, cb) { cb(); } });
pipeline(r, t, w, cb);
}
let i = 0;
bench.start();
(function next() {
if (i++ >= n) return bench.end(totalOps);
run(next);
})();
}
function benchWebStream(chunk, datasize, n, totalOps) {
async function run() {
let remaining = datasize;
const rs = new ReadableStream({
pull(controller) {
if (remaining <= 0) { controller.close(); return; }
const size = Math.min(remaining, chunk.length);
remaining -= size;
controller.enqueue(
size === chunk.length ? chunk : chunk.subarray(0, size));
},
});
const ts = new TransformStream({
transform(c, controller) {
controller.enqueue(copyBuf(c));
},
});
const ws = new WritableStream({ write() {} });
await rs.pipeThrough(ts).pipeTo(ws);
}
(async () => {
bench.start();
for (let i = 0; i < n; i++) await run();
bench.end(totalOps);
})();
}
function benchIter(chunk, datasize, n, totalOps) {
const { pipeTo } = require('stream/iter');
const upper = (chunks) => {
if (chunks === null) return null;
return chunks.map((c) => copyBuf(c));
};
async function run() {
let remaining = datasize;
async function* source() {
while (remaining > 0) {
const size = Math.min(remaining, chunk.length);
remaining -= size;
yield [size === chunk.length ? chunk : chunk.subarray(0, size)];
}
}
await pipeTo(source(), upper,
{ write() {}, writeSync() { return true; } });
}
(async () => {
bench.start();
for (let i = 0; i < n; i++) await run();
bench.end(totalOps);
})();
}
function benchIterSync(chunk, datasize, n, totalOps) {
const { pipeToSync } = require('stream/iter');
const upper = (chunks) => {
if (chunks === null) return null;
return chunks.map((c) => copyBuf(c));
};
bench.start();
for (let i = 0; i < n; i++) {
let remaining = datasize;
function* source() {
while (remaining > 0) {
const size = Math.min(remaining, chunk.length);
remaining -= size;
yield [size === chunk.length ? chunk : chunk.subarray(0, size)];
}
}
pipeToSync(source(), upper, { writeSync() {} });
}
bench.end(totalOps);
}