Skip to content

perf: reduce metadata syscalls in ScanDocumentationFiles - #3840

Open
Mpdreamz wants to merge 1 commit into
mainfrom
perf/cheaper-file-enumeration
Open

perf: reduce metadata syscalls in ScanDocumentationFiles#3840
Mpdreamz wants to merge 1 commit into
mainfrom
perf/cheaper-file-enumeration

Conversation

@Mpdreamz

Copy link
Copy Markdown
Member

Summary

ScanDocumentationFiles performed up to 7 metadata operations per file before producing a DocumentationFile:

Operation Count/file Cost
f.Attributes ×2 1 stat (cached after first access)
f.Directory! ×2 2 new IDirectoryInfo wrappers + 2 stats (same dir re-stated per contained file)
f.LinkTarget ×1 may be separate stat
Path.GetRelativePath ×2 pure string but redundant

Three changes:

  1. Relative path computed once from the raw string returned by EnumerateFiles — before IFileInfo.New(). This also lets the dot-prefix hidden-folder check (StartsWith('.')) run with zero metadata syscalls; path-filtered files never get a wrapper allocated.
  2. Single Attributes read per file consolidates the two separate .Where() chains that each read it.
  3. Per-directory attribute cache — a Dictionary<string, FileAttributes> keyed by parent path avoids allocating a fresh IDirectoryInfo and re-stating the same directory for every file it contains. Across 435k files in a large but finite directory tree, most directories are shared by many files.

Test plan

  • dotnet test tests/Elastic.Markdown.Tests/ --filter NavigationTests|DocSet — 199 tests pass
  • dotnet test tests/Elastic.Markdown.Tests/ — full 1951-test suite passes
  • dotnet run --project src/tooling/docs-migrate -- bench — record DocumentationSet ctor time

🤖 Generated with Claude Code

The original implementation performed up to 7 metadata operations per
file before producing a DocumentationFile:

  f.Attributes      × 2  (hidden + system check on the file)
  f.Directory!      × 2  (each call allocates a new IDirectoryInfo
                          wrapper and triggers its own stat)
  f.LinkTarget      × 1  (symlink check)
  GetRelativePath   × 2  (once in the filter, once in the final select)

Three changes:

1. Compute relative path once from the raw string returned by
   EnumerateFiles — before IFileInfo allocation. This also moves the
   dot-prefix hidden-folder check ahead of IFileInfo.New() so that
   path-filtered files never get a wrapper allocated at all.

2. Consolidate the file-level Where into a single Attributes read per
   file (the original read it twice via separate .Where() chains).

3. Cache directory-attribute lookups in a per-call dictionary keyed by
   directory path. Directories are shared by many files; without the
   cache each contained file allocated a fresh IDirectoryInfo and
   triggered a redundant stat on the same directory.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants