-
Notifications
You must be signed in to change notification settings - Fork 5.3k
Description
Description
DI scope validation does not cache previously walked trees, leading to performance degradation
When running scope validation on build for a project with around ~4500 dependencies in the DI-container, it takes more than a minute to build the container on my machine (MacBook Pro 2023 M2 Max).
This issue seems to have been reported in the past by @davidfowl in dotnet/extensions#2353 and a solution was partially implemented in dotnet/extensions#2374.
However, both seem to have gotten lost when Microsoft.Extensions.DependencyInjection was moved to this repository.
Configuration
Together with @siematypie, I recreated the benchmark used in the original PR for https://github.com/dotnet/performance to verify the issue and implemented a fix based on the work done by @brandondahler.
The proposed fix can be found here: #96254
The benchmark results:
BenchmarkDotNet v0.13.11-nightly.20231126.107, macOS Sonoma 14.3 (23D5033f) [Darwin 23.3.0]
Apple M2 Max, 1 CPU, 12 logical and 12 physical cores
.NET SDK 9.0.100-alpha.1.23620.7
[Host] : .NET 9.0.0 (9.0.23.61907), Arm64 RyuJIT AdvSIMD
Job-AZASTU : .NET 9.0.0 (42.42.42.42424), Arm64 RyuJIT AdvSIMD
Job-MAUZGI : .NET 9.0.0 (42.42.42.42424), Arm64 RyuJIT AdvSIMD
PowerPlanMode=00000000-0000-0000-0000-000000000000 IterationTime=250.0000 ms MaxIterationCount=20
MinIterationCount=15 WarmupCount=1
| Method | Job | Toolchain | Mean | Error | StdDev | Median | Min | Max | Ratio | Gen0 | Gen1 | Allocated | Alloc Ratio |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| ValidateCallSite | Job-AZASTU | /runtime-baseline/artifacts/bin/testhost/net9.0-osx-Debug-arm64/shared/Microsoft.NETCore.App/9.0.0/corerun | 86.55 μs | 0.415 μs | 0.347 μs | 86.42 μs | 86.22 μs | 87.40 μs | 1.00 | 2.7322 | 0.3415 | 23.41 KB | 1.00 |
| ValidateCallSite | Job-MAUZGI | /runtime/artifacts/bin/testhost/net9.0-osx-Debug-arm64/shared/Microsoft.NETCore.App/9.0.0/corerun | 32.97 μs | 0.074 μs | 0.069 μs | 32.95 μs | 32.90 μs | 33.10 μs | 0.38 | 3.6842 | 0.7895 | 30.64 KB | 1.31 |
The benchmark used:
using System;
using BenchmarkDotNet.Attributes;
using MicroBenchmarks;
namespace Microsoft.Extensions.DependencyInjection;
[BenchmarkCategory(Categories.Libraries)]
public class CallSiteValidator
{
private ServiceCollection _services;
[GlobalSetup]
public void Setup()
{
_services = new ServiceCollection();
_services.AddTransient<A>();
_services.AddTransient<B>();
_services.AddTransient<C>();
_services.AddTransient<D>();
_services.AddTransient<E>();
_services.AddTransient<F>();
_services.AddTransient<G>();
_services.AddTransient<H>();
_services.AddTransient<I>();
_services.AddTransient<J>();
_services.AddTransient<K>();
_services.AddTransient<L>();
_services.AddTransient<M>();
_services.AddTransient<N>();
_services.AddTransient<O>();
_services.AddTransient<P>();
}
[Benchmark]
public void ValidateCallSite()
{
_services.BuildServiceProvider(new ServiceProviderOptions { ValidateScopes = true, ValidateOnBuild = true });
}
private class A
{
public A(B b, C c, D d, E e, F f, G g, H h, I i, J j, K k, L l)
{
}
}
private class B
{
public B(C c, D d, E e, F f, G g, H h, I i, J j, K k, L l)
{
}
}
private class C
{
public C(D d, E e, F f, G g, H h, I i, J j, K k, L l)
{
}
}
private class D
{
public D(E e, F f, G g, H h, I i, J j, K k, L l)
{
}
}
private class E
{
public E(F f, G g, H h, I i, J j, K k, L l)
{
}
}
private class F
{
public F(G g, H h, I i, J j, K k, L l)
{
}
}
private class G
{
public G(H h, I i, J j, K k, L l)
{
}
}
private class H
{
public H(I i, J j, K k, L l)
{
}
}
private class I
{
public I(J j, K k, L l)
{
}
}
private class J
{
public J(K k, L l)
{
}
}
private class K
{
public K(L l)
{
}
}
private class L
{
public L(M m)
{
}
}
private class M
{
public M(N n)
{
}
}
private class N
{
public N(O o)
{
}
}
private class O
{
public O(P p)
{
}
}
private class P
{
public P()
{
}
}
}On the same machine, I also tried the dependency graph from https://github.com/sergey-kolodiy/AspNetCore.DI/blob/master/AspNetCore.DI/AspNetCore.DI.Default/Startup.cs mentioned in the original issue.
Without the fix applied the benchmark was cancelled during the warmup phase due to the long runtime
OverheadJitting 1: 1 op, 97042.00 ns, 97.0420 us/op
WorkloadJitting 1: 1 op, 414446043542.00 ns, 414.4460 s/op
WorkloadWarmup 1: 1 op, 415264853000.00 ns, 415.2649 s/op
With the fix applied the benchmark completed:
BenchmarkDotNet v0.13.11-nightly.20231126.107, macOS Sonoma 14.3 (23D5033f) [Darwin 23.3.0]
Apple M2 Max, 1 CPU, 12 logical and 12 physical cores
.NET SDK 9.0.100-alpha.1.23620.7
[Host] : .NET 9.0.0 (9.0.23.61907), Arm64 RyuJIT AdvSIMD
Job-WOBNXZ : .NET 9.0.0 (42.42.42.42424), Arm64 RyuJIT AdvSIMD
PowerPlanMode=00000000-0000-0000-0000-000000000000 Toolchain=CoreRun IterationTime=250.0000 ms
MaxIterationCount=20 MinIterationCount=15 WarmupCount=1
| Method | Mean | Error | StdDev | Median | Min | Max | Gen0 | Gen1 | Allocated |
|---|---|---|---|---|---|---|---|---|---|
| ValidateCallSite | 180.3 μs | 0.64 μs | 0.56 μs | 180.3 μs | 179.3 μs | 181.5 μs | 14.2045 | 1.4205 | 119.28 KB |