Skip to content

perf: use a Set for visited dirs, only when resolving real paths - #175

Open
Paul-Browne wants to merge 1 commit into
thecodrr:masterfrom
Paul-Browne:perf/visited-set
Open

Paul-Browne wants to merge 1 commit into
thecodrr:masterfrom
Paul-Browne:perf/visited-set

Conversation

@Paul-Browne

Copy link
Copy Markdown

state.visited is an array that every crawled directory is pushed into, and isRecursiveUsingRealPaths scans it with Array#includes on every directory symlink. That makes withSymlinks() crawls (default resolvePaths: true) O(directories × directory symlinks).

The array is also filled on every crawl, even though it is only read when useRealPaths is set.

Change

  • visited is now a Set<string>, so the recursion check is O(1).
  • It is only allocated when options.useRealPaths is set. Otherwise it is undefined, so default crawls no longer keep a second copy of every directory path.
  • WalkerState.visited changes from string[] to Set<string> | undefined. This type is exported, but it's internal walker state.

Benchmarks

Node 22, Linux, warm page cache. Trees have N directories with one file each, plus N/2 symlinks to those directories. Crawled with new fdir().withFullPaths().withSymlinks().crawl(root).sync(), median of 5 runs.

N dirs before after
2,000 42 ms 32 ms
10,000 570 ms 147 ms
30,000 3,632 ms 425 ms

Default crawls without symlink resolution (200k files / 10k dirs) no measurable change.

@Paul-Browne

Copy link
Copy Markdown
Author

Related issues (none are fixed by this PR, since output is unchanged):

  • withSymlinks() spends most of its time in fs.realpath: would you take a faster resolver, opt-in or in the next major? #174: complementary. That issue speeds up the fs.realpath calls in withSymlinks(); this PR removes the O(directories × dir symlinks) visited.includes() scan that runs after them. On small trees like the ones in #174 the scan is minor, but it grows quadratically (8.6× faster at 30k dirs, see the benchmarks above). Once realpath gets cheaper, it'll also be a bigger share of what's left.
  • symlinks can cause duplicate entries #138: groundwork. The fix proposed there checks visited before every directory traversal. With an array, that would make every symlink crawl quadratic; with a Set, it's O(1). The Set is allocated in exactly the configuration #138 hits (withSymlinks() defaults to real paths). Happy to follow up with a fix for #138 as a separate PR, since it changes output.

Heads up: this conflicts with #170 in __tests__/symlinks.test.ts. Both PRs add a fixture in the same place. The source changes merge cleanly, so whichever lands second just needs a quick rebase.

This branch has not been deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant