Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Optimize the performance of InternalConfigurationRootExtensions.GetChildrenImplementation #74736

Closed
wants to merge 24 commits into from
Closed
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -63,12 +63,24 @@ public virtual IEnumerable<string> GetChildKeys(
string? parentPath)
{
var results = new List<string>();
results.AddRange(GetChildKeysInternal(parentPath));
results.AddRange(earlierKeys);
results.Sort(ConfigurationKeyComparer.Comparison);
return results;
}

/// <summary>
/// Returns the list of keys that this provider has.
/// </summary>
/// <param name="parentPath">The path for the parent IConfiguration.</param>
/// <returns>The list of keys for this provider.</returns>
internal IEnumerable<string> GetChildKeysInternal(string? parentPath)
{
if (parentPath is null)
{
foreach (KeyValuePair<string, string?> kv in Data)
{
results.Add(Segment(kv.Key, 0));
yield return Segment(kv.Key, 0);
wangyuantao marked this conversation as resolved.
Show resolved Hide resolved
}
}
else
Expand All @@ -81,16 +93,10 @@ public virtual IEnumerable<string> GetChildKeys(
kv.Key.StartsWith(parentPath, StringComparison.OrdinalIgnoreCase) &&
kv.Key[parentPath.Length] == ':')
{
results.Add(Segment(kv.Key, parentPath.Length + 1));
yield return Segment(kv.Key, parentPath.Length + 1);
}
}
}

results.AddRange(earlierKeys);

results.Sort(ConfigurationKeyComparer.Comparison);
wangyuantao marked this conversation as resolved.
Show resolved Hide resolved

return results;
}

