Skip to content

Instantly share code, notes, and snippets.

@MarshallOfSound
Created June 12, 2026 22:39
Show Gist options
  • Select an option

  • Save MarshallOfSound/7d22707e3e550a782e96f9da0081e9f1 to your computer and use it in GitHub Desktop.

Select an option

Save MarshallOfSound/7d22707e3e550a782e96f9da0081e9f1 to your computer and use it in GitHub Desktop.
Core fs micro-benchmarks for the nodejs/node FSReqPromise lazy stat arrays change

Core fs micro-benchmarks for nodejs/node FSReqPromise lazy stat arrays

Scripts used to benchmark https://github.com/nodejs/node/compare/main...MarshallOfSound:node:perf/fsreq-promise-lazy-stats

Usage

# all scenarios on one binary (one JSON line each: ns/op + ops/sec)
node fsbench.js

# filter scenarios by substring
node fsbench.js "fsp.stat"

# interleaved A/B between two binaries, medians of N full-suite reps
python3 fscompare.py /path/to/node-old /path/to/node-new 5

Covers sync / callback / promises variants of readFile, writeFile, readdir, stat, open+close, copyFile, appendFile, access and realpath across payload sizes (1KB/64KB/4MB) and directory sizes (16/1k/8k), plus batched-concurrency variants (4/16/64 in-flight) for the async ops. Each scenario self-calibrates to ~250ms. Runs on tmpdir (tmpfs on most Linux setups), so numbers measure dispatch/allocation overhead rather than disk latency.

'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); });
#!/usr/bin/env python3
"""Interleaved A/B for fsbench.js. Usage: fscompare.py <nodeA> <nodeB> [reps] [filter]"""
import json, subprocess, sys, statistics, os
A, B = sys.argv[1], sys.argv[2]
REPS = int(sys.argv[3]) if len(sys.argv) > 3 else 5
FILT = sys.argv[4] if len(sys.argv) > 4 else ''
HERE = os.path.dirname(os.path.abspath(__file__))
def run(node):
out = subprocess.run([node, os.path.join(HERE, 'fsbench.js'), FILT],
capture_output=True, text=True, check=True)
return {r['name']: r['ops_per_sec'] for r in map(json.loads, out.stdout.splitlines())}
ra, rb = [], []
for i in range(REPS):
ra.append(run(A)); rb.append(run(B))
print(f'rep {i+1}/{REPS} done', file=sys.stderr)
names = ra[0].keys()
print(f"{'scenario':42s} {'A ops/s':>12s} {'B ops/s':>12s} {'delta':>8s}")
for n in names:
ma = statistics.median(r[n] for r in ra)
mb = statistics.median(r[n] for r in rb)
print(f"{n:42s} {ma:12,.0f} {mb:12,.0f} {(mb-ma)/ma*100:+7.1f}%")
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment