fs: stop stat()ing every entry in recursive readdir - #65487
Open
codebytere wants to merge 1 commit into
Open
Conversation
Collaborator
|
Review requested:
|
codebytere
force-pushed
the
perf/fs-readdir-recursive-dirent-types
branch
from
August 22, 2026 15:42
0b1854c to
441770b
Compare
anonrig
approved these changes
Aug 22, 2026
readdir({ recursive: true }) asked the binding for names only and then
called internalModuleStat() on every entry to find the directories to
descend into; with withFileTypes it built the Dirents and still stat()ed
every entry that was not already a directory. Both variants also ran
path.join() and path.relative() per entry to build the relative result.
Ask the binding for file types in all cases, descend into directories
directly, and only stat() symbolic links and entries of unknown type
(which is what could point to a directory). The relative name is the
parent's prefix plus the entry name. Results, their order and the
symlink-following behavior are unchanged for fs.readdirSync, fs.readdir
and fs.promises.readdir.
The known_issues test for Buffer paths (nodejs#58892) called back without
checking the error; the error now reaches the callback instead of being
thrown from the completion handler, so the test asserts success to keep
expressing the issue.
Signed-off-by: Shelley Vohr <shelley.vohr@gmail.com>
codebytere
force-pushed
the
perf/fs-readdir-recursive-dirent-types
branch
from
August 22, 2026 17:21
441770b to
5858205
Compare
anonrig
approved these changes
Aug 22, 2026
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #65487 +/- ##
========================================
Coverage 90.12% 90.13%
========================================
Files 752 751 -1
Lines 252315 252458 +143
Branches 47444 47509 +65
========================================
+ Hits 227395 227545 +150
- Misses 16217 16220 +3
+ Partials 8703 8693 -10
🚀 New features to boost your workflow:
|
Collaborator
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Makes
fs.readdir(),fs.readdirSync()andfsPromises.readdir()with{ recursive: true }use the entry types the binding already returns instead of astat()per entry, and build relative names by prefix instead ofpath.join()+path.relative()per entry. Results, their order and the symlink-following behavior are unchanged.benchmark/fs/bench-readdir.js/bench-readdirSync.js(gain arecursiveoption),recursive=true, 30 runs:lib/(473 entries)test/parallel/(~4 000 entries)readdirSyncreaddirSync,withFileTypesreaddirreaddir,withFileTypesThe recursive variants asked the binding for names only and then called
internalModuleStat()on every entry to find the directories to descend into; withwithFileTypesthey built theDirents and still stat()ed every entry that was not already a directory (to follow symbolic links, #52663). On a 13 500-entry tree that is 13 500 extrastat()calls, and the sync variant was ~4.5x slower than a hand-writtenreaddirSync(dir, { withFileTypes: true })walk.Now the binding is always asked for types, directories are descended into directly, and only symbolic links and entries of unknown type (the ones that could still lead to a directory) are stat()ed. Output was compared entry-for-entry against
mainontest/and on a tree with file/directory/broken symlinks and a FIFO. The known_issues test for Buffer paths (#58892) now asserts success: theERR_INVALID_ARG_TYPEreaches the callback instead of being thrown from the completion handler, so with a baremustCall()it would have started passing.Tests:
test-fs-readdir-recursive*,test-fs-readdir-types*,test-fs-opendirandfs.cptests pass.Disclosure: the code, measurements and this description were written by Claude Code, directed and reviewed by @codebytere.