Skip to content

perf: raise MaxDegreeOfParallelism in ResolveDirectoryTree - #3836

Open
Mpdreamz wants to merge 2 commits into
mainfrom
perf/resolve-tree-parallelism
Open

perf: raise MaxDegreeOfParallelism in ResolveDirectoryTree#3836
Mpdreamz wants to merge 2 commits into
mainfrom
perf/resolve-tree-parallelism

Conversation

@Mpdreamz

Copy link
Copy Markdown
Member

Summary

  • MinimalParseAsync is ~60% blocked in the open() syscall (~755µs/file across 435k files). With the default ProcessorCount cap the IO queue stays shallow — Thread.StartCallback showing 805s own time in dotTrace confirms pool workers are parked waiting for IO to complete.
  • MarkdownFiles is a FrozenSet<MarkdownFile> which is not IList<T>, so Parallel.ForEachAsync falls back to the locked-enumerator partitioner rather than range partitioning — extra lock contention at high DOP.

Two fixes:

  1. Materialise MarkdownFiles into an array before the loop so the partitioner gets an indexable source.
  2. Set MaxDegreeOfParallelism = max(ProcessorCount × 4, 32). The 4× multiplier keeps enough IO in flight to cover the latency; the 32 floor avoids stalling on machines with very few cores.

The multiplier should be re-validated with docs-migrate bench on the target machine. Each in-flight parse holds a MarkdownDocument in memory, so there is a real wall-time vs RSS trade-off.

Test plan

  • dotnet test tests/Elastic.Markdown.Tests/ — 1951 tests pass
  • dotnet run --project src/tooling/docs-migrate -- bench — record ResolveDirectoryTree time before and after

🤖 Generated with Claude Code

MinimalParseAsync is ~60% blocked in the open() syscall (~755µs/file
across 435k files). With the default ProcessorCount cap the IO queue
stays shallow; Thread.StartCallback showing 805s own time in dotTrace
confirms pool workers are parked waiting for IO to complete.

Two fixes in one commit:
1. Materialise MarkdownFiles (FrozenSet<T>) into an array before the
   parallel loop. FrozenSet is not IList<T>, so Parallel.ForEachAsync
   falls back to the locked-enumerator partitioner rather than range
   partitioning — extra lock contention at high DOP.
2. Set MaxDegreeOfParallelism = max(ProcessorCount * 4, 32). The 4x
   multiplier keeps enough IO in flight to cover the latency; capped
   at 32 to avoid runaway RSS (each in-flight parse holds a
   MarkdownDocument tree in memory).

The right multiplier should be re-validated with bench on the target
machine once the other perf PRs land.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Parallel.ForEachAsync (async) does not use Partitioner.Create and has
no IList<T> fast path — it locks a shared enumerator regardless of
whether the source is a FrozenSet or a flat array.  The array copy
allocated 435k references into a second data structure for no gain.

The actual fix is solely the MaxDegreeOfParallelism change.

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.

1 participant