Skip to content

Commit a6a8c62

Browse files
authored
Merge pull request #1478 from autofac/feature/perf
- Performance-focused refactor across resolve pipeline and middleware to reduce per-resolve overhead and trim hot-path allocations. - Removed several LINQ-based paths in critical resolution/injection flows and replaced them with lower-overhead logic. - Added and improved caching for AnyKey and adapted AnyKey registrations, including per-scope cache behavior. - Added support for externally managed reflection caches and expanded reflection cache dictionary handling for keyed services/parameters. - Optimized reflection activation and keyed service parameter injection to skip unnecessary work when possible. - Updated benchmark coverage with additional AnyKey scenarios and benchmark-set updates to verify performance impact. - Included cleanup and stability updates (for example, removing unused usings and reverting temporary audit configuration changes). - Validation completed with test and benchmark runs during development.
2 parents 812f644 + 7b3a2c8 commit a6a8c62

24 files changed

Lines changed: 333 additions & 278 deletions

bench/Autofac.Benchmarks/BenchmarkSet.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ public static class BenchmarkSet
1313
typeof(ConcurrencyBenchmark),
1414
typeof(ConcurrencyNestedScopeBenchmark),
1515
typeof(KeyedGenericBenchmark),
16+
typeof(KeyedAnyKeySimpleBenchmark),
1617
typeof(KeyedNestedBenchmark),
1718
typeof(KeyedSimpleBenchmark),
1819
typeof(KeylessGenericBenchmark),

bench/Autofac.Benchmarks/Decorators/DecoratorBenchmarkBase.cs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
// Copyright (c) Autofac Project. All rights reserved.
22
// Licensed under the MIT License. See LICENSE in the project root for license information.
33

4-
using Microsoft.CodeAnalysis.CSharp.Syntax;
5-
64
namespace Autofac.Benchmarks.Decorators;
75

86
public abstract class DecoratorBenchmarkBase<TCommandHandler>
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
// Copyright (c) Autofac Project. All rights reserved.
2+
// Licensed under the MIT License. See LICENSE in the project root for license information.
3+
4+
using Autofac.Benchmarks.Decorators.Scenario;
5+
using Autofac.Core;
6+
7+
namespace Autofac.Benchmarks.Decorators;
8+
9+
/// <summary>
10+
/// Benchmarks keyed decorators when components are registered with AnyKey.
11+
/// </summary>
12+
public class KeyedAnyKeySimpleBenchmark : DecoratorBenchmarkBase<ICommandHandler>
13+
{
14+
[GlobalSetup]
15+
public void Setup()
16+
{
17+
var builder = new ContainerBuilder();
18+
19+
builder.RegisterType<CommandHandlerOne>()
20+
.Keyed<ICommandHandler>(KeyedService.AnyKey);
21+
builder.RegisterType<CommandHandlerTwo>()
22+
.Keyed<ICommandHandler>(KeyedService.AnyKey);
23+
builder.RegisterDecorator<ICommandHandler>(
24+
(c, inner) => new CommandHandlerDecoratorOne(inner),
25+
fromKey: "handler");
26+
27+
Container = builder.Build();
28+
}
29+
}

src/Autofac/Core/Activators/Reflection/ReflectionActivator.cs

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -228,7 +228,11 @@ private void UseSingleConstructorActivation(IResolvePipelineBuilder pipelineBuil
228228

229229
var instance = boundConstructor.Instantiate();
230230

231-
InjectProperties(instance, context, boundConstructor, GetAllParameters(context.Parameters));
231+
if (ShouldInjectProperties(boundConstructor))
232+
{
233+
var prioritizedParameters = GetAllParameters(context.Parameters);
234+
InjectProperties(instance, context, boundConstructor, prioritizedParameters);
235+
}
232236

233237
context.Instance = instance;
234238

@@ -422,20 +426,12 @@ private string GetBindingFailureMessage(BoundConstructor[] constructorBindings)
422426

423427
private void InjectProperties(object instance, IComponentContext context, BoundConstructor constructor, IEnumerable<Parameter> allParameters)
424428
{
425-
// We only need to do any injection if we have a set of property information.
426-
if (_defaultFoundPropertySet is null)
427-
{
428-
return;
429-
}
430-
431-
// If this constructor sets all required members,
432-
// and we have no configured properties, we can just jump out.
433-
if (_configuredProperties.Length == 0 && constructor.SetsRequiredMembers)
429+
if (!ShouldInjectProperties(constructor))
434430
{
435431
return;
436432
}
437433

438-
var workingSetOfProperties = (InjectablePropertyState[])_defaultFoundPropertySet.Clone();
434+
var workingSetOfProperties = (InjectablePropertyState[])_defaultFoundPropertySet!.Clone();
439435

440436
foreach (var configuredProperty in _configuredProperties)
441437
{
@@ -509,6 +505,16 @@ private void InjectProperties(object instance, IComponentContext context, BoundC
509505
}
510506
}
511507

508+
private bool ShouldInjectProperties(BoundConstructor constructor)
509+
{
510+
if (_defaultFoundPropertySet is null)
511+
{
512+
return false;
513+
}
514+
515+
return _configuredProperties.Length != 0 || !constructor.SetsRequiredMembers;
516+
}
517+
512518
private string BuildRequiredPropertyResolutionMessage(IReadOnlyList<InjectableProperty> failingRequiredProperties)
513519
{
514520
var propertyDescriptions = new StringBuilder();

src/Autofac/Core/KeyedServiceParameterInjector.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,21 @@ private static IEnumerable<Parameter> AppendKeyParameter(IEnumerable<Parameter>
132132
return new Parameter[] { keyParameter };
133133
}
134134

135+
// Build a concrete array to avoid LINQ AppendIterator allocation.
136+
if (parameters is IReadOnlyCollection<Parameter> collection)
137+
{
138+
var result = new Parameter[collection.Count + 1];
139+
var i = 0;
140+
foreach (var p in collection)
141+
{
142+
result[i++] = p;
143+
}
144+
145+
result[i] = keyParameter;
146+
return result;
147+
}
148+
149+
// Fallback for unknown enumerable types.
135150
return parameters.Append(keyParameter);
136151
}
137152
}

src/Autofac/Core/Lifetime/LifetimeScope.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
using System.Diagnostics;
66
using System.Globalization;
77
using System.Runtime.CompilerServices;
8-
using System.Threading;
98
#if NET5_0_OR_GREATER
109
using System.Runtime.Loader;
1110
#endif

src/Autofac/Core/ReflectionCacheSet.cs

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ public sealed class ReflectionCacheSet
1818

1919
private readonly ConcurrentDictionary<string, IReflectionCache> _caches = new();
2020

21+
private readonly List<WeakReference<IReflectionCache>> _externalCaches = new();
22+
2123
/// <summary>
2224
/// Initializes a new instance of the <see cref="ReflectionCacheSet"/> class.
2325
/// </summary>
@@ -101,6 +103,26 @@ public TCacheStore GetOrCreateCache<TCacheStore>(string cacheName, Func<string,
101103
}
102104
}
103105

106+
/// <summary>
107+
/// Register an externally-owned <see cref="IReflectionCache"/> so it participates
108+
/// in <see cref="Clear()"/> and <see cref="Clear(ReflectionCacheClearPredicate)"/> calls.
109+
/// The cache is held via a weak reference so it does not prevent garbage collection
110+
/// of the owning object (e.g., a container or registration source).
111+
/// </summary>
112+
/// <param name="cache">The cache to register.</param>
113+
public void RegisterExternalCache(IReflectionCache cache)
114+
{
115+
if (cache is null)
116+
{
117+
throw new ArgumentNullException(nameof(cache));
118+
}
119+
120+
lock (_externalCaches)
121+
{
122+
_externalCaches.Add(new WeakReference<IReflectionCache>(cache));
123+
}
124+
}
125+
104126
/// <summary>
105127
/// Clear the internal reflection cache. Only call this method if you are
106128
/// dynamically unloading types from the process; calling this method
@@ -117,6 +139,8 @@ public void Clear()
117139
{
118140
cache.Clear();
119141
}
142+
143+
ClearExternalCaches(static cache => cache.Clear());
120144
}
121145

122146
/// <summary>
@@ -139,6 +163,8 @@ public void Clear(ReflectionCacheClearPredicate predicate)
139163
{
140164
cache.Clear(predicate);
141165
}
166+
167+
ClearExternalCaches(cache => cache.Clear(predicate));
142168
}
143169

144170
/// <summary>
@@ -173,6 +199,25 @@ private static bool TryGetSharedCache([NotNullWhen(true)] out ReflectionCacheSet
173199
return _sharedSet.TryGetTarget(out sharedCache);
174200
}
175201

202+
private void ClearExternalCaches(Action<IReflectionCache> clearAction)
203+
{
204+
lock (_externalCaches)
205+
{
206+
for (var i = _externalCaches.Count - 1; i >= 0; i--)
207+
{
208+
if (_externalCaches[i].TryGetTarget(out var cache))
209+
{
210+
clearAction(cache);
211+
}
212+
else
213+
{
214+
// Prune dead references.
215+
_externalCaches.RemoveAt(i);
216+
}
217+
}
218+
}
219+
}
220+
176221
private IEnumerable<IReflectionCache> GetAllCaches()
177222
{
178223
foreach (var externalItem in _caches)

src/Autofac/Core/Resolving/Middleware/ActivatorErrorHandlingMiddleware.cs

Lines changed: 12 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33

44
using System.Globalization;
55
using Autofac.Core.Resolving.Pipeline;
6-
using Autofac.Diagnostics;
76

87
namespace Autofac.Core.Resolving.Middleware;
98

@@ -29,20 +28,23 @@ private ActivatorErrorHandlingMiddleware()
2928
/// <inheritdoc />
3029
public void Execute(ResolveRequestContext context, Action<ResolveRequestContext> next)
3130
{
32-
if (!AutofacMetrics.MetricsEnabled)
31+
try
3332
{
34-
ExecuteCore(context, next);
35-
return;
36-
}
33+
next(context);
3734

38-
var timer = ValueStopwatch.StartNew();
39-
try
35+
if (context.Instance is null)
36+
{
37+
// Exited the Activation Stage without creating an instance.
38+
throw new DependencyResolutionException(MiddlewareMessages.ActivatorDidNotPopulateInstance);
39+
}
40+
}
41+
catch (ObjectDisposedException)
4042
{
41-
ExecuteCore(context, next);
43+
throw;
4244
}
43-
finally
45+
catch (Exception ex)
4446
{
45-
AutofacMetrics.RecordMiddlewareExecution(nameof(ActivatorErrorHandlingMiddleware), timer.GetElapsedTime());
47+
throw PropagateActivationException(context.Registration.Activator, ex);
4648
}
4749
}
4850

@@ -65,26 +67,4 @@ private static DependencyResolutionException PropagateActivationException(IInsta
6567
result.Data[ActivatorChainExceptionData] = activatorChain;
6668
return result;
6769
}
68-
69-
private static void ExecuteCore(ResolveRequestContext context, Action<ResolveRequestContext> next)
70-
{
71-
try
72-
{
73-
next(context);
74-
75-
if (context.Instance is null)
76-
{
77-
// Exited the Activation Stage without creating an instance.
78-
throw new DependencyResolutionException(MiddlewareMessages.ActivatorDidNotPopulateInstance);
79-
}
80-
}
81-
catch (ObjectDisposedException)
82-
{
83-
throw;
84-
}
85-
catch (Exception ex)
86-
{
87-
throw PropagateActivationException(context.Registration.Activator, ex);
88-
}
89-
}
9070
}

src/Autofac/Core/Resolving/Middleware/CircularDependencyDetectorMiddleware.cs

Lines changed: 26 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
using System.Globalization;
55
using System.Runtime.CompilerServices;
66
using Autofac.Core.Resolving.Pipeline;
7-
using Autofac.Diagnostics;
87

98
namespace Autofac.Core.Resolving.Middleware;
109