private static string Segment(string key, int prefixLength)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,48 @@ internal static IEnumerable<IConfigurationSection> GetChildrenImplementation(thi
{
using ReferenceCountedProviders? reference = (root as ConfigurationManager)?.GetProvidersReference();
IEnumerable<IConfigurationProvider> providers = reference?.Providers ?? root.Providers;
var optimizedProviders = new List<ConfigurationProvider>();
bool allProvidersAreOptimized = true;
foreach (IConfigurationProvider provider in providers)
{
// If a IConfigurationProvider inherit from ConfigurationProvider, but override the implementation GetChildKeys
// To make sure we get the expected keys, we should honor the override and go the legacy path.
Type type = provider.GetType();
if (provider is not ConfigurationProvider optimizedProvider
|| type.GetMethod(nameof(provider.GetChildKeys))?.DeclaringType == type)
{
allProvidersAreOptimized = false;
wangyuantao marked this conversation as resolved.
Show resolved Hide resolved
break;
}

IEnumerable<IConfigurationSection> children = providers
.Aggregate(Enumerable.Empty<string>(),
(seed, source) => source.GetChildKeys(seed, path))
.Distinct(StringComparer.OrdinalIgnoreCase)
.Select(key => root.GetSection(path == null ? key : ConfigurationPath.Combine(path, key)));
optimizedProviders.Add(optimizedProvider);
}

IEnumerable<string> allKeys;
if (allProvidersAreOptimized)
{
// Optimized implementation: each provider return keys via yield return
// No re-create the accumulated list, No sort,
// Note P = number of providers, K = max number of keys per provider,
// The time complexity is O(K*P*log(K*P))
allKeys = optimizedProviders
.SelectMany(p => p.GetChildKeysInternal(path))
.Distinct(StringComparer.OrdinalIgnoreCase)
.OrderBy(key => key, ConfigurationKeyComparer.Instance);
}
else
{
// Legacy implementation: accumulate keys by passing the partial keys from one provider to the next,
// If each provider re-create the accumulated list and sort inside,
// the time complexity is O(K*P^2*log(K*P))
allKeys = providers
.Aggregate(Enumerable.Empty<string>(),
(seed, source) => source.GetChildKeys(seed, path))
.Distinct(StringComparer.OrdinalIgnoreCase);
}

IEnumerable<IConfigurationSection> children = allKeys
.Select(key => root.GetSection(path == null ? key : ConfigurationPath.Combine(path, key)));
if (reference is null)
{
return children;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using Microsoft.Extensions.Configuration.Memory;
using Microsoft.Extensions.Primitives;
using Xunit;

namespace Microsoft.Extensions.Configuration.Test
Expand Down Expand Up @@ -971,5 +973,139 @@ public void ProviderWithNullReloadToken()
// Assert
Assert.NotNull(config);
}

[Fact]
public void ConfigurationGetChildrenKeysOrdered()
{
// Arrange
var dic1 = new Dictionary<string, string>()
{
{"Services:2", "Service2"}
};
var dic2 = new Dictionary<string, string>()
{
{"Services:10", "Service10"},
};
var memConfigSrc1 = new MemoryConfigurationSource { InitialData = dic1 };
var memConfigSrc2 = new MemoryConfigurationSource { InitialData = dic2 };

var configurationBuilder = new ConfigurationBuilder();
configurationBuilder.Add(memConfigSrc1);
configurationBuilder.Add(memConfigSrc2);

var config = configurationBuilder.Build();

// Act
var keys = config.GetSection("Services").GetChildren().Select(c => c.Key).ToList();

// Assert
Assert.Equal(2, keys.Count);
Assert.Equal("2", keys[0]);
Assert.Equal("10", keys[1]);
}

[Theory]
[ClassData(typeof(OverrideGetChildKeysConfigurationProviders))]
public void ConfigurationGetChildrenNotOptimizedConfigurationProvider(IConfigurationProvider provider)
{
// Arrange
var dic2 = new Dictionary<string, string>()
{
{"key2", "val"},
};
var src1 = new FixedProviderConfigurationSource(provider);
var memConfigSrc2 = new MemoryConfigurationSource { InitialData = dic2 };
var configurationBuilder = new ConfigurationBuilder();
configurationBuilder.Add(src1);
configurationBuilder.Add(memConfigSrc2);
var configSorted = configurationBuilder.Build();
configurationBuilder = new ConfigurationBuilder();
configurationBuilder.Add(memConfigSrc2);
configurationBuilder.Add(src1);
var configNotSorted = configurationBuilder.Build();

// Act
var keysSorted = configSorted.GetChildren().Select(c => c.Key).ToList();
var keysNotSorted = configNotSorted.GetChildren().Select(c => c.Key).ToList();

// Assert
Assert.Equal(3, keysSorted.Count);
Assert.Equal(3, keysNotSorted.Count);

// The keys should be sorted by the 2nd IConfigurationProvider
// because it inherits from helper class ConfigurationProvider.
Assert.Equal("key1", keysSorted[0]);
Assert.Equal("key2", keysSorted[1]);
Assert.Equal("key3", keysSorted[2]);
Assert.Equal("key2", keysNotSorted[0]);
Assert.Equal("key3", keysNotSorted[1]);
Assert.Equal("key1", keysNotSorted[2]);
}

private class OverrideGetChildKeysConfigurationProviders : TheoryData<IConfigurationProvider>
{
public OverrideGetChildKeysConfigurationProviders()
{
Add(new InheritHelperClassButOverrideMethodGetChildKeysConfigurationProvider());
Add(new NotInheritHelperClassConfigurationProvider());
}

public static IEnumerable<string> GetChildKeys(IEnumerable<string> earlierKeys, string? parentPath)
{
if (parentPath != null)
{
throw new ArgumentException("Not support parentPath");
}

foreach (var key in earlierKeys)
{
yield return key;
}

// For the IConfigurationProvider implementation that not inherit from helper class ConfigurationProvider,
// the returned keys might not be sorted.
yield return "key3";
yield return "key1";
}
}

private class FixedProviderConfigurationSource : IConfigurationSource
{
private readonly IConfigurationProvider provider;
public FixedProviderConfigurationSource(IConfigurationProvider provider)
{
this.provider = provider;
}

public IConfigurationProvider Build(IConfigurationBuilder builder) => provider;
}

private class InheritHelperClassButOverrideMethodGetChildKeysConfigurationProvider : ConfigurationProvider
{
public override IEnumerable<string> GetChildKeys(IEnumerable<string> earlierKeys, string? parentPath)
=> OverrideGetChildKeysConfigurationProviders.GetChildKeys(earlierKeys, parentPath);
}

private class NotInheritHelperClassConfigurationProvider : IConfigurationProvider
{
public IEnumerable<string> GetChildKeys(IEnumerable<string> earlierKeys, string? parentPath)
=> OverrideGetChildKeysConfigurationProviders.GetChildKeys(earlierKeys, parentPath);

public IChangeToken GetReloadToken() => new ConfigurationReloadToken();

public void Load()
{
}

public void Set(string key, string? value)
{
}

public bool TryGet(string key, out string? value)
{
value = "val";
return true;
}
}
}
}