Skip to content

Commit c804b4f

Browse files
authored
Merge pull request #1491 from autofac/feature/issue-1461
Cache compiled factory-delegate generators to avoid recompilation across scopes (#1461)
2 parents 8f05d9b + a5c3de1 commit c804b4f

6 files changed

Lines changed: 557 additions & 60 deletions

File tree

bench/Autofac.Benchmarks/BenchmarkSet.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,5 +34,6 @@ public static class BenchmarkSet
3434
typeof(LambdaResolveBenchmark),
3535
typeof(RequiredPropertyBenchmark),
3636
typeof(ModuleRegistrationBenchmark),
37+
typeof(GeneratedFactoryChildScopeBenchmark),
3738
};
3839
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
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+
namespace Autofac.Benchmarks;
5+
6+
/// <summary>
7+
/// Measures the cost of resolving a component registered in a child lifetime scope that
8+
/// depends on <c>Func&lt;T&gt;</c>, where <c>T</c> is registered in the root container.
9+
/// </summary>
10+
/// <remarks>
11+
/// See https://github.com/autofac/Autofac/issues/1461. Prior to the fix, each child scope
12+
/// that registered a consumer of <c>Func&lt;T&gt;</c> caused the factory's expression tree to
13+
/// be recompiled via <c>Expression.Lambda(...).Compile()</c> on every resolution. The fix
14+
/// caches the compiled generator keyed on <c>(delegateType, parameterMapping)</c> so the
15+
/// expensive compile happens once and is reused across all scopes. Compare against a baseline
16+
/// package version using a command such as
17+
/// <c>dotnet run -c Release --project bench/Autofac.Benchmarks -- --baseline-version 9.1.0
18+
/// --filter *GeneratedFactoryChildScopeBenchmark*</c>.
19+
/// </remarks>
20+
public class GeneratedFactoryChildScopeBenchmark
21+
{
22+
private IContainer _container = default!;
23+
24+
[Params(100, 1000)]
25+
public int ScopeCount
26+
{
27+
get; set;
28+
}
29+
30+
[GlobalSetup]
31+
public void Setup()
32+
{
33+
var builder = new ContainerBuilder();
34+
builder.RegisterType<Product>();
35+
_container = builder.Build();
36+
}
37+
38+
/// <summary>
39+
/// Creates a child scope with a consumer of <c>Func&lt;Product&gt;</c> registered locally,
40+
/// then resolves that consumer on each iteration. Before the fix, each child-scope
41+
/// resolution recompiled the factory expression tree; after the fix, the compiled generator
42+
/// is retrieved from a shared cache.
43+
/// </summary>
44+
[Benchmark]
45+
public void ResolveGeneratedFactoryFromChildScope()
46+
{
47+
for (var i = 0; i < ScopeCount; i++)
48+
{
49+
using var scope = _container.BeginLifetimeScope(c => c.RegisterType<Consumer>());
50+
var consumer = scope.Resolve<Consumer>();
51+
GC.KeepAlive(consumer.Factory());
52+
}
53+
}
54+
55+
private sealed class Product
56+
{
57+
}
58+
59+
private sealed class Consumer
60+
{
61+
public Func<Product> Factory
62+
{
63+
get;
64+
}
65+
66+
public Consumer(Func<Product> factory)
67+
{
68+
Factory = factory;
69+
}
70+
}
71+
}

src/Autofac/Core/InternalReflectionCaches.cs

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

44
using System.Reflection;
55
using Autofac.Core.Activators.Reflection;
6+
using Autofac.Features.GeneratedFactories;
67
using Autofac.Util;
78
using Autofac.Util.Cache;
89

@@ -48,6 +49,20 @@ public InternalReflectionCaches(ReflectionCacheSet set)
4849
ServiceKeyParameterAttributes = set.GetOrCreateCache<ReflectionCacheParameterDictionary<bool>>(nameof(ServiceKeyParameterAttributes));
4950
ServiceKeyPropertyAttributes = set.GetOrCreateCache<ReflectionCacheDictionary<PropertyInfo, bool>>(nameof(ServiceKeyPropertyAttributes));
5051
ServiceKeyUsageByType = set.GetOrCreateCache<ReflectionCacheDictionary<Type, bool>>(nameof(ServiceKeyUsageByType));
52+
53+
GeneratedFactoryServiceOnlyGenerators = set.GetOrCreateCache(
54+
nameof(GeneratedFactoryServiceOnlyGenerators),
55+
_ => new ReflectionCacheTypeKeyedDictionary<ParameterMapping, Func<Service, IComponentContext, IEnumerable<Parameter>, Delegate>>
56+
{
57+
Usage = ReflectionCacheUsage.All,
58+
});
59+
60+
GeneratedFactoryServiceRegistrationGenerators = set.GetOrCreateCache(
61+
nameof(GeneratedFactoryServiceRegistrationGenerators),
62+
_ => new ReflectionCacheTypeKeyedDictionary<ParameterMapping, Func<Service, ServiceRegistration, IComponentContext, IEnumerable<Parameter>, Delegate>>
63+
{
64+
Usage = ReflectionCacheUsage.All,
65+
});
5166
}
5267

5368
/// <summary>
@@ -179,4 +194,24 @@ public ReflectionCacheDictionary<Type, bool> ModuleOverridesAttachToRegistration
179194
{
180195
get;
181196
}
197+
198+
/// <summary>
199+
/// Gets the cache of compiled factory-delegate generators for
200+
/// <see cref="Features.GeneratedFactories.FactoryGenerator"/> instances that resolve via
201+
/// <c>ResolveService</c>. Keyed on <c>(delegateType, effectiveParameterMapping)</c>.
202+
/// </summary>
203+
public ReflectionCacheTypeKeyedDictionary<ParameterMapping, Func<Service, IComponentContext, IEnumerable<Parameter>, Delegate>> GeneratedFactoryServiceOnlyGenerators
204+
{
205+
get;
206+
}
207+
208+
/// <summary>
209+
/// Gets the cache of compiled factory-delegate generators for
210+
/// <see cref="Features.GeneratedFactories.FactoryGenerator"/> instances that resolve via
211+
/// <see cref="IComponentContext.ResolveComponent"/>. Keyed on <c>(delegateType, effectiveParameterMapping)</c>.
212+
/// </summary>
213+
public ReflectionCacheTypeKeyedDictionary<ParameterMapping, Func<Service, ServiceRegistration, IComponentContext, IEnumerable<Parameter>, Delegate>> GeneratedFactoryServiceRegistrationGenerators
214+
{
215+
get;
216+
}
182217
}

src/Autofac/Features/GeneratedFactories/FactoryGenerator.cs

Lines changed: 131 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -38,25 +38,20 @@ public FactoryGenerator(Type delegateType, Service service, ParameterMapping par
3838

3939
Enforce.ArgumentTypeIsFunction(delegateType);
4040

41-
_generator = CreateGenerator(
42-
(activatorContextParam, resolveParameterArray) =>
43-
{
44-
// c, service, [new Parameter(name, (object)dps)]*
45-
var resolveParams = new[]
46-
{
47-
activatorContextParam,
48-
Expression.Constant(service, typeof(Service)),
49-
Expression.NewArrayInit(typeof(Parameter), resolveParameterArray),
50-
};
51-
52-
// c.Resolve(...)
53-
// default! here is for reflection only
54-
return Expression.Call(
55-
ReflectionExtensions.GetMethod<IComponentContext>(cc => cc.ResolveService(default!, default!)),
56-
resolveParams);
57-
},
58-
delegateType,
59-
GetParameterMapping(delegateType, parameterMapping));
41+
var pm = GetParameterMapping(delegateType, parameterMapping);
42+
43+
// Retrieve the cached compiled generator for this delegate type and parameter mapping,
44+
// or compile it once on first use. The cached delegate is parameterised on 'service'
45+
// (passed at invocation time) so it contains no instance-specific captures and is safe
46+
// to share across all FactoryGenerator instances with the same structural signature.
47+
// Accessing ReflectionCacheSet.Shared at point-of-use (not stored in a field) ensures
48+
// correct weak-reference collection of the cache set.
49+
var compiledGenerator = ReflectionCacheSet.Shared.Internal.GeneratedFactoryServiceOnlyGenerators.GetOrAdd(
50+
(delegateType, pm),
51+
static key => CreateServiceOnlyGenerator(key.Item1, key.Item2));
52+
53+
// Close over the specific service so _generator matches the expected signature.
54+
_generator = (context, parameters) => compiledGenerator(service, context, parameters);
6055
}
6156

6257
/// <summary>
@@ -72,27 +67,22 @@ public FactoryGenerator(Type delegateType, Service service, ServiceRegistration
7267
{
7368
Enforce.ArgumentTypeIsFunction(delegateType);
7469

75-
_generator = CreateGenerator(
76-
(activatorContextParam, resolveParameterArray) =>
77-
{
78-
// new ResolveRequest(service, productRegistration, [new Parameter(name, (object)dps)])*)
79-
var newExpression = Expression.New(
80-
_requestConstructor,
81-
Expression.Constant(service, typeof(Service)),
82-
Expression.Constant(productRegistration, typeof(ServiceRegistration)),
83-
Expression.NewArrayInit(typeof(Parameter), resolveParameterArray),
84-
Expression.Constant(null, typeof(IComponentRegistration)));
70+
var pm = GetParameterMapping(delegateType, parameterMapping);
8571

86-
// c.Resolve(...)
87-
// default! for reflection only
88-
return Expression.Call(
89-
activatorContextParam,
90-
ReflectionExtensions.GetMethod<IComponentContext>(cc => cc.ResolveComponent(
91-
new ResolveRequest(default!, default, default(Parameter[])!, default))),
92-
newExpression);
93-
},
94-
delegateType,
95-
GetParameterMapping(delegateType, parameterMapping));
72+
// Retrieve the cached compiled generator for this delegate type and parameter mapping,
73+
// or compile it once on first use. The cached delegate is parameterised on both 'service'
74+
// and 'productRegistration' (passed at invocation time), so it contains no instance-specific
75+
// captures and is safe to share across all FactoryGenerator instances for the same delegate type.
76+
// Accessing ReflectionCacheSet.Shared at point-of-use (not stored in a field) ensures
77+
// correct weak-reference collection of the cache set.
78+
var compiledGenerator = ReflectionCacheSet.Shared.Internal.GeneratedFactoryServiceRegistrationGenerators.GetOrAdd(
79+
(delegateType, pm),
80+
static key => CreateServiceRegistrationGenerator(key.Item1, key.Item2));
81+
82+
// Close over the specific service and product registration so _generator matches the
83+
// expected signature. These values are instance-specific but are not baked into the
84+
// compiled expression tree — they are passed as arguments at each invocation.
85+
_generator = (context, parameters) => compiledGenerator(service, productRegistration, context, parameters);
9686
}
9787

9888
/// <summary>
@@ -144,13 +134,18 @@ private static bool DelegateTypeIsFunc(Type delegateType)
144134
return delegateType.Name.StartsWith("Func`", StringComparison.Ordinal);
145135
}
146136

147-
private static Func<IComponentContext, IEnumerable<Parameter>, Delegate> CreateGenerator(Func<Expression, Expression[], Expression> makeResolveCall, Type delegateType, ParameterMapping pm)
137+
/// <summary>
138+
/// Creates a compiled generator for the overload that resolves via <c>ResolveService</c>.
139+
/// The returned delegate accepts <paramref name="delegateType"/>-specific parameters at runtime and contains
140+
/// no instance-specific compile-time captures, making it safe to cache and share across all
141+
/// <see cref="FactoryGenerator"/> instances with the same (<paramref name="delegateType"/>, <paramref name="pm"/>) pair.
142+
/// </summary>
143+
private static Func<Service, IComponentContext, IEnumerable<Parameter>, Delegate> CreateServiceOnlyGenerator(Type delegateType, ParameterMapping pm)
148144
{
149-
// (c, p) => ([dps]*) => (drt)Resolve(c, productRegistration, [new NamedParameter(name, (object)dps)]*)
150-
// (c, p)
145+
// Outer parameters: (Service svc, IComponentContext c, IEnumerable<Parameter> p)
146+
var serviceParam = Expression.Parameter(typeof(Service), "svc");
151147
var activatorContextParam = Expression.Parameter(typeof(IComponentContext), "c");
152148
var activatorParamsParam = Expression.Parameter(typeof(IEnumerable<Parameter>), "p");
153-
var activatorParams = new[] { activatorContextParam, activatorParamsParam };
154149

155150
var invoke = delegateType.GetDeclaredMethod("Invoke");
156151

@@ -163,18 +158,81 @@ private static Func<IComponentContext, IEnumerable<Parameter>, Delegate> CreateG
163158
Expression? resolveCast = null;
164159
if (DelegateTypeIsFunc(delegateType) && pm == ParameterMapping.ByType)
165160
{
166-
// Issue #269:
167-
// If we're resolving a Func<X1...XN>() and there are duplicate input parameter types
168-
// and the parameter mapping is by type, we shouldn't be able to resolve it.
161+
// Issue #269: duplicate input parameter types are not allowed for ByType mapping.
169162
var arguments = delegateType.GenericTypeArguments;
170163
var returnType = arguments[arguments.Length - 1];
164+
Array.Resize(ref arguments, arguments.Length - 1);
165+
if (arguments.Distinct().Count() != arguments.Length)
166+
{
167+
object[] argumentsArray = arguments.ToArray();
168+
var message = string.Format(CultureInfo.CurrentCulture, GeneratedFactoryRegistrationSourceResources.DuplicateTypesInTypeMappedFuncParameterList, returnType.AssemblyQualifiedName, string.Join(", ", argumentsArray));
169+
resolveCast = Expression.Throw(Expression.Constant(new DependencyResolutionException(message)), invoke.ReturnType);
170+
}
171+
}
172+
173+
if (resolveCast == null)
174+
{
175+
var resolveParameterArray = MapParameters(creatorParams, pm);
171176

172-
// Remove the return type to check the list of input types only.
177+
// c.ResolveService(svc, [new Parameter(...)]*) — 'svc' is supplied as a runtime parameter.
178+
var resolveParams = new Expression[]
179+
{
180+
activatorContextParam,
181+
serviceParam,
182+
Expression.NewArrayInit(typeof(Parameter), resolveParameterArray),
183+
};
184+
185+
var resolveCall = Expression.Call(
186+
ReflectionExtensions.GetMethod<IComponentContext>(cc => cc.ResolveService(default!, default!)),
187+
resolveParams);
188+
189+
resolveCast = Expression.Convert(resolveCall, invoke.ReturnType);
190+
}
191+
192+
// ([dps]*) => (drt)c.ResolveService(svc, [...])
193+
var creator = Expression.Lambda(delegateType, resolveCast, creatorParams);
194+
195+
// (svc, c, p) => ([dps]*) => ...
196+
var activator = Expression.Lambda<Func<Service, IComponentContext, IEnumerable<Parameter>, Delegate>>(
197+
creator,
198+
serviceParam,
199+
activatorContextParam,
200+
activatorParamsParam);
201+
202+
return activator.Compile();
203+
}
204+
205+
/// <summary>
206+
/// Creates a compiled generator for the overload that resolves via <see cref="IComponentContext.ResolveComponent"/>.
207+
/// The returned delegate accepts <paramref name="delegateType"/>-specific parameters at runtime and contains
208+
/// no instance-specific compile-time captures, making it safe to cache and share across all
209+
/// <see cref="FactoryGenerator"/> instances with the same (<paramref name="delegateType"/>, <paramref name="pm"/>) pair.
210+
/// </summary>
211+
private static Func<Service, ServiceRegistration, IComponentContext, IEnumerable<Parameter>, Delegate> CreateServiceRegistrationGenerator(Type delegateType, ParameterMapping pm)
212+
{
213+
// Outer parameters: (Service svc, ServiceRegistration sr, IComponentContext c, IEnumerable<Parameter> p)
214+
var serviceParam = Expression.Parameter(typeof(Service), "svc");
215+
var serviceRegistrationParam = Expression.Parameter(typeof(ServiceRegistration), "sr");
216+
var activatorContextParam = Expression.Parameter(typeof(IComponentContext), "c");
217+
var activatorParamsParam = Expression.Parameter(typeof(IEnumerable<Parameter>), "p");
218+
219+
var invoke = delegateType.GetDeclaredMethod("Invoke");
220+
221+
// [dps]*
222+
var creatorParams = invoke
223+
.GetParameters()
224+
.Select(pi => Expression.Parameter(pi.ParameterType, pi.Name))
225+
.ToList();
226+
227+
Expression? resolveCast = null;
228+
if (DelegateTypeIsFunc(delegateType) && pm == ParameterMapping.ByType)
229+
{
230+
// Issue #269: duplicate input parameter types are not allowed for ByType mapping.
231+
var arguments = delegateType.GenericTypeArguments;
232+
var returnType = arguments[arguments.Length - 1];
173233
Array.Resize(ref arguments, arguments.Length - 1);
174234
if (arguments.Distinct().Count() != arguments.Length)
175235
{
176-
// There are duplicate input types - that's a problem. Throw
177-
// when the function is invoked.
178236
object[] argumentsArray = arguments.ToArray();
179237
var message = string.Format(CultureInfo.CurrentCulture, GeneratedFactoryRegistrationSourceResources.DuplicateTypesInTypeMappedFuncParameterList, returnType.AssemblyQualifiedName, string.Join(", ", argumentsArray));
180238
resolveCast = Expression.Throw(Expression.Constant(new DependencyResolutionException(message)), invoke.ReturnType);
@@ -183,23 +241,36 @@ private static Func<IComponentContext, IEnumerable<Parameter>, Delegate> CreateG
183241

184242
if (resolveCast == null)
185243
{
186-
// Issue #269: There aren't duplicate parameter types in the generated
187-
// factory, so in the case of Func<X1...XN>() typed parameter mapping,
188-
// if there are duplicate types in the target constructor, both constructor
189-
// parameters will get the same value passed in. (We don't know
190-
// the activator, so we can't do much more about it than that.)
191-
//
192-
// (drt)
193244
var resolveParameterArray = MapParameters(creatorParams, pm);
194-
var resolveCall = makeResolveCall(activatorContextParam, resolveParameterArray);
245+
246+
// new ResolveRequest(svc, sr, [...], null) — 'svc' and 'sr' are runtime parameters.
247+
var newExpression = Expression.New(
248+
_requestConstructor,
249+
serviceParam,
250+
serviceRegistrationParam,
251+
Expression.NewArrayInit(typeof(Parameter), resolveParameterArray),
252+
Expression.Constant(null, typeof(IComponentRegistration)));
253+
254+
// c.ResolveComponent(new ResolveRequest(...))
255+
var resolveCall = Expression.Call(
256+
activatorContextParam,
257+
ReflectionExtensions.GetMethod<IComponentContext>(cc => cc.ResolveComponent(
258+
new ResolveRequest(default!, default, default(Parameter[])!, default))),
259+
newExpression);
260+
195261
resolveCast = Expression.Convert(resolveCall, invoke.ReturnType);
196262
}
197263

198-
// ([dps]*) => c.Resolve(service, [new Parameter(name, dps)]*)
264+
// ([dps]*) => (drt)c.ResolveComponent(new ResolveRequest(svc, sr, [...]))
199265
var creator = Expression.Lambda(delegateType, resolveCast, creatorParams);
200266

201-
// (c, p) => (
202-
var activator = Expression.Lambda<Func<IComponentContext, IEnumerable<Parameter>, Delegate>>(creator, activatorParams);
267+
// (svc, sr, c, p) => ([dps]*) => ...
268+
var activator = Expression.Lambda<Func<Service, ServiceRegistration, IComponentContext, IEnumerable<Parameter>, Delegate>>(
269+
creator,
270+
serviceParam,
271+
serviceRegistrationParam,
272+
activatorContextParam,
273+
activatorParamsParam);
203274

204275
return activator.Compile();
205276
}

0 commit comments

Comments
 (0)