@@ -39,51 +38,6 @@ public CircularDependencyDetectorMiddleware(int maxResolveDepth)
3938

4039
/// <inheritdoc/>
4140
public void Execute(ResolveRequestContext context, Action<ResolveRequestContext> next)
42-
{
43-
if (!AutofacMetrics.MetricsEnabled)
44-
{
45-
ExecuteCore(context, next);
46-
return;
47-
}
48-
49-
var timer = ValueStopwatch.StartNew();
50-
try
51-
{
52-
ExecuteCore(context, next);
53-
}
54-
finally
55-
{
56-
AutofacMetrics.RecordMiddlewareExecution(nameof(CircularDependencyDetectorMiddleware), timer.GetElapsedTime());
57-
}
58-
}
59-
60-
/// <inheritdoc/>
61-
public override string ToString() => nameof(CircularDependencyDetectorMiddleware);
62-
63-
private static string CreateDependencyGraphTo(IComponentRegistration registration, IEnumerable<ResolveRequestContext> requestStack)
64-
{
65-
if (registration == null)
66-
{
67-
throw new ArgumentNullException(nameof(registration));
68-
}
69-
70-
if (requestStack == null)
71-
{
72-
throw new ArgumentNullException(nameof(requestStack));
73-
}
74-
75-
var dependencyGraph = Display(registration);
76-
77-
return requestStack.Select(a => a.Registration)
78-
.Aggregate(dependencyGraph, (current, requestor) => Display(requestor) + " -> " + current);
79-
}
80-
81-
private static string Display(IComponentRegistration registration)
82-
{
83-
return registration.Activator.DisplayName();
84-
}
85-
86-
private void ExecuteCore(ResolveRequestContext context, Action<ResolveRequestContext> next)
8741
{
8842
if (context.Operation is not IDependencyTrackingResolveOperation dependencyTrackingResolveOperation)
8943
{
@@ -141,4 +95,30 @@ private void ExecuteCore(ResolveRequestContext context, Action<ResolveRequestCon
14195
requestStack.Pop();
14296
}
14397
}
98+
99+
/// <inheritdoc/>
100+
public override string ToString() => nameof(CircularDependencyDetectorMiddleware);
101+
102+
private static string CreateDependencyGraphTo(IComponentRegistration registration, IEnumerable<ResolveRequestContext> requestStack)
103+
{
104+
if (registration == null)
105+
{
106+
throw new ArgumentNullException(nameof(registration));
107+
}
108+
109+
if (requestStack == null)
110+
{
111+
throw new ArgumentNullException(nameof(requestStack));
112+
}
113+
114+
var dependencyGraph = Display(registration);
115+
116+
return requestStack.Select(a => a.Registration)
117+
.Aggregate(dependencyGraph, (current, requestor) => Display(requestor) + " -> " + current);
118+
}
119+
120+
private static string Display(IComponentRegistration registration)
121+
{
122+
return registration.Activator.DisplayName();
123+
}
144124
}

src/Autofac/Core/Resolving/Middleware/CoreEventMiddleware.cs

Lines changed: 1 addition & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
// Licensed under the MIT License. See LICENSE in the project root for license information.
33

44
using Autofac.Core.Resolving.Pipeline;
5-
using Autofac.Diagnostics;
65

76
namespace Autofac.Core.Resolving.Middleware;
87

@@ -40,20 +39,6 @@ internal CoreEventMiddleware(ResolveEventType eventType, PipelinePhase phase, Ac
4039
/// <inheritdoc/>
4140
public void Execute(ResolveRequestContext context, Action<ResolveRequestContext> next)
4241
{
43-
if (!AutofacMetrics.MetricsEnabled)
44-
{
45-
_callback(context, next);
46-
return;
47-
}
48-
49-
var timer = ValueStopwatch.StartNew();
50-
try
51-
{
52-
_callback(context, next);
53-
}
54-
finally
55-
{
56-
AutofacMetrics.RecordMiddlewareExecution(ToString(), timer.GetElapsedTime());
57-
}
42+
_callback(context, next);
5843
}
5944
}

0 commit comments

Comments
 (0)