Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
2 changes: 2 additions & 0 deletions bench/Autofac.Benchmarks/BenchmarkSet.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ public static class BenchmarkSet
public static readonly Type[] All =
{
typeof(ChildScopeResolveBenchmark),
typeof(ContainerBuildBenchmark),
typeof(ContainerBuildInheritedMembersBenchmark),
typeof(ConcurrencyBenchmark),
typeof(ConcurrencyNestedScopeBenchmark),
typeof(KeyedGenericBenchmark),
Expand Down
44 changes: 44 additions & 0 deletions bench/Autofac.Benchmarks/ContainerBuildBenchmark.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// Copyright (c) Autofac Project. All rights reserved.
// Licensed under the MIT License. See LICENSE in the project root for license information.

namespace Autofac.Benchmarks;

/// <summary>
/// Measures container build (registration) time, isolated from any resolve activity.
/// </summary>
/// <remarks>
/// Building a container constructs a
/// <see cref="Autofac.Core.Activators.Reflection.ReflectionActivator"/> for each
/// reflection-based registration, which inspects the implementation type's members.
/// This benchmark guards that per-registration cost as the registration count grows.
/// </remarks>
public class ContainerBuildBenchmark
{
[Params(1000, 5000)]
public int RegistrationCount
{
get; set;
}

[Benchmark]
public void Build()
{
var builder = new ContainerBuilder();

for (var i = 0; i < RegistrationCount; i++)
{
builder.RegisterType<Component>().As<IComponent>();
}

using var container = builder.Build();
GC.KeepAlive(container);
}

private interface IComponent
{
}

private sealed class Component : IComponent
{
}
}
260 changes: 260 additions & 0 deletions bench/Autofac.Benchmarks/ContainerBuildInheritedMembersBenchmark.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,260 @@
// Copyright (c) Autofac Project. All rights reserved.
// Licensed under the MIT License. See LICENSE in the project root for license information.

using Autofac.Core;

namespace Autofac.Benchmarks;

/// <summary>
/// Measures container build (registration) time for many <em>distinct</em> implementation
/// types that <em>share a base class</em> exposing many inherited writable properties.
/// </summary>
/// <remarks>
/// <para>
/// <see cref="Autofac.Core.Activators.Reflection.ReflectionActivator"/> scans each
/// implementation type for <c>[ServiceKey]</c> on constructor parameters and properties.
/// Scanning <em>inherited</em> members from the derived type yields
/// <see cref="System.Reflection.PropertyInfo"/> objects whose <c>ReflectedType</c> is the
/// derived type, so a per-member attribute cache misses on every (derived type x inherited
/// member) pair - O(types x inherited members) of attribute reflection during registration.
/// Scanning declared-only members per type and recursing into the base type collapses that
/// to O(distinct declared members).
/// </para>
/// <para>
/// This benchmark exists because <see cref="ContainerBuildBenchmark"/> cannot detect that
/// cost: it registers a single sealed type with no properties and no base class, so there
/// are no inherited members to rescan, and the per-type memoization makes the scan a one-off
/// regardless of registration count. The two <c>TypeCount</c> values are the point of this
/// benchmark - a regression shows up as super-linear growth between them.
/// </para>
/// <para>
/// No <c>[ServiceKey]</c> attribute appears anywhere below, deliberately: the scan cost is
/// paid by every reflection-based registration whether or not the feature is used.
/// </para>
/// </remarks>
public class ContainerBuildInheritedMembersBenchmark
{
private const int InheritedPropertyCount = 20;

private List<Type> _implementationTypes = new();

[Params(250, 1000)]
public int TypeCount
{
get; set;
}

[GlobalSetup]
public void Setup()
{
_implementationTypes = DistinctDerivedTypes(TypeCount);

// Guard the fixture itself: the benchmark is only meaningful while the registered
// types really are distinct and really do inherit a wide set of writable properties.
if (_implementationTypes.Distinct().Count() != TypeCount)
{
throw new InvalidOperationException("Implementation types are not distinct.");
}

var writableInheritedProperties = typeof(WideBase).GetProperties().Count(property => property.CanWrite);
if (writableInheritedProperties != InheritedPropertyCount)
{
throw new InvalidOperationException(
$"Expected {InheritedPropertyCount} writable properties on {nameof(WideBase)} but found {writableInheritedProperties}.");
}
}

[Benchmark]
public void Build()
{
// The [ServiceKey] scan caches live in the process-wide ReflectionCacheSet.Shared and
// are not cleared on container build (their usage includes resolution), so without
// this reset only the first invocation would scan anything and BenchmarkDotNet's
// steady-state measurement would report a warm no-op. Clearing here - rather than in
// an [IterationSetup] - keeps the reset inside the measured region whatever
// invocation count BenchmarkDotNet picks, and avoids attaching a job attribute that
// would compose badly with the jobs Program.cs configures. The clear itself is
// proportional to the entries this benchmark created, and is negligible next to the
// scan it forces.
Comment thread
tillig marked this conversation as resolved.
Outdated
ReflectionCacheSet.Shared.Clear();

var builder = new ContainerBuilder();

foreach (var implementationType in _implementationTypes)
{
builder.RegisterType(implementationType);
}

using var container = builder.Build();
GC.KeepAlive(container);
}

/// <summary>
/// Produces <paramref name="count"/> distinct closed <see cref="Derived{T}"/> types, all
/// inheriting <see cref="WideBase"/>. The index is encoded as a fixed-depth nesting of
/// <see cref="Wrap{TA, TB}"/> over two markers, which yields distinct type arguments
/// without a hand-written type declaration per registration. Nesting depth stays at
/// log2(count), so type names never get deep enough for name construction to skew the
/// measurement.
/// </summary>
/// <param name="count">The number of distinct types to produce.</param>
/// <returns>The distinct closed implementation types.</returns>
private static List<Type> DistinctDerivedTypes(int count)
{
var bits = 1;
while (1 << bits < count)
{
bits++;
}

var types = new List<Type>(count);

for (var i = 0; i < count; i++)
{
var argument = typeof(Zero);

for (var bit = 0; bit < bits; bit++)
{
var next = ((i >> bit) & 1) == 1 ? typeof(One) : typeof(Zero);
argument = typeof(Wrap<,>).MakeGenericType(argument, next);
}

types.Add(typeof(Derived<>).MakeGenericType(argument));
}

return types;
}

/// <summary>
/// Stands in for a framework or domain base class with many injectable properties; every
/// registered type inherits these writable members.
/// </summary>
private class WideBase
{
public string? Property01
{
get; set;
}

public string? Property02
{
get; set;
}

public string? Property03
{
get; set;
}

public string? Property04
{
get; set;
}

public string? Property05
{
get; set;
}

public string? Property06
{
get; set;
}

public string? Property07
{
get; set;
}

public string? Property08
{
get; set;
}

public string? Property09
{
get; set;
}

public string? Property10
{
get; set;
}

public string? Property11
{
get; set;
}

public string? Property12
{
get; set;
}

public string? Property13
{
get; set;
}

public string? Property14
{
get; set;
}

public string? Property15
{
get; set;
}

public string? Property16
{
get; set;
}

public string? Property17
{
get; set;
}

public string? Property18
{
get; set;
}

public string? Property19
{
get; set;
}

public string? Property20
{
get; set;
}
}

/// <summary>
/// The registered implementation type. Declares no members of its own, so all of
/// <see cref="WideBase"/>'s properties reach it by inheritance.
/// </summary>
/// <typeparam name="T">Marker type argument that makes each closed type distinct.</typeparam>
private sealed class Derived<T> : WideBase
{
}

private sealed class Zero
{
}

private sealed class One
{
}

/// <summary>
/// Combines two marker types so an index can be encoded as a nesting of them.
/// </summary>
/// <typeparam name="TA">First marker.</typeparam>
/// <typeparam name="TB">Second marker.</typeparam>
private sealed class Wrap<TA, TB>
{
}
}
46 changes: 36 additions & 10 deletions src/Autofac/Core/Activators/Reflection/ReflectionActivator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,14 +52,7 @@ public ReflectionActivator(
}

_implementationType = implementationType;

// The cache key is the (already annotated) implementation type; the factory
// ignores the dictionary's unannotated key parameter and reads the annotated
// local instead, so the [DynamicallyAccessedMembers] contract flows correctly
// into UsesServiceKeyAttribute.
_requiresServiceKeyParameter = ReflectionCacheSet.Shared.Internal.ServiceKeyUsageByType.GetOrAdd(
_implementationType,
_ => UsesServiceKeyAttribute(implementationType));
_requiresServiceKeyParameter = UsesServiceKeyAttributeCached(implementationType);
ConstructorFinder = constructorFinder ?? throw new ArgumentNullException(nameof(constructorFinder));
ConstructorSelector = constructorSelector ?? throw new ArgumentNullException(nameof(constructorSelector));
_configuredProperties = configuredProperties.ToArray();
Expand Down Expand Up @@ -133,6 +126,10 @@ public void ConfigurePipeline(IComponentRegistryServices componentRegistryServic
"Trimming",
"IL2070:UnrecognizedReflectionPattern",
Justification = "This is a best-effort scan for [ServiceKey] across all constructors and properties, including non-public ones. Autofac's trim/AOT contract preserves only public constructors and properties (see ActivatorMemberTypes); if a non-public member is trimmed away, it simply is not found here, which matches the documented behavior that non-public activation is not trim/AOT-safe. No public member that drives activation is missed.")]
[UnconditionalSuppressMessage(
"Trimming",
"IL2072:UnrecognizedReflectionPattern",
Justification = "Recurses into BaseType to inspect declared members level-by-level. BaseType members are preserved via the derived type's ActivatorMemberTypes annotation; if a base member is trimmed it simply is not found, matching the documented best-effort behavior.")]
private static bool UsesServiceKeyAttribute([DynamicallyAccessedMembers(ActivatorMemberTypes.ActivatedType)] Type implementationType)
{
// Intentionally not picky about _which_ constructor or property has the
Expand All @@ -141,7 +138,17 @@ private static bool UsesServiceKeyAttribute([DynamicallyAccessedMembers(Activato
// where we "may or may not need it." If you mark a property with the
// attribute but never inject properties, we'll still provide the
// parameter "just in case" you change your mind at runtime.
foreach (var constructor in implementationType.GetConstructors(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic))
//
// We scan DeclaredOnly members and recurse into the base type (memoized per
// type via ServiceKeyUsageByType). This is critical for performance: scanning
// *inherited* members via GetProperties on a derived type yields PropertyInfo
// objects whose ReflectedType is the derived type, so the per-member attribute
// cache misses on every (derived type x inherited member) pair - O(types x
// inherited members) of attribute reflection. Scanning DeclaredOnly anchors each
// member to its declaring type so the cache is shared across all derived types.
Comment thread
tillig marked this conversation as resolved.
const BindingFlags DeclaredMembers = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.DeclaredOnly;

foreach (var constructor in implementationType.GetConstructors(DeclaredMembers))
{
foreach (var parameter in constructor.GetParameters())
{
Expand All @@ -152,17 +159,36 @@ private static bool UsesServiceKeyAttribute([DynamicallyAccessedMembers(Activato
}
}

foreach (var property in implementationType.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic))
foreach (var property in implementationType.GetProperties(DeclaredMembers))
{
if (property.CanWrite && ServiceKeyAttributeCache.PropertyHasServiceKey(property))
{
return true;
}
}

var baseType = implementationType.BaseType;
if (baseType is not null && baseType != typeof(object))
{
// IL2072 suppressed on this method: baseType is unannotated but its members
// are preserved transitively via the derived type's annotation.
return UsesServiceKeyAttributeCached(baseType);
}

return false;
}

// Memoizes the per-type result so each base type in a hierarchy is scanned once,
// regardless of how many derived types share it. The factory captures the annotated
// 'type' local (the dictionary key parameter is ignored) so the DynamicallyAccessedMembers
// contract flows without an unannotated-to-annotated assignment.
#pragma warning disable S6612 // Intentionally capture the annotated 'type' local (not the unannotated lambda key) so the DynamicallyAccessedMembers contract flows and trimming stays satisfied.
private static bool UsesServiceKeyAttributeCached([DynamicallyAccessedMembers(ActivatorMemberTypes.ActivatedType)] Type type)
=> ReflectionCacheSet.Shared.Internal.ServiceKeyUsageByType.GetOrAdd(
type,
_ => UsesServiceKeyAttribute(type));
#pragma warning restore S6612
Comment thread
tillig marked this conversation as resolved.
Outdated

private void UseSingleConstructorActivation(IResolvePipelineBuilder pipelineBuilder, ConstructorBinder singleConstructor)
{
if (singleConstructor.ParameterCount == 0)
Expand Down
Loading
Loading