|
'use strict'; |
|
// Unified core-fs micro-benchmark: sync / callback / promises variants of |
|
// readFile, writeFile, readdir, stat, open+close, copyFile, appendFile, |
|
// access, realpath — across payload and directory sizes. |
|
// |
|
// Usage: node fsbench.js [filter-substring] |
|
// Output: one JSON line per scenario: { name, n, ms, ns_per_op, ops_per_sec } |
|
|
|
const fs = require('fs'); |
|
const fsp = require('fs/promises'); |
|
const path = require('path'); |
|
const os = require('os'); |
|
|
|
const ROOT = fs.mkdtempSync(path.join(os.tmpdir(), 'fsbench-')); |
|
const FILTER = process.argv[2] || ''; |
|
|
|
// ---------- fixtures ---------- |
|
const SIZES = { '1kb': 1024, '64kb': 64 * 1024, '4mb': 4 * 1024 * 1024 }; |
|
const payloads = {}; |
|
for (const [label, bytes] of Object.entries(SIZES)) { |
|
payloads[label] = Buffer.alloc(bytes, 'a'); |
|
fs.writeFileSync(path.join(ROOT, `read-${label}.bin`), payloads[label]); |
|
} |
|
const DIR_SIZES = { 16: 16, '1k': 1024, '8k': 8192 }; |
|
for (const [label, count] of Object.entries(DIR_SIZES)) { |
|
const d = path.join(ROOT, `dir-${label}`); |
|
fs.mkdirSync(d); |
|
for (let i = 0; i < count; i++) fs.writeFileSync(path.join(d, `f${i}`), ''); |
|
} |
|
const statTarget = path.join(ROOT, 'read-1kb.bin'); |
|
const deepLink = path.join(ROOT, 'deep', 'a', 'b', 'c'); |
|
fs.mkdirSync(deepLink, { recursive: true }); |
|
const realTarget = path.join(deepLink, 'target.txt'); |
|
fs.writeFileSync(realTarget, 'x'); |
|
|
|
// ---------- harness ---------- |
|
const TARGET_MS = 250; |
|
async function bench(name, op, { calibrateN = 64, units = 1 } = {}) { |
|
if (FILTER && !name.includes(FILTER)) return; |
|
// warmup + calibrate so each scenario runs ~TARGET_MS |
|
let n = calibrateN; |
|
for (;;) { |
|
const t0 = process.hrtime.bigint(); |
|
for (let i = 0; i < n; i++) await op(i); |
|
const ms = Number(process.hrtime.bigint() - t0) / 1e6; |
|
if (ms >= TARGET_MS / 4) { |
|
n = Math.max(8, Math.round((n * TARGET_MS) / ms)); |
|
break; |
|
} |
|
n *= 4; |
|
} |
|
// measured run |
|
const t0 = process.hrtime.bigint(); |
|
for (let i = 0; i < n; i++) await op(i); |
|
const ms = Number(process.hrtime.bigint() - t0) / 1e6; |
|
console.log(JSON.stringify({ |
|
name, n: n * units, |
|
ms: +ms.toFixed(1), |
|
ns_per_op: +((ms * 1e6) / (n * units)).toFixed(0), |
|
ops_per_sec: +(((n * units) / ms) * 1000).toFixed(0), |
|
})); |
|
} |
|
|
|
const cb = (fn, ...args) => new Promise((res, rej) => |
|
fn(...args, (err, v) => err ? rej(err) : res(v))); |
|
|
|
// ---------- scenarios ---------- |
|
(async () => { |
|
// readFile |
|
for (const label of Object.keys(SIZES)) { |
|
const f = path.join(ROOT, `read-${label}.bin`); |
|
await bench(`readFileSync ${label}`, () => fs.readFileSync(f)); |
|
await bench(`readFile(cb) ${label}`, () => cb(fs.readFile, f)); |
|
await bench(`fsp.readFile ${label}`, () => fsp.readFile(f)); |
|
} |
|
await bench('readFileSync 1kb utf8', () => |
|
fs.readFileSync(path.join(ROOT, 'read-1kb.bin'), 'utf8')); |
|
|
|
// writeFile (same path overwrite; payload from memory) |
|
for (const label of Object.keys(SIZES)) { |
|
const f = path.join(ROOT, `write-${label}.bin`); |
|
const data = payloads[label]; |
|
await bench(`writeFileSync ${label}`, () => fs.writeFileSync(f, data)); |
|
await bench(`writeFile(cb) ${label}`, () => cb(fs.writeFile, f, data)); |
|
await bench(`fsp.writeFile ${label}`, () => fsp.writeFile(f, data)); |
|
} |
|
|
|
// readdir |
|
for (const label of Object.keys(DIR_SIZES)) { |
|
const d = path.join(ROOT, `dir-${label}`); |
|
await bench(`readdirSync ${label}`, () => fs.readdirSync(d)); |
|
await bench(`readdirSync ${label} withFileTypes`, () => |
|
fs.readdirSync(d, { withFileTypes: true })); |
|
await bench(`readdir(cb) ${label}`, () => cb(fs.readdir, d)); |
|
await bench(`fsp.readdir ${label}`, () => fsp.readdir(d)); |
|
} |
|
|
|
// stat family |
|
await bench('statSync', () => fs.statSync(statTarget)); |
|
await bench('statSync bigint', () => fs.statSync(statTarget, { bigint: true })); |
|
await bench('stat(cb)', () => cb(fs.stat, statTarget)); |
|
await bench('fsp.stat', () => fsp.stat(statTarget)); |
|
await bench('lstatSync', () => fs.lstatSync(statTarget)); |
|
await bench('existsSync', () => fs.existsSync(statTarget)); |
|
await bench('existsSync missing', () => fs.existsSync(statTarget + '.nope')); |
|
await bench('accessSync', () => fs.accessSync(statTarget)); |
|
|
|
// open/close |
|
await bench('openSync+closeSync', () => fs.closeSync(fs.openSync(statTarget, 'r'))); |
|
await bench('fsp.open+close', async () => { const h = await fsp.open(statTarget, 'r'); await h.close(); }); |
|
|
|
// copyFile / appendFile |
|
const copySrc = path.join(ROOT, 'read-64kb.bin'); |
|
const copyDst = path.join(ROOT, 'copy-dst.bin'); |
|
await bench('copyFileSync 64kb', () => fs.copyFileSync(copySrc, copyDst)); |
|
await bench('fsp.copyFile 64kb', () => fsp.copyFile(copySrc, copyDst)); |
|
const appendF = path.join(ROOT, 'append.log'); |
|
const line = Buffer.from('x'.repeat(1023) + '\n'); |
|
let appendCount = 0; |
|
await bench('appendFileSync 1kb', () => { |
|
// reset periodically so the file doesn't grow unbounded |
|
if (++appendCount % 4096 === 0) fs.writeFileSync(appendF, ''); |
|
fs.appendFileSync(appendF, line); |
|
}); |
|
|
|
// concurrency variants: C requests in flight per await (threadpool is |
|
// UV_THREADPOOL_SIZE=4 by default, so c16/c64 probe queueing behavior) |
|
for (const C of [4, 16, 64]) { |
|
const batch = (op) => () => { |
|
const ps = new Array(C); |
|
for (let i = 0; i < C; i++) ps[i] = op(); |
|
return Promise.all(ps); |
|
}; |
|
const f1 = path.join(ROOT, 'read-1kb.bin'); |
|
const f64 = path.join(ROOT, 'read-64kb.bin'); |
|
const wf = path.join(ROOT, `write-conc.bin`); |
|
const d1k = path.join(ROOT, 'dir-1k'); |
|
await bench(`fsp.readFile 1kb c${C}`, batch(() => fsp.readFile(f1)), { units: C }); |
|
await bench(`readFile(cb) 1kb c${C}`, batch(() => cb(fs.readFile, f1)), { units: C }); |
|
await bench(`fsp.readFile 64kb c${C}`, batch(() => fsp.readFile(f64)), { units: C }); |
|
await bench(`fsp.writeFile 1kb c${C}`, batch(() => fsp.writeFile(wf, payloads['1kb'])), { units: C }); |
|
await bench(`fsp.readdir 1k c${C}`, batch(() => fsp.readdir(d1k)), { units: C }); |
|
await bench(`fsp.stat c${C}`, batch(() => fsp.stat(statTarget)), { units: C }); |
|
} |
|
|
|
// realpath |
|
await bench('realpathSync deep', () => fs.realpathSync(realTarget)); |
|
await bench('realpathSync.native deep', () => fs.realpathSync.native(realTarget)); |
|
|
|
fs.rmSync(ROOT, { recursive: true, force: true }); |
|
})().catch((e) => { console.error(e); process.exit(1); }); |