Improve GetChildren / GetChildKeys performance for large configurations#130306
Draft
rosebyte wants to merge 1 commit into
Draft
Improve GetChildren / GetChildKeys performance for large configurations#130306rosebyte wants to merge 1 commit into
GetChildren / GetChildKeys performance for large configurations#130306rosebyte wants to merge 1 commit into
Conversation
GetChildren / GetChildKeys performance for large configurations
Contributor
There was a problem hiding this comment.
Pull request overview
This PR refactors how configuration child keys are aggregated across providers, aiming to reduce allocations and improve correctness/consistency by de-duplicating keys during aggregation (rather than at the end), while keeping ordering via ConfigurationKeyComparer. It also adds targeted tests around de-duplication, large key sets, deep keys, and provider key-filtering behavior.
Changes:
- Introduces
SortedChildKeysas an internal accumulator to de-duplicate and lazily sort child keys. - Updates
GetChildrenImplementation(and chained-provider child key retrieval) to use the new accumulator-based aggregation. - Adds tests validating de-duplication and various edge cases for
GetChildren()/GetChildKeys()behavior.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| src/libraries/Microsoft.Extensions.Configuration/tests/ConfigurationTest.cs | Adds test coverage for de-duplication and edge cases around child key aggregation. |
| src/libraries/Microsoft.Extensions.Configuration/src/SortedChildKeys.cs | New internal accumulator type for de-duplicating + lazily sorting child keys. |
| src/libraries/Microsoft.Extensions.Configuration/src/InternalConfigurationRootExtensions.cs | Reworks provider aggregation and section projection to use SortedChildKeys. |
| src/libraries/Microsoft.Extensions.Configuration/src/ConfigurationProvider.cs | Updates base provider to add keys directly into SortedChildKeys when used as the seed. |
| src/libraries/Microsoft.Extensions.Configuration/src/ConfigurationKeyComparer.cs | Avoids unnecessary parsing work / allocations by gating TryParse attempts. |
| src/libraries/Microsoft.Extensions.Configuration/src/ChainedConfigurationProvider.cs | Uses the accumulator-based path to avoid per-child section allocations when chaining. |
Comment on lines
+107
to
114
| private static void ProcessProvider(IConfigurationProvider provider, SortedChildKeys accumulator, string? path) | ||
| { | ||
| IEnumerable<string> returned = provider.GetChildKeys(accumulator, path); | ||
| if (!ReferenceEquals(returned, accumulator)) | ||
| { | ||
| accumulator.Overwrite(returned); | ||
| } | ||
| } |
Comment on lines
+150
to
+158
| public ChildKeysBag(SortedChildKeys accumulator) | ||
| { | ||
| _accumulator = accumulator; | ||
| } | ||
|
|
||
| public ChildKeysBag(List<string> fallback) | ||
| { | ||
| _fallback = fallback; | ||
| } |
Comment on lines
+95
to
+99
| SortedChildKeys accumulator = earlierKeys is SortedChildKeys existing ? existing : new(earlierKeys); | ||
| if (_config is IConfigurationRoot root) | ||
| { | ||
| return root.GetChildKeysImplementation(parentPath, accumulator); | ||
| } |
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.
Fixes #65885.
Measured with a small custom harness (minimum of 15 timed runs;
GC.GetAllocatedBytesForCurrentThreadfor allocation) on .NET 11 preview, Apple arm64. Absolute numbers are machine-specific and the modern binder is far faster than the .NET 6 the issue was filed against, so the ratios are the point. A BenchmarkDotNet re-run on the CI baseline is advisable before merge.The issue's repro shape (single JSON-equivalent provider, 6003 keys, binding a small object from
MySection:Example):GetChildren("MySection:Example")(2 children)GetChildren("OtherSection")(2 children, 6000 descendants)ConfigurationBinder.Get<ConfiguredObject>(end to end)The end-to-end bind is "only" 1.5x because on .NET 11 the binder's own reflection cost now dominates the greatly reduced aggregation; the part this PR changes is 3.8x-13.6x faster and allocates up to 99.7% less.
Provider-count scaling (one
GetChildren, 100 distinct keys carried byPoverlapping providers):main grows as
O(P^2)(about 4x per doubling); this PR grows asO(P)(about 1.9x per doubling): 347x at 64 providers.Small in-memory configurations (typical case, time and bytes per
GetChildren):What this changes, and what it does not
O(P^2)->O(P)), the LINQ and per-provider allocations, and the materialise-then-de-duplicate behaviour. This is the cost the issue reports.GetChildrenstill scans the whole provider'sData(O(K)per call); it is now much leaner, but not sub-linear. Making it sub-linear needs a per-provider sorted key index (binary-search theparentPath:range) so a call becomesO(log K + children). That was prototyped and deliberately left out: it is a parallel structure with invalidation and retained memory whose build cost only amortises after roughlylog Kcalls, so it is a poor trade for the common "read each section once or twice" usage and was not needed to resolve the reported cost. It remains an option for a follow-up if a workload does thousands ofGetChildrencalls against one huge provider.Compatibility and risk
IConfigurationProviderimplementations that overrideGetChildKeysare unaffected: a foreign returned sequence is absorbed by the accumulator.