Skip to content

fs: add native fast paths for recursive readdir and buffer I/O#64396

Open
anonrig wants to merge 4 commits into
nodejs:mainfrom
anonrig:fs-huge-perf-win
Open

fs: add native fast paths for recursive readdir and buffer I/O#64396
anonrig wants to merge 4 commits into
nodejs:mainfrom
anonrig:fs-huge-perf-win

Conversation

@anonrig

@anonrig anonrig commented Jul 9, 2026

Copy link
Copy Markdown
Member

Summary

Adds C++ fast paths for common fs sync operations, following the same pattern as the existing readFileUtf8 / writeFileUtf8 bindings:

  1. fs.readdirSync(path, { recursive: true }) — full directory walk in C++ (readdirRecursiveSync). Uses scandir dirent types so most entries skip the extra internalModuleStat the JS walk does per entry. Symlinks / UV_DIRENT_UNKNOWN still stat/lstat as needed for correct descent and Dirent types.
  2. fs.readFileSync(path) / encoding: null — single native readFileBuffer (open / fstat / read / close). Throws ERR_FS_FILE_TOO_LARGE when size exceeds 2 GiB.
  3. fs.writeFileSync(path, buffer) — single native writeFileBuffer for ArrayBufferView data when flush is not set.

Public API and results (sorted) stay the same; sequential/parallel readdir recursive and read/write file tests pass.

Benchmarks (arm64 macOS, Release)

Tree: ~2110 entries. Short runs (n=25 for readdir).

Path ops/s vs JS walk
C++ readdirRecursiveSync 354 1.77×
JS readdir + internalModuleStat 199
fs.readdirSync({ recursive: true }) 435 ~2.2×
same + withFileTypes 364 ~1.8×

Buffer I/O (same binary, short n):

Case C++ vs multi-call JS
readFileSync small ~1.04×
readFileSync 64 KiB ~1.08×
writeFileSync 1 KiB buffer ~1.04×

Larger trees show recursive readdir closer to ~3× (stats dominate).

Test plan

  • test-fs-readdir-recursive (parallel + sequential)
  • test-fs-readdir-types / test-fs-readdir-types-symlinks
  • test-fs-readfile (+ empty, fd, flags, utf8 fast path)
  • test-fs-write-file* (sync, buffer, typedarrays, flush)
  • CI on this PR

@nodejs-github-bot nodejs-github-bot added c++ Issues and PRs that require attention from people who are familiar with C++. fs Issues and PRs related to the fs subsystem / file system. needs-ci PRs that need a full CI run. labels Jul 9, 2026
@anonrig anonrig requested review from mcollina and ronag July 9, 2026 18:30
Move readdirSync({ recursive: true }) into a C++ walk that uses
scandir dirent types so most entries avoid an extra stat(). Add
readFileBuffer and writeFileBuffer bindings so Buffer readFileSync
and writeFileSync avoid open/stat/read/write/close round-trips in JS,
matching the existing utf8 fast paths. Throw ERR_FS_FILE_TOO_LARGE
from C++ when the file exceeds 2 GiB.

On a ~2k-entry tree, recursive readdir is about 2x faster than the
JS walk; buffer read/write see smaller single-digit gains.

Signed-off-by: Yagiz Nizipli <yagiz@nizipli.com>
@anonrig anonrig force-pushed the fs-huge-perf-win branch from 0aef62a to e76ef36 Compare July 9, 2026 18:32
@mcollina

mcollina commented Jul 9, 2026

Copy link
Copy Markdown
Member

format cpp ;)

anonrig added 2 commits July 9, 2026 14:32
Always call the native readdirRecursiveSync binding and remove the
unused JS fallback. Document readFileBuffer, writeFileBuffer, and
readdirRecursiveSync on the fs internal binding typings.

Signed-off-by: Yagiz Nizipli <yagiz@nizipli.com>
Replace the extra S_IS* macros with a switch on S_IFMT, avoid a
temporary string when encoding relative paths, and tighten the
directory-descent type checks.

Signed-off-by: Yagiz Nizipli <yagiz@nizipli.com>
@anonrig anonrig force-pushed the fs-huge-perf-win branch from 3073a3b to f0e1dc9 Compare July 9, 2026 18:51
Use primordials Array instead of the global, return undefined
explicitly for the JSDoc check, and match clang-format on the
directory descent condition.

Signed-off-by: Yagiz Nizipli <yagiz@nizipli.com>
@codecov

codecov Bot commented Jul 9, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 68.54305% with 95 lines in your changes missing coverage. Please review.
✅ Project coverage is 90.19%. Comparing base (8fec65d) to head (5a4bf75).

Files with missing lines Patch % Lines
src/node_file.cc 63.17% 34 Missing and 61 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #64396      +/-   ##
==========================================
- Coverage   90.25%   90.19%   -0.06%     
==========================================
  Files         741      741              
  Lines      241207   241475     +268     
  Branches    45430    45514      +84     
==========================================
+ Hits       217698   217809     +111     
- Misses      15084    15167      +83     
- Partials     8425     8499      +74     
Files with missing lines Coverage Δ
lib/fs.js 98.29% <100.00%> (-0.08%) ⬇️
src/node_errors.h 86.95% <ø> (ø)
src/node_file.cc 73.19% <63.17%> (-1.04%) ⬇️

... and 32 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

c++ Issues and PRs that require attention from people who are familiar with C++. fs Issues and PRs related to the fs subsystem / file system. needs-ci PRs that need a full CI run.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants