Skip to content

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

Open
anonrig wants to merge 7 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 7 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
@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
anonrig force-pushed the fs-huge-perf-win branch from 3073a3b to f0e1dc9 Compare July 9, 2026 18:51
@codecov

codecov Bot commented Jul 9, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 69.32907% with 96 lines in your changes missing coverage. Please review.
✅ Project coverage is 90.08%. Comparing base (ad7a5b8) to head (f1bd58e).
⚠️ Report is 11 commits behind head on main.

Files with missing lines Patch % Lines
src/node_file.cc 63.07% 35 Missing and 61 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #64396      +/-   ##
==========================================
- Coverage   90.11%   90.08%   -0.04%     
==========================================
  Files         752      752              
  Lines      251569   251845     +276     
  Branches    47268    47349      +81     
==========================================
+ Hits       226701   226863     +162     
- Misses      16233    16265      +32     
- Partials     8635     8717      +82     
Files with missing lines Coverage Δ
lib/fs.js 98.29% <100.00%> (-0.07%) ⬇️
src/node_errors.h 86.95% <ø> (ø)
src/node_file.cc 73.28% <63.07%> (-0.95%) ⬇️

... and 29 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.

Comment thread lib/fs.js Outdated
return binding.readFileUtf8(path, stringToFlags(options.flag));
const flags = stringToFlags(options.flag);
// C++ fast paths: single native call instead of open/stat/read/close.
if (options.encoding === 'utf8' || options.encoding === 'utf-8') {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could these not also be uppercase? #64341 adds a new isUtf8Encoding to avoid missing valid encoding strings. Maybe a new lint rule would help avoid missing these.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exactly! My PR #64341 introduces isUtf8Encoding() specifically to catch all valid case-insensitive UTF-8 variations (like UTF-8, utf-8, etc.) without needing repetitive if/else checks. Once #64341 lands, we can easily reuse that helper right here!

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. Added isUtf8Encoding() that uses normalizeEncoding(), so the C++ fast paths also accept UTF8, UTF-8, and mixed-case aliases (not only lowercase utf8/utf-8).

Comment thread src/node_file.cc Outdated
if (offset < static_cast<size_t>(size)) {
// Truncate logical length without reallocating when the file shrank.
args.GetReturnValue().Set(
Buffer::Copy(isolate, data, offset).ToLocalChecked());

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's not use ToLocalChecked().

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done — ReadFileBuffer now uses ToLocal() and returns on allocation failure instead of ToLocalChecked().

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>
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>
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>
The C++ readdirRecursiveSync fast path handles Buffer paths, so
test-fs-readdir-sync-recursive-with-buffer now passes. Known-issues
tests are expected to fail; a passing negative test fails CI.

Also avoid ToLocalChecked() in ReadFileBuffer and drop the always-true
st_size >= 0 comparison.

Refs: nodejs#58892
The node-core eslint rules require test files to load common first.
Use normalizeEncoding so readFileSync/writeFileSync take the C++
fast path for UTF8, UTF-8, and mixed-case variants, not only
lowercase utf8/utf-8.

Refs: nodejs#64341

@mcollina mcollina left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm

@mcollina mcollina added the request-ci Add this label to start a Jenkins CI on a PR. label Aug 16, 2026
@github-actions github-actions Bot removed the request-ci Add this label to start a Jenkins CI on a PR. label Aug 16, 2026
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

@jasnell

jasnell commented Aug 17, 2026

Copy link
Copy Markdown
Member

Large number of directly relevant failures on Windows.

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.

6 participants