Skip to content
Closed
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
103 changes: 103 additions & 0 deletions benchmark/fs/bench-filehandle-pipetosync.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
// Benchmark: pipeToSync with sync compression transforms.
// Measures fully synchronous file-to-file pipeline (no threadpool, no promises).
'use strict';

const common = require('../common.js');
const fs = require('fs');
const { openSync, closeSync, writeSync, unlinkSync } = fs;

const tmpdir = require('../../test/common/tmpdir');
tmpdir.refresh();
const srcFile = tmpdir.resolve(`.removeme-sync-bench-src-${process.pid}`);
const dstFile = tmpdir.resolve(`.removeme-sync-bench-dst-${process.pid}`);

const bench = common.createBenchmark(main, {
compression: ['gzip', 'deflate', 'brotli', 'zstd'],
filesize: [1024 * 1024, 16 * 1024 * 1024, 64 * 1024 * 1024],
n: [5],
}, {
flags: ['--experimental-stream-iter'],
});

function main({ compression, filesize, n }) {
// Create the fixture file with repeating lowercase ASCII
const chunk = Buffer.alloc(Math.min(filesize, 64 * 1024), 'abcdefghij');
const fd = openSync(srcFile, 'w');
let remaining = filesize;
while (remaining > 0) {
const toWrite = Math.min(remaining, chunk.length);
writeSync(fd, chunk, 0, toWrite);
remaining -= toWrite;
}
closeSync(fd);

const { pipeToSync } = require('stream/iter');
const {
compressGzipSync,
compressDeflateSync,
compressBrotliSync,
compressZstdSync,
} = require('zlib/iter');
const { open } = fs.promises;

const compressFactory = {
gzip: compressGzipSync,
deflate: compressDeflateSync,
brotli: compressBrotliSync,
zstd: compressZstdSync,
}[compression];

// Stateless uppercase transform (sync)
const upper = (chunks) => {
if (chunks === null) return null;
const out = new Array(chunks.length);
for (let j = 0; j < chunks.length; j++) {
const src = chunks[j];
const buf = Buffer.allocUnsafe(src.length);
for (let i = 0; i < src.length; i++) {
const b = src[i];
buf[i] = (b >= 0x61 && b <= 0x7a) ? b - 0x20 : b;
}
out[j] = buf;
}
return out;
};

// Use a synchronous wrapper since pipeToSync is fully sync.
// We need FileHandle for pullSync/writer, so open async then run sync.
(async () => {
const srcFh = await open(srcFile, 'r');
const dstFh = await open(dstFile, 'w');

// Warm up
runSync(srcFh, dstFh, upper, compressFactory, pipeToSync);

// Reset file positions for the benchmark
await srcFh.close();
await dstFh.close();

bench.start();
let totalBytes = 0;
for (let i = 0; i < n; i++) {
const src = await open(srcFile, 'r');
const dst = await open(dstFile, 'w');
totalBytes += runSync(src, dst, upper, compressFactory, pipeToSync);
await src.close();
await dst.close();
}
bench.end(totalBytes / (1024 * 1024));

cleanup();
})();
}

function runSync(srcFh, dstFh, upper, compressFactory, pipeToSync) {
const w = dstFh.writer();
pipeToSync(srcFh.pullSync(upper, compressFactory()), w);
return w.endSync();
}

function cleanup() {
try { unlinkSync(srcFile); } catch { /* Ignore */ }
try { unlinkSync(dstFile); } catch { /* Ignore */ }
}
201 changes: 201 additions & 0 deletions benchmark/fs/bench-filehandle-pull-vs-webstream.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
// Compare FileHandle.createReadStream() vs readableWebStream() vs pull()
// reading a large file through two transforms: uppercase then compress.
'use strict';

const common = require('../common.js');
const fs = require('fs');
const zlib = require('zlib');
const { Transform, Writable, pipeline } = require('stream');

const tmpdir = require('../../test/common/tmpdir');
tmpdir.refresh();
const filename = tmpdir.resolve(`.removeme-benchmark-garbage-${process.pid}`);

const bench = common.createBenchmark(main, {
api: ['classic', 'webstream', 'pull'],
compression: ['gzip', 'deflate', 'brotli', 'zstd'],
filesize: [1024 * 1024, 16 * 1024 * 1024, 64 * 1024 * 1024],
n: [5],
}, {
flags: ['--experimental-stream-iter'],
// Classic and webstream only support gzip (native zlib / CompressionStream).
// Brotli, deflate, zstd are pull-only via stream/iter transforms.
combinationFilter({ api, compression }) {
if (api === 'classic' && compression !== 'gzip') return false;
if (api === 'webstream' && compression !== 'gzip') return false;
return true;
},
});

function main({ api, compression, filesize, n }) {
// Create the fixture file with repeating lowercase ASCII
const chunk = Buffer.alloc(Math.min(filesize, 64 * 1024), 'abcdefghij');
const fd = fs.openSync(filename, 'w');
let remaining = filesize;
while (remaining > 0) {
const toWrite = Math.min(remaining, chunk.length);
fs.writeSync(fd, chunk, 0, toWrite);
remaining -= toWrite;
}
fs.closeSync(fd);

if (api === 'classic') {
benchClassic(n, filesize).then(() => cleanup());
} else if (api === 'webstream') {
benchWebStream(n, filesize).then(() => cleanup());
} else {
benchPull(n, filesize, compression).then(() => cleanup());
}
}

function cleanup() {
try { fs.unlinkSync(filename); } catch { /* ignore */ }
}

// Stateless uppercase transform (shared by all paths)
function uppercaseChunk(chunk) {
const buf = Buffer.allocUnsafe(chunk.length);
for (let i = 0; i < chunk.length; i++) {
const b = chunk[i];
buf[i] = (b >= 0x61 && b <= 0x7a) ? b - 0x20 : b;
}
return buf;
}

// ---------------------------------------------------------------------------
// Classic streams path: createReadStream -> Transform (upper) -> createGzip
// ---------------------------------------------------------------------------
async function benchClassic(n, filesize) {
await runClassic();

bench.start();
let totalBytes = 0;
for (let i = 0; i < n; i++) {
totalBytes += await runClassic();
}
bench.end(totalBytes / (1024 * 1024));
}

function runClassic() {
return new Promise((resolve, reject) => {
const rs = fs.createReadStream(filename);

const upper = new Transform({
transform(chunk, encoding, callback) {
callback(null, uppercaseChunk(chunk));
},
});

const gz = zlib.createGzip();

let totalBytes = 0;
const sink = new Writable({
write(chunk, encoding, callback) {
totalBytes += chunk.length;
callback();
},
});

pipeline(rs, upper, gz, sink, (err) => {
if (err) reject(err);
else resolve(totalBytes);
});
});
}

// ---------------------------------------------------------------------------
// WebStream path: readableWebStream -> TransformStream (upper) -> CompressionStream
// ---------------------------------------------------------------------------
async function benchWebStream(n, filesize) {
await runWebStream();

bench.start();
let totalBytes = 0;
for (let i = 0; i < n; i++) {
totalBytes += await runWebStream();
}
bench.end(totalBytes / (1024 * 1024));
}

async function runWebStream() {
const fh = await fs.promises.open(filename, 'r');
try {
const rs = fh.readableWebStream();

const upper = new TransformStream({
transform(chunk, controller) {
const buf = new Uint8Array(chunk.length);
for (let i = 0; i < chunk.length; i++) {
const b = chunk[i];
buf[i] = (b >= 0x61 && b <= 0x7a) ? b - 0x20 : b;
}
controller.enqueue(buf);
},
});

const compress = new CompressionStream('gzip');
const output = rs.pipeThrough(upper).pipeThrough(compress);
const reader = output.getReader();

let totalBytes = 0;
while (true) {
const { done, value } = await reader.read();
if (done) break;
totalBytes += value.byteLength;
}
return totalBytes;
} finally {
await fh.close();
}
}

// ---------------------------------------------------------------------------
// Pull/iter path: pull() with uppercase transform + selected compression
// ---------------------------------------------------------------------------
async function benchPull(n, filesize, compression) {
const iter = require('zlib/iter');

const compressFactory = {
gzip: iter.compressGzip,
deflate: iter.compressDeflate,
brotli: iter.compressBrotli,
zstd: iter.compressZstd,
}[compression];

// Warm up
await runPull(compressFactory);

bench.start();
let totalBytes = 0;
for (let i = 0; i < n; i++) {
totalBytes += await runPull(compressFactory);
}
bench.end(totalBytes / (1024 * 1024));
}

async function runPull(compressFactory) {
const fh = await fs.promises.open(filename, 'r');
try {
// Stateless transform: uppercase each chunk in the batch
const upper = (chunks) => {
if (chunks === null) return null;
const out = new Array(chunks.length);
for (let j = 0; j < chunks.length; j++) {
out[j] = uppercaseChunk(chunks[j]);
}
return out;
};

const readable = fh.pull(upper, compressFactory());

let totalBytes = 0;
for await (const chunks of readable) {
for (let i = 0; i < chunks.length; i++) {
totalBytes += chunks[i].byteLength;
}
}
return totalBytes;
} finally {
await fh.close();
}
}
Loading
Loading