From 2e7bb772af96ee44693022731c74a0595b74e0f9 Mon Sep 17 00:00:00 2001 From: Travis Illig Date: Tue, 17 Feb 2026 11:30:51 -0800 Subject: [PATCH 01/15] Update benchmark project with nullable annotations, ability to run comparisons. --- .../Autofac.BenchmarkProfiling.csproj | 4 +- .../Autofac.Benchmarks.csproj | 22 +++-- .../ChildScopeResolveBenchmark.cs | 2 +- .../Decorators/DecoratorBenchmarkBase.cs | 9 +- .../DeepGraphResolveBenchmark.cs | 2 +- .../EnumerableResolveBenchmark.cs | 2 +- .../LambdaResolveBenchmark.cs | 2 +- .../MultiConstructorBenchmark.cs | 2 +- .../OpenGenericBenchmark.cs | 2 +- bench/Autofac.Benchmarks/Program.cs | 89 ++++++++++++++++++- .../PropertyInjectionBenchmark.cs | 14 +-- .../RequiredPropertyBenchmark.cs | 2 +- .../RootContainerResolveBenchmark.cs | 2 +- 13 files changed, 123 insertions(+), 31 deletions(-) diff --git a/bench/Autofac.BenchmarkProfiling/Autofac.BenchmarkProfiling.csproj b/bench/Autofac.BenchmarkProfiling/Autofac.BenchmarkProfiling.csproj index 997a45d36..f79ab0102 100644 --- a/bench/Autofac.BenchmarkProfiling/Autofac.BenchmarkProfiling.csproj +++ b/bench/Autofac.BenchmarkProfiling/Autofac.BenchmarkProfiling.csproj @@ -1,7 +1,7 @@ Exe - net8.0 + net10.0 $([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture) ../../build/Test.ruleset AllEnabledByDefault @@ -10,7 +10,7 @@ enable - + diff --git a/bench/Autofac.Benchmarks/Autofac.Benchmarks.csproj b/bench/Autofac.Benchmarks/Autofac.Benchmarks.csproj index 8fd924eb6..426a2af52 100644 --- a/bench/Autofac.Benchmarks/Autofac.Benchmarks.csproj +++ b/bench/Autofac.Benchmarks/Autofac.Benchmarks.csproj @@ -1,20 +1,21 @@  - net8.0 + net10.0 $(NoWarn);CS1591 Exe false $([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture) + latest + enable true - ../../Autofac.snk - true - true - true ../../build/Test.ruleset - AllEnabledByDefault true - false + AllEnabledByDefault enable + false + true + true + true @@ -25,15 +26,18 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + + + + diff --git a/bench/Autofac.Benchmarks/ChildScopeResolveBenchmark.cs b/bench/Autofac.Benchmarks/ChildScopeResolveBenchmark.cs index 3325cd120..4e65204e8 100644 --- a/bench/Autofac.Benchmarks/ChildScopeResolveBenchmark.cs +++ b/bench/Autofac.Benchmarks/ChildScopeResolveBenchmark.cs @@ -5,7 +5,7 @@ namespace Autofac.Benchmarks; public class ChildScopeResolveBenchmark { - private IContainer _container; + private IContainer _container = default!; [Benchmark] public void Resolve() diff --git a/bench/Autofac.Benchmarks/Decorators/DecoratorBenchmarkBase.cs b/bench/Autofac.Benchmarks/Decorators/DecoratorBenchmarkBase.cs index f3ab9657d..42ed50f32 100644 --- a/bench/Autofac.Benchmarks/Decorators/DecoratorBenchmarkBase.cs +++ b/bench/Autofac.Benchmarks/Decorators/DecoratorBenchmarkBase.cs @@ -1,11 +1,14 @@ // Copyright (c) Autofac Project. All rights reserved. // Licensed under the MIT License. See LICENSE in the project root for license information. +using Microsoft.CodeAnalysis.CSharp.Syntax; + namespace Autofac.Benchmarks.Decorators; public abstract class DecoratorBenchmarkBase + where TCommandHandler : notnull { - protected IContainer Container { get; set; } + protected IContainer Container { get; set; } = default!; [Benchmark(Baseline = true)] public virtual void Baseline() @@ -25,7 +28,7 @@ public virtual void ResolveEnumerableT(int repetitions) using (var scope = Container.BeginLifetimeScope()) { var iteration = 0; - object item = null; + object? item = null; while (iteration++ < repetitions) { item = scope.Resolve>(); @@ -43,7 +46,7 @@ public virtual void ResolveT(int repetitions) using (var scope = Container.BeginLifetimeScope()) { var iteration = 0; - object item = null; + object? item = null; while (iteration++ < repetitions) { item = scope.Resolve(); diff --git a/bench/Autofac.Benchmarks/DeepGraphResolveBenchmark.cs b/bench/Autofac.Benchmarks/DeepGraphResolveBenchmark.cs index b1cfc5593..496ebabb8 100644 --- a/bench/Autofac.Benchmarks/DeepGraphResolveBenchmark.cs +++ b/bench/Autofac.Benchmarks/DeepGraphResolveBenchmark.cs @@ -8,7 +8,7 @@ namespace Autofac.Benchmarks; /// public class DeepGraphResolveBenchmark { - private IContainer _container; + private IContainer _container = default!; [GlobalSetup] public void Setup() diff --git a/bench/Autofac.Benchmarks/EnumerableResolveBenchmark.cs b/bench/Autofac.Benchmarks/EnumerableResolveBenchmark.cs index 90484eb71..4948672a4 100644 --- a/bench/Autofac.Benchmarks/EnumerableResolveBenchmark.cs +++ b/bench/Autofac.Benchmarks/EnumerableResolveBenchmark.cs @@ -5,7 +5,7 @@ namespace Autofac.Benchmarks; public class EnumerableResolveBenchmark { - private IContainer _container; + private IContainer _container = default!; [GlobalSetup] public void Setup() diff --git a/bench/Autofac.Benchmarks/LambdaResolveBenchmark.cs b/bench/Autofac.Benchmarks/LambdaResolveBenchmark.cs index e8ffa6845..b3413600e 100644 --- a/bench/Autofac.Benchmarks/LambdaResolveBenchmark.cs +++ b/bench/Autofac.Benchmarks/LambdaResolveBenchmark.cs @@ -5,7 +5,7 @@ namespace Autofac.Benchmarks; public class LambdaResolveBenchmark { - private IContainer _container; + private IContainer _container = default!; [GlobalSetup] public void Setup() diff --git a/bench/Autofac.Benchmarks/MultiConstructorBenchmark.cs b/bench/Autofac.Benchmarks/MultiConstructorBenchmark.cs index 66cf3ee06..dfbdff2ca 100644 --- a/bench/Autofac.Benchmarks/MultiConstructorBenchmark.cs +++ b/bench/Autofac.Benchmarks/MultiConstructorBenchmark.cs @@ -5,7 +5,7 @@ namespace Autofac.Benchmarks; public class MultiConstructorBenchmark { - private IContainer _container; + private IContainer _container = default!; [GlobalSetup] public void Setup() diff --git a/bench/Autofac.Benchmarks/OpenGenericBenchmark.cs b/bench/Autofac.Benchmarks/OpenGenericBenchmark.cs index 0a1ccd5d9..83717c4cf 100644 --- a/bench/Autofac.Benchmarks/OpenGenericBenchmark.cs +++ b/bench/Autofac.Benchmarks/OpenGenericBenchmark.cs @@ -5,7 +5,7 @@ namespace Autofac.Benchmarks; public class OpenGenericBenchmark { - private IContainer _container; + private IContainer _container = default!; [GlobalSetup] public void Setup() diff --git a/bench/Autofac.Benchmarks/Program.cs b/bench/Autofac.Benchmarks/Program.cs index 2d8f5d77d..23ad4eb9e 100644 --- a/bench/Autofac.Benchmarks/Program.cs +++ b/bench/Autofac.Benchmarks/Program.cs @@ -1,12 +1,97 @@ // Copyright (c) Autofac Project. All rights reserved. // Licensed under the MIT License. See LICENSE in the project root for license information. +using BenchmarkDotNet.Jobs; using BenchmarkDotNet.Running; namespace Autofac.Benchmarks; internal static class Program { - internal static void Main(string[] args) => - new BenchmarkSwitcher(BenchmarkSet.All).Run(args, new BenchmarkConfig()); + internal static void Main(string[] args) + { + ArgumentNullException.ThrowIfNull(args); + var (filteredArgs, baselineVersion) = ExtractBaselineVersion(args); + + // Usage: + // + // Just run the benchmark with the source code version of the project: + // dotnet run -c Release -p bench/Autofac.Benchmarks + // + // Run the benchmark comparing the source code version to a specific package version: + // dotnet run -c Release -p bench/Autofac.Benchmarks -- --baseline-version 9.0.0 + var config = new BenchmarkConfig(); + + config.AddJob( + Job.InProcess + .WithId("Source")); + + if (!string.IsNullOrWhiteSpace(baselineVersion)) + { + config.AddJob( + Job.Default + .WithId($"Package-{baselineVersion}") + .AsBaseline() + .WithMsBuildArguments( + "/p:UseProjectReference=false", + $"/p:BaselinePackageVersion={baselineVersion}")); + } + + new BenchmarkSwitcher(BenchmarkSet.All).Run(filteredArgs, config); + } + + private static (string[] RemainingArgs, string? BaselineVersion) ExtractBaselineVersion(string[] args) + { + var forwarded = new List(args.Length); + string? baseline = null; + + for (var i = 0; i < args.Length; i++) + { + var arg = args[i]; + if (TryMatchBaselineArg(arg, out var inlineVersion)) + { + if (!string.IsNullOrWhiteSpace(inlineVersion)) + { + baseline = inlineVersion; + continue; + } + + if (i + 1 >= args.Length) + { + throw new ArgumentException("Missing version value for baseline argument.", nameof(args)); + } + + baseline = args[++i]; + continue; + } + + forwarded.Add(arg); + } + + return (forwarded.ToArray(), baseline); + } + + private static bool TryMatchBaselineArg(string arg, out string? valueFromAssignment) + { + valueFromAssignment = null; + + static bool Matches(string candidate) => + candidate.Equals("--baseline-version", StringComparison.OrdinalIgnoreCase) || + candidate.Equals("--baselineVersion", StringComparison.OrdinalIgnoreCase); + + var equalsIndex = arg.AsSpan().IndexOf('='); + if (equalsIndex >= 0) + { + var prefix = arg[..equalsIndex]; + if (Matches(prefix)) + { + valueFromAssignment = arg[(equalsIndex + 1)..]; + return true; + } + + return false; + } + + return Matches(arg); + } } diff --git a/bench/Autofac.Benchmarks/PropertyInjectionBenchmark.cs b/bench/Autofac.Benchmarks/PropertyInjectionBenchmark.cs index 26f2e8a4c..a98c3ee3d 100644 --- a/bench/Autofac.Benchmarks/PropertyInjectionBenchmark.cs +++ b/bench/Autofac.Benchmarks/PropertyInjectionBenchmark.cs @@ -5,7 +5,7 @@ namespace Autofac.Benchmarks; public class PropertyInjectionBenchmark { - private IContainer _container; + private IContainer _container = default!; [GlobalSetup] public void Setup() @@ -30,29 +30,29 @@ public void Resolve() internal class A { - public B1 B1 { get; set; } + public B1? B1 { get; set; } - public B2 B2 { get; set; } + public B2? B2 { get; set; } } internal class B1 { - public C1 C1 { get; set; } + public C1? C1 { get; set; } } internal class B2 { - public C2 C2 { get; set; } + public C2? C2 { get; set; } } internal class C1 { - public D1 D1 { get; set; } + public D1? D1 { get; set; } } internal class C2 { - public D2 D2 { get; set; } + public D2? D2 { get; set; } } internal class D1 diff --git a/bench/Autofac.Benchmarks/RequiredPropertyBenchmark.cs b/bench/Autofac.Benchmarks/RequiredPropertyBenchmark.cs index a228e2e2a..a57544e67 100644 --- a/bench/Autofac.Benchmarks/RequiredPropertyBenchmark.cs +++ b/bench/Autofac.Benchmarks/RequiredPropertyBenchmark.cs @@ -5,7 +5,7 @@ namespace Autofac.Benchmarks; public class RequiredPropertyBenchmark { - private IContainer _container; + private IContainer _container = default!; [GlobalSetup] public void Setup() diff --git a/bench/Autofac.Benchmarks/RootContainerResolveBenchmark.cs b/bench/Autofac.Benchmarks/RootContainerResolveBenchmark.cs index 402b37002..ba98832f0 100644 --- a/bench/Autofac.Benchmarks/RootContainerResolveBenchmark.cs +++ b/bench/Autofac.Benchmarks/RootContainerResolveBenchmark.cs @@ -8,7 +8,7 @@ namespace Autofac.Benchmarks; /// public class RootContainerResolveBenchmark { - private IContainer _container; + private IContainer _container = default!; [GlobalSetup] public void Setup() From 210a52413e321204ee5ef2813391aebb1aaafa1c Mon Sep 17 00:00:00 2001 From: Travis Illig Date: Tue, 17 Feb 2026 11:42:09 -0800 Subject: [PATCH 02/15] Correct the benchmark instructions. --- bench/Autofac.Benchmarks/Program.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bench/Autofac.Benchmarks/Program.cs b/bench/Autofac.Benchmarks/Program.cs index 23ad4eb9e..2ea80f06a 100644 --- a/bench/Autofac.Benchmarks/Program.cs +++ b/bench/Autofac.Benchmarks/Program.cs @@ -16,10 +16,10 @@ internal static void Main(string[] args) // Usage: // // Just run the benchmark with the source code version of the project: - // dotnet run -c Release -p bench/Autofac.Benchmarks + // dotnet run -c Release --project bench/Autofac.Benchmarks // // Run the benchmark comparing the source code version to a specific package version: - // dotnet run -c Release -p bench/Autofac.Benchmarks -- --baseline-version 9.0.0 + // dotnet run -c Release --project bench/Autofac.Benchmarks -- --baseline-version 9.0.0 var config = new BenchmarkConfig(); config.AddJob( From 4979915979f09b02188b5f80f93fac965e3010b0 Mon Sep 17 00:00:00 2001 From: Travis Illig Date: Wed, 18 Feb 2026 08:45:52 -0800 Subject: [PATCH 03/15] Improve comments to explain how to run benchmarks, --- bench/Autofac.Benchmarks/Program.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bench/Autofac.Benchmarks/Program.cs b/bench/Autofac.Benchmarks/Program.cs index 2ea80f06a..9e7392128 100644 --- a/bench/Autofac.Benchmarks/Program.cs +++ b/bench/Autofac.Benchmarks/Program.cs @@ -16,10 +16,10 @@ internal static void Main(string[] args) // Usage: // // Just run the benchmark with the source code version of the project: - // dotnet run -c Release --project bench/Autofac.Benchmarks + // dotnet run -c Release --project bench/Autofac.Benchmarks -- --filter *Benchmarks* // // Run the benchmark comparing the source code version to a specific package version: - // dotnet run -c Release --project bench/Autofac.Benchmarks -- --baseline-version 9.0.0 + // dotnet run -c Release --project bench/Autofac.Benchmarks -- --baseline-version 9.0.0 --filter *Benchmarks* var config = new BenchmarkConfig(); config.AddJob( From 36aea8df85ba2ae767d2e0509d630a10a6c58896 Mon Sep 17 00:00:00 2001 From: Travis Illig Date: Thu, 19 Feb 2026 09:26:07 -0800 Subject: [PATCH 04/15] Run benchmarks out of process. --- bench/Autofac.Benchmarks/Program.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bench/Autofac.Benchmarks/Program.cs b/bench/Autofac.Benchmarks/Program.cs index 9e7392128..c0e85c115 100644 --- a/bench/Autofac.Benchmarks/Program.cs +++ b/bench/Autofac.Benchmarks/Program.cs @@ -23,7 +23,7 @@ internal static void Main(string[] args) var config = new BenchmarkConfig(); config.AddJob( - Job.InProcess + Job.Default .WithId("Source")); if (!string.IsNullOrWhiteSpace(baselineVersion)) From 4307847b9fc5af1ca305cfaff7b2b493af1faa7d Mon Sep 17 00:00:00 2001 From: Travis Illig Date: Thu, 19 Feb 2026 13:13:28 -0800 Subject: [PATCH 05/15] Improved output for the benchmark profiling wrapper. --- bench/Autofac.BenchmarkProfiling/Program.cs | 58 ++++++++++++++++++++- 1 file changed, 57 insertions(+), 1 deletion(-) diff --git a/bench/Autofac.BenchmarkProfiling/Program.cs b/bench/Autofac.BenchmarkProfiling/Program.cs index cd2025b6e..6e7a56412 100644 --- a/bench/Autofac.BenchmarkProfiling/Program.cs +++ b/bench/Autofac.BenchmarkProfiling/Program.cs @@ -1,5 +1,8 @@ -using BenchmarkDotNet.Running; +using System.Diagnostics; +using System.Reflection; +using BenchmarkDotNet.Running; using BenchmarkDotNet.Toolchains.InProcess.NoEmit; +using Autofac.Core; namespace Autofac.BenchmarkProfiling; @@ -10,6 +13,11 @@ class Program { static void Main(string[] args) { + if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable("AUTOFAC_SCOPE_DIAGNOSTICS"))) + { + AppContext.SetSwitch("Autofac.ScopeIsolatedDiagnostics", true); + } + // Pick a benchmark. var availableBenchmarks = Benchmarks.BenchmarkSet.All; @@ -98,6 +106,20 @@ void workloadAction(int repeat) // Warmup. workloadAction(100); + if (int.TryParse(Environment.GetEnvironmentVariable("AUTOFAC_MEASURE_ITERATIONS"), out var measurementIterations) && + measurementIterations > 0) + { + var sw = Stopwatch.StartNew(); + workloadAction(measurementIterations); + sw.Stop(); + var perIteration = sw.Elapsed.TotalMilliseconds / measurementIterations; + Console.WriteLine( + "[Profiling] Duration: {0} iterations took {1:F2} ms (avg {2:F4} ms)", + measurementIterations, + sw.Elapsed.TotalMilliseconds, + perIteration); + } + // Now start a new thread. var runThread = new Thread(new ThreadStart(() => { @@ -112,6 +134,8 @@ void workloadAction(int repeat) runThread.Join(); cleanupAction.InvokeSingle(); + + LogScopeDiagnosticsIfEnabled(); } private static void PrintBenchmarks(Type[] availableBenchmarks) @@ -137,4 +161,36 @@ private static void PrintCases(BenchmarkRunInfo benchRunInfo) } } } + + private static void LogScopeDiagnosticsIfEnabled() + { + if (!AppContext.TryGetSwitch("Autofac.ScopeIsolatedDiagnostics", out var enabled) || !enabled) + { + return; + } + + var diagnosticsType = typeof(IComponentRegistry).Assembly.GetType("Autofac.Core.Registration.ScopeIsolatedServiceDiagnostics"); + var snapshotProperty = diagnosticsType?.GetProperty( + "Snapshot", + BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static); + + if (snapshotProperty?.GetValue(null) is object snapshot) + { + var cacheHits = (long)(snapshot.GetType().GetProperty("CacheHits")?.GetValue(snapshot) ?? 0L); + var cacheMisses = (long)(snapshot.GetType().GetProperty("CacheMisses")?.GetValue(snapshot) ?? 0L); + var cacheAdds = (long)(snapshot.GetType().GetProperty("CacheAdds")?.GetValue(snapshot) ?? 0L); + var cacheRemovals = (long)(snapshot.GetType().GetProperty("CacheRemovals")?.GetValue(snapshot) ?? 0L); + var cachedInitializations = (long)(snapshot.GetType().GetProperty("CachedInitializations")?.GetValue(snapshot) ?? 0L); + var discardedInfos = (long)(snapshot.GetType().GetProperty("ServiceInfoDiscarded")?.GetValue(snapshot) ?? 0L); + + Console.WriteLine( + "[Profiling] Scope cache stats -> Hits={0}, Misses={1}, Adds={2}, Removes={3}, CachedInit={4}, Discarded={5}", + cacheHits, + cacheMisses, + cacheAdds, + cacheRemovals, + cachedInitializations, + discardedInfos); + } + } } From 1f3fbd652080c47ec0d7a326a8da3c7703979ab5 Mon Sep 17 00:00:00 2001 From: Travis Illig Date: Thu, 19 Feb 2026 13:14:13 -0800 Subject: [PATCH 06/15] Add logs to benchmarks to explain what was executed. --- bench/Autofac.Benchmarks/Program.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/bench/Autofac.Benchmarks/Program.cs b/bench/Autofac.Benchmarks/Program.cs index c0e85c115..ad3b56edb 100644 --- a/bench/Autofac.Benchmarks/Program.cs +++ b/bench/Autofac.Benchmarks/Program.cs @@ -37,6 +37,7 @@ internal static void Main(string[] args) $"/p:BaselinePackageVersion={baselineVersion}")); } + Console.WriteLine($"Benchmark types configured: {BenchmarkSet.All.Length}"); new BenchmarkSwitcher(BenchmarkSet.All).Run(filteredArgs, config); } From 58214f5cdbec333a65af5fc96f44bc116eabf39a Mon Sep 17 00:00:00 2001 From: Travis Illig Date: Thu, 26 Feb 2026 14:16:36 -0800 Subject: [PATCH 07/15] Add metrics instrumentation --- .../Reflection/AutowiringPropertyInjector.cs | 19 ++ .../Reflection/ReflectionActivator.cs | 37 +++ src/Autofac/Core/Lifetime/LifetimeScope.cs | 46 ++- .../DefaultRegisteredServicesTracker.cs | 13 +- .../ActivatorErrorHandlingMiddleware.cs | 45 ++- .../CircularDependencyDetectorMiddleware.cs | 21 ++ .../Middleware/CoreEventMiddleware.cs | 18 +- .../Middleware/DelegateMiddleware.cs | 18 +- .../Middleware/DisposalTrackingMiddleware.cs | 27 +- .../RegistrationPipelineInvokeMiddleware.cs | 18 +- .../Middleware/ScopeSelectionMiddleware.cs | 27 +- .../Resolving/Middleware/SharingMiddleware.cs | 27 +- .../Middleware/StartableMiddleware.cs | 27 +- src/Autofac/Diagnostics/AutofacMetrics.cs | 270 ++++++++++++++++++ src/Autofac/Diagnostics/ValueStopwatch.cs | 46 +++ .../CollectionRegistrationSource.cs | 27 +- .../Decorators/DecoratorMiddleware.cs | 27 +- 17 files changed, 678 insertions(+), 35 deletions(-) create mode 100644 src/Autofac/Diagnostics/AutofacMetrics.cs create mode 100644 src/Autofac/Diagnostics/ValueStopwatch.cs diff --git a/src/Autofac/Core/Activators/Reflection/AutowiringPropertyInjector.cs b/src/Autofac/Core/Activators/Reflection/AutowiringPropertyInjector.cs index ef1a9160c..93ff3caac 100644 --- a/src/Autofac/Core/Activators/Reflection/AutowiringPropertyInjector.cs +++ b/src/Autofac/Core/Activators/Reflection/AutowiringPropertyInjector.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. See LICENSE in the project root for license information. using System.Reflection; +using Autofac.Diagnostics; using Autofac.Util; namespace Autofac.Core.Activators.Reflection; @@ -49,11 +50,18 @@ public static void InjectProperties(IComponentContext context, object instance, } var resolveParameters = parameters as Parameter[] ?? parameters.ToArray(); + var recordMetrics = AutofacMetrics.MetricsEnabled; + ValueStopwatch instrumentationTimer = default; + if (recordMetrics) + { + instrumentationTimer = ValueStopwatch.StartNew(); + } var injectablePropertiesCache = ReflectionCacheSet.Shared.Internal.AutowiringInjectableProperties; var instanceType = instance.GetType(); var injectableProperties = injectablePropertiesCache.GetOrAdd(instanceType, type => GetInjectableProperties(type).ToList()); + var injectedProperties = 0; for (var index = 0; index < injectableProperties.Count; index++) { @@ -77,6 +85,7 @@ public static void InjectProperties(IComponentContext context, object instance, { var setter = ReflectionCacheSet.Shared.Internal.AutowiringPropertySetters.GetOrAdd(property, MakeFastPropertySetter); setter(instance, valueProvider!()); + injectedProperties++; continue; } @@ -86,8 +95,18 @@ public static void InjectProperties(IComponentContext context, object instance, { var setter = ReflectionCacheSet.Shared.Internal.AutowiringPropertySetters.GetOrAdd(property, MakeFastPropertySetter); setter(instance, propertyValue); + injectedProperties++; } } + + if (recordMetrics) + { + AutofacMetrics.RecordPropertyInjection( + instanceType, + injectableProperties.Count, + injectedProperties, + instrumentationTimer.ElapsedTicks); + } } private static IEnumerable GetInjectableProperties(Type instanceType) diff --git a/src/Autofac/Core/Activators/Reflection/ReflectionActivator.cs b/src/Autofac/Core/Activators/Reflection/ReflectionActivator.cs index 013723ad5..8cb01e1bb 100644 --- a/src/Autofac/Core/Activators/Reflection/ReflectionActivator.cs +++ b/src/Autofac/Core/Activators/Reflection/ReflectionActivator.cs @@ -5,6 +5,7 @@ using System.Reflection; using System.Text; using Autofac.Core.Resolving.Pipeline; +using Autofac.Diagnostics; using Autofac.Util; namespace Autofac.Core.Activators.Reflection; @@ -218,12 +219,24 @@ private void UseSingleConstructorActivation(IResolvePipelineBuilder pipelineBuil { CheckNotDisposed(); + var recordMetrics = AutofacMetrics.MetricsEnabled; + ValueStopwatch instrumentationTimer = default; + if (recordMetrics) + { + instrumentationTimer = ValueStopwatch.StartNew(); + } + var instance = boundConstructor.Instantiate(); InjectProperties(instance, context, boundConstructor, GetAllParameters(context.Parameters)); context.Instance = instance; + if (recordMetrics) + { + AutofacMetrics.RecordReflectionActivation(_implementationType, instrumentationTimer.ElapsedTicks); + } + next(context); }); } @@ -233,6 +246,13 @@ private void UseSingleConstructorActivation(IResolvePipelineBuilder pipelineBuil { CheckNotDisposed(); + var recordMetrics = AutofacMetrics.MetricsEnabled; + ValueStopwatch instrumentationTimer = default; + if (recordMetrics) + { + instrumentationTimer = ValueStopwatch.StartNew(); + } + var prioritizedParameters = GetAllParameters(context.Parameters); var bound = singleConstructor.Bind(prioritizedParameters, context); @@ -248,6 +268,11 @@ private void UseSingleConstructorActivation(IResolvePipelineBuilder pipelineBuil context.Instance = instance; + if (recordMetrics) + { + AutofacMetrics.RecordReflectionActivation(_implementationType, instrumentationTimer.ElapsedTicks); + } + next(context); }); } @@ -277,6 +302,13 @@ private object ActivateInstance(IComponentContext context, IEnumerable creator) return tempResult; } - lock (_synchRoot) + var instrumentationDetail = AutofacMetrics.MetricsEnabled ? $"Tag:{Tag},Id:{id}" : null; + var lockTaken = false; + try { + if (AutofacMetrics.MetricsEnabled) + { + var wait = ValueStopwatch.StartNew(); + Monitor.Enter(_synchRoot, ref lockTaken); + AutofacMetrics.RecordLockContention("LifetimeScopeSharedInstance", instrumentationDetail, wait.ElapsedTicks); + } + else + { + Monitor.Enter(_synchRoot, ref lockTaken); + } + if (_sharedInstances.TryGetValue(id, out var result)) { return result; @@ -284,6 +299,13 @@ public object CreateSharedInstance(Guid id, Func creator) return result; } + finally + { + if (lockTaken) + { + Monitor.Exit(_synchRoot); + } + } } /// @@ -306,8 +328,21 @@ public object CreateSharedInstance(Guid primaryId, Guid? qualifyingId, Func diff --git a/src/Autofac/Core/Registration/DefaultRegisteredServicesTracker.cs b/src/Autofac/Core/Registration/DefaultRegisteredServicesTracker.cs index 122082ee6..6761991c5 100644 --- a/src/Autofac/Core/Registration/DefaultRegisteredServicesTracker.cs +++ b/src/Autofac/Core/Registration/DefaultRegisteredServicesTracker.cs @@ -4,6 +4,7 @@ using System.Collections.Concurrent; using System.Runtime.CompilerServices; using Autofac.Core.Resolving.Pipeline; +using Autofac.Diagnostics; using Autofac.Util; namespace Autofac.Core.Registration; @@ -303,6 +304,7 @@ private ServiceRegistrationInfo GetInitializedServiceInfo(Service service) } var info = GetServiceInfo(service); + var instrumentationService = AutofacMetrics.MetricsEnabled ? service.ToString() : null; if (info.IsInitialized) { return info; @@ -324,7 +326,16 @@ private ServiceRegistrationInfo GetInitializedServiceInfo(Service service) var lockTaken = false; try { - Monitor.Enter(info, ref lockTaken); + if (AutofacMetrics.MetricsEnabled) + { + var wait = ValueStopwatch.StartNew(); + Monitor.Enter(info, ref lockTaken); + AutofacMetrics.RecordLockContention("Service", instrumentationService, wait.ElapsedTicks); + } + else + { + Monitor.Enter(info, ref lockTaken); + } if (info.IsInitialized) { diff --git a/src/Autofac/Core/Resolving/Middleware/ActivatorErrorHandlingMiddleware.cs b/src/Autofac/Core/Resolving/Middleware/ActivatorErrorHandlingMiddleware.cs index 2077e40cc..6ec24f0bf 100644 --- a/src/Autofac/Core/Resolving/Middleware/ActivatorErrorHandlingMiddleware.cs +++ b/src/Autofac/Core/Resolving/Middleware/ActivatorErrorHandlingMiddleware.cs @@ -1,8 +1,10 @@ // Copyright (c) Autofac Project. All rights reserved. // Licensed under the MIT License. See LICENSE in the project root for license information. +using System.Diagnostics; using System.Globalization; using Autofac.Core.Resolving.Pipeline; +using Autofac.Diagnostics; namespace Autofac.Core.Resolving.Middleware; @@ -28,23 +30,20 @@ private ActivatorErrorHandlingMiddleware() /// public void Execute(ResolveRequestContext context, Action next) { - try + if (!AutofacMetrics.MetricsEnabled) { - next(context); - - if (context.Instance is null) - { - // Exited the Activation Stage without creating an instance. - throw new DependencyResolutionException(MiddlewareMessages.ActivatorDidNotPopulateInstance); - } + ExecuteCore(context, next); + return; } - catch (ObjectDisposedException) + + var start = Stopwatch.GetTimestamp(); + try { - throw; + ExecuteCore(context, next); } - catch (Exception ex) + finally { - throw PropagateActivationException(context.Registration.Activator, ex); + AutofacMetrics.RecordMiddlewareExecution(nameof(ActivatorErrorHandlingMiddleware), Stopwatch.GetTimestamp() - start); } } @@ -67,4 +66,26 @@ private static DependencyResolutionException PropagateActivationException(IInsta result.Data[ActivatorChainExceptionData] = activatorChain; return result; } + + private static void ExecuteCore(ResolveRequestContext context, Action next) + { + try + { + next(context); + + if (context.Instance is null) + { + // Exited the Activation Stage without creating an instance. + throw new DependencyResolutionException(MiddlewareMessages.ActivatorDidNotPopulateInstance); + } + } + catch (ObjectDisposedException) + { + throw; + } + catch (Exception ex) + { + throw PropagateActivationException(context.Registration.Activator, ex); + } + } } diff --git a/src/Autofac/Core/Resolving/Middleware/CircularDependencyDetectorMiddleware.cs b/src/Autofac/Core/Resolving/Middleware/CircularDependencyDetectorMiddleware.cs index a10cd6b17..dedfe8287 100644 --- a/src/Autofac/Core/Resolving/Middleware/CircularDependencyDetectorMiddleware.cs +++ b/src/Autofac/Core/Resolving/Middleware/CircularDependencyDetectorMiddleware.cs @@ -1,9 +1,11 @@ // Copyright (c) Autofac Project. All rights reserved. // Licensed under the MIT License. See LICENSE in the project root for license information. +using System.Diagnostics; using System.Globalization; using System.Runtime.CompilerServices; using Autofac.Core.Resolving.Pipeline; +using Autofac.Diagnostics; namespace Autofac.Core.Resolving.Middleware; @@ -38,6 +40,25 @@ public CircularDependencyDetectorMiddleware(int maxResolveDepth) /// public void Execute(ResolveRequestContext context, Action next) + { + if (!AutofacMetrics.MetricsEnabled) + { + ExecuteCore(context, next); + return; + } + + var start = Stopwatch.GetTimestamp(); + try + { + ExecuteCore(context, next); + } + finally + { + AutofacMetrics.RecordMiddlewareExecution(nameof(CircularDependencyDetectorMiddleware), Stopwatch.GetTimestamp() - start); + } + } + + private void ExecuteCore(ResolveRequestContext context, Action next) { if (context.Operation is not IDependencyTrackingResolveOperation dependencyTrackingResolveOperation) { diff --git a/src/Autofac/Core/Resolving/Middleware/CoreEventMiddleware.cs b/src/Autofac/Core/Resolving/Middleware/CoreEventMiddleware.cs index 90d23e72d..1ab8434aa 100644 --- a/src/Autofac/Core/Resolving/Middleware/CoreEventMiddleware.cs +++ b/src/Autofac/Core/Resolving/Middleware/CoreEventMiddleware.cs @@ -1,7 +1,9 @@ // Copyright (c) Autofac Project. All rights reserved. // Licensed under the MIT License. See LICENSE in the project root for license information. +using System.Diagnostics; using Autofac.Core.Resolving.Pipeline; +using Autofac.Diagnostics; namespace Autofac.Core.Resolving.Middleware; @@ -39,6 +41,20 @@ internal CoreEventMiddleware(ResolveEventType eventType, PipelinePhase phase, Ac /// public void Execute(ResolveRequestContext context, Action next) { - _callback(context, next); + if (!AutofacMetrics.MetricsEnabled) + { + _callback(context, next); + return; + } + + var start = Stopwatch.GetTimestamp(); + try + { + _callback(context, next); + } + finally + { + AutofacMetrics.RecordMiddlewareExecution(ToString(), Stopwatch.GetTimestamp() - start); + } } } diff --git a/src/Autofac/Core/Resolving/Middleware/DelegateMiddleware.cs b/src/Autofac/Core/Resolving/Middleware/DelegateMiddleware.cs index 252681e3c..b1410aab6 100644 --- a/src/Autofac/Core/Resolving/Middleware/DelegateMiddleware.cs +++ b/src/Autofac/Core/Resolving/Middleware/DelegateMiddleware.cs @@ -1,7 +1,9 @@ // Copyright (c) Autofac Project. All rights reserved. // Licensed under the MIT License. See LICENSE in the project root for license information. +using System.Diagnostics; using Autofac.Core.Resolving.Pipeline; +using Autofac.Diagnostics; namespace Autofac.Core.Resolving.Middleware; @@ -32,7 +34,21 @@ public DelegateMiddleware(string descriptor, PipelinePhase phase, Action public void Execute(ResolveRequestContext context, Action next) { - _callback(context, next); + if (!AutofacMetrics.MetricsEnabled) + { + _callback(context, next); + return; + } + + var start = Stopwatch.GetTimestamp(); + try + { + _callback(context, next); + } + finally + { + AutofacMetrics.RecordMiddlewareExecution(ToString(), Stopwatch.GetTimestamp() - start); + } } /// diff --git a/src/Autofac/Core/Resolving/Middleware/DisposalTrackingMiddleware.cs b/src/Autofac/Core/Resolving/Middleware/DisposalTrackingMiddleware.cs index 69b69d02b..4ada3ec10 100644 --- a/src/Autofac/Core/Resolving/Middleware/DisposalTrackingMiddleware.cs +++ b/src/Autofac/Core/Resolving/Middleware/DisposalTrackingMiddleware.cs @@ -1,7 +1,9 @@ // Copyright (c) Autofac Project. All rights reserved. // Licensed under the MIT License. See LICENSE in the project root for license information. +using System.Diagnostics; using Autofac.Core.Resolving.Pipeline; +using Autofac.Diagnostics; namespace Autofac.Core.Resolving.Middleware; @@ -25,6 +27,28 @@ private DisposalTrackingMiddleware() /// public void Execute(ResolveRequestContext context, Action next) + { + if (!AutofacMetrics.MetricsEnabled) + { + ExecuteCore(context, next); + return; + } + + var start = Stopwatch.GetTimestamp(); + try + { + ExecuteCore(context, next); + } + finally + { + AutofacMetrics.RecordMiddlewareExecution(nameof(DisposalTrackingMiddleware), Stopwatch.GetTimestamp() - start); + } + } + + /// + public override string ToString() => nameof(DisposalTrackingMiddleware); + + private static void ExecuteCore(ResolveRequestContext context, Action next) { next(context); @@ -44,7 +68,4 @@ public void Execute(ResolveRequestContext context, Action } } } - - /// - public override string ToString() => nameof(DisposalTrackingMiddleware); } diff --git a/src/Autofac/Core/Resolving/Middleware/RegistrationPipelineInvokeMiddleware.cs b/src/Autofac/Core/Resolving/Middleware/RegistrationPipelineInvokeMiddleware.cs index c9a9bd6f0..90a1c0059 100644 --- a/src/Autofac/Core/Resolving/Middleware/RegistrationPipelineInvokeMiddleware.cs +++ b/src/Autofac/Core/Resolving/Middleware/RegistrationPipelineInvokeMiddleware.cs @@ -1,7 +1,9 @@ // Copyright (c) Autofac Project. All rights reserved. // Licensed under the MIT License. See LICENSE in the project root for license information. +using System.Diagnostics; using Autofac.Core.Resolving.Pipeline; +using Autofac.Diagnostics; namespace Autofac.Core.Resolving.Middleware; @@ -25,7 +27,21 @@ private RegistrationPipelineInvokeMiddleware() /// public void Execute(ResolveRequestContext context, Action next) { - context.Registration.ResolvePipeline.Invoke(context); + if (!AutofacMetrics.MetricsEnabled) + { + context.Registration.ResolvePipeline.Invoke(context); + return; + } + + var start = Stopwatch.GetTimestamp(); + try + { + context.Registration.ResolvePipeline.Invoke(context); + } + finally + { + AutofacMetrics.RecordMiddlewareExecution(nameof(RegistrationPipelineInvokeMiddleware), Stopwatch.GetTimestamp() - start); + } } /// diff --git a/src/Autofac/Core/Resolving/Middleware/ScopeSelectionMiddleware.cs b/src/Autofac/Core/Resolving/Middleware/ScopeSelectionMiddleware.cs index 56e5cb390..78e312090 100644 --- a/src/Autofac/Core/Resolving/Middleware/ScopeSelectionMiddleware.cs +++ b/src/Autofac/Core/Resolving/Middleware/ScopeSelectionMiddleware.cs @@ -1,9 +1,11 @@ // Copyright (c) Autofac Project. All rights reserved. // Licensed under the MIT License. See LICENSE in the project root for license information. +using System.Diagnostics; using System.Globalization; using System.Text; using Autofac.Core.Resolving.Pipeline; +using Autofac.Diagnostics; namespace Autofac.Core.Resolving.Middleware; @@ -27,6 +29,28 @@ private ScopeSelectionMiddleware() /// public void Execute(ResolveRequestContext context, Action next) + { + if (!AutofacMetrics.MetricsEnabled) + { + ExecuteCore(context, next); + return; + } + + var start = Stopwatch.GetTimestamp(); + try + { + ExecuteCore(context, next); + } + finally + { + AutofacMetrics.RecordMiddlewareExecution(nameof(ScopeSelectionMiddleware), Stopwatch.GetTimestamp() - start); + } + } + + /// + public override string ToString() => nameof(ScopeSelectionMiddleware); + + private static void ExecuteCore(ResolveRequestContext context, Action next) { try { @@ -47,7 +71,4 @@ public void Execute(ResolveRequestContext context, Action next(context); } - - /// - public override string ToString() => nameof(ScopeSelectionMiddleware); } diff --git a/src/Autofac/Core/Resolving/Middleware/SharingMiddleware.cs b/src/Autofac/Core/Resolving/Middleware/SharingMiddleware.cs index ad0837f86..d22c05aca 100644 --- a/src/Autofac/Core/Resolving/Middleware/SharingMiddleware.cs +++ b/src/Autofac/Core/Resolving/Middleware/SharingMiddleware.cs @@ -1,7 +1,9 @@ // Copyright (c) Autofac Project. All rights reserved. // Licensed under the MIT License. See LICENSE in the project root for license information. +using System.Diagnostics; using Autofac.Core.Resolving.Pipeline; +using Autofac.Diagnostics; namespace Autofac.Core.Resolving.Middleware; @@ -20,6 +22,28 @@ internal class SharingMiddleware : IResolveMiddleware /// public void Execute(ResolveRequestContext context, Action next) + { + if (!AutofacMetrics.MetricsEnabled) + { + ExecuteCore(context, next); + return; + } + + var start = Stopwatch.GetTimestamp(); + try + { + ExecuteCore(context, next); + } + finally + { + AutofacMetrics.RecordMiddlewareExecution(nameof(SharingMiddleware), Stopwatch.GetTimestamp() - start); + } + } + + /// + public override string ToString() => nameof(SharingMiddleware); + + private static void ExecuteCore(ResolveRequestContext context, Action next) { var registration = context.Registration; var decoratorRegistration = context.DecoratorTarget; @@ -55,7 +79,4 @@ public void Execute(ResolveRequestContext context, Action } } } - - /// - public override string ToString() => nameof(SharingMiddleware); } diff --git a/src/Autofac/Core/Resolving/Middleware/StartableMiddleware.cs b/src/Autofac/Core/Resolving/Middleware/StartableMiddleware.cs index 20a46c765..79f8df516 100644 --- a/src/Autofac/Core/Resolving/Middleware/StartableMiddleware.cs +++ b/src/Autofac/Core/Resolving/Middleware/StartableMiddleware.cs @@ -1,8 +1,10 @@ // Copyright (c) Autofac Project. All rights reserved. // Licensed under the MIT License. See LICENSE in the project root for license information. +using System.Diagnostics; using Autofac.Builder; using Autofac.Core.Resolving.Pipeline; +using Autofac.Diagnostics; namespace Autofac.Core.Resolving.Middleware; @@ -25,6 +27,28 @@ private StartableMiddleware() /// public void Execute(ResolveRequestContext context, Action next) + { + if (!AutofacMetrics.MetricsEnabled) + { + ExecuteCore(context, next); + return; + } + + var start = Stopwatch.GetTimestamp(); + try + { + ExecuteCore(context, next); + } + finally + { + AutofacMetrics.RecordMiddlewareExecution(nameof(StartableMiddleware), Stopwatch.GetTimestamp() - start); + } + } + + /// + public override string ToString() => nameof(StartableMiddleware); + + private static void ExecuteCore(ResolveRequestContext context, Action next) { next(context); @@ -39,7 +63,4 @@ public void Execute(ResolveRequestContext context, Action startable.Start(); } } - - /// - public override string ToString() => nameof(StartableMiddleware); } diff --git a/src/Autofac/Diagnostics/AutofacMetrics.cs b/src/Autofac/Diagnostics/AutofacMetrics.cs new file mode 100644 index 000000000..462e27e23 --- /dev/null +++ b/src/Autofac/Diagnostics/AutofacMetrics.cs @@ -0,0 +1,270 @@ +// Copyright (c) Autofac Project. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using System.Diagnostics; +using System.Diagnostics.Metrics; + +namespace Autofac.Diagnostics; + +/// +/// Centralized metrics wiring for Autofac diagnostics. +/// +internal static class AutofacMetrics +{ + private const string DiagnosticsEnvironmentVariable = "AUTOFAC_METRICS"; + + static AutofacMetrics() + { + MetricsEnabled = IsEnabled(DiagnosticsEnvironmentVariable); + if (!MetricsEnabled) + { + return; + } + + DiagnosticsMeter = new Meter("autofac", "1.0.0"); + + CollectionBuildDuration = DiagnosticsMeter.CreateHistogram( + name: "autofac.collection.build.duration", + unit: "s", + description: "Time spent materializing implicit collection services."); + CollectionBuildCount = DiagnosticsMeter.CreateCounter( + name: "autofac.collection.build.count", + description: "Number of implicit collection builds performed."); + CollectionItemCount = DiagnosticsMeter.CreateCounter( + name: "autofac.collection.build.items", + description: "Total number of elements added to implicit collections."); + + LockContentionDuration = DiagnosticsMeter.CreateHistogram( + name: "autofac.lock.contention.duration", + unit: "s", + description: "Time threads waited to enter Autofac locks, broken down by category/detail."); + LockContentionCount = DiagnosticsMeter.CreateCounter( + name: "autofac.lock.contention.count", + description: "Number of lock contention events observed in Autofac."); + LockContentionTotalTime = DiagnosticsMeter.CreateCounter( + name: "autofac.lock.contention.total_time", + unit: "s", + description: "Total time spent waiting on Autofac locks."); + + PropertyInjectionDuration = DiagnosticsMeter.CreateHistogram( + name: "autofac.property.injection.duration", + unit: "s", + description: "Time spent performing property injection."); + PropertyInjectionCount = DiagnosticsMeter.CreateCounter( + name: "autofac.property.injection.count", + description: "Number of instances that had property injection applied."); + PropertyInjectionAssignments = DiagnosticsMeter.CreateCounter( + name: "autofac.property.injection.assignments", + description: "Number of individual property assignments performed."); + + ReflectionActivationDuration = DiagnosticsMeter.CreateHistogram( + name: "autofac.reflection.activation.duration", + unit: "s", + description: "Time spent activating components via ReflectionActivator."); + + MiddlewareExecutionDuration = DiagnosticsMeter.CreateHistogram( + name: "autofac.middleware.duration", + unit: "s", + description: "Time spent executing Autofac resolve pipeline middleware."); + MiddlewareExecutionCount = DiagnosticsMeter.CreateCounter( + name: "autofac.middleware.count", + description: "Number of resolve pipeline middleware executions."); + } + + /// + /// Gets a value indicating whether diagnostics metrics are enabled. + /// + public static bool MetricsEnabled { get; } + + /// + /// Gets the underlying diagnostics meter. + /// + public static Meter? DiagnosticsMeter { get; } + + /// + /// Gets the histogram tracking lock contention duration. + /// + public static Histogram? LockContentionDuration { get; } + + /// + /// Gets the counter tracking the number of lock contention events. + /// + public static Counter? LockContentionCount { get; } + + /// + /// Gets the counter accumulating total lock wait time. + /// + public static Counter? LockContentionTotalTime { get; } + + /// + /// Gets the histogram tracking implicit collection build durations. + /// + public static Histogram? CollectionBuildDuration { get; } + + /// + /// Gets the counter measuring how many collections were materialized. + /// + public static Counter? CollectionBuildCount { get; } + + /// + /// Gets the counter measuring how many items were added across all collections. + /// + public static Counter? CollectionItemCount { get; } + + /// + /// Gets the histogram tracking property injection durations. + /// + public static Histogram? PropertyInjectionDuration { get; } + + /// + /// Gets the counter measuring how many instances had property injection. + /// + public static Counter? PropertyInjectionCount { get; } + + /// + /// Gets the counter tracking the number of property assignments performed. + /// + public static Counter? PropertyInjectionAssignments { get; } + + /// + /// Gets the histogram tracking reflection activator durations. + /// + public static Histogram? ReflectionActivationDuration { get; } + + /// + /// Gets the histogram tracking middleware execution duration. + /// + public static Histogram? MiddlewareExecutionDuration { get; } + + /// + /// Gets the counter tracking how many middleware executions occurred. + /// + public static Counter? MiddlewareExecutionCount { get; } + + /// + /// Records a collection build event. + /// + /// The kind of collection (e.g., standard, any-keyed). + /// Additional detail such as the service description. + /// The number of elements added to the collection. + /// The elapsed stopwatch ticks for the build. + public static void RecordCollectionBuild(string kind, string? detail, int itemCount, long elapsedTicks) + { + if (!MetricsEnabled || CollectionBuildDuration is null || elapsedTicks <= 0) + { + return; + } + + var seconds = elapsedTicks / (double)Stopwatch.Frequency; + var tags = new TagList + { + { "autofac.collection.kind", kind }, + { "autofac.collection.detail", detail ?? "" }, + }; + + CollectionBuildDuration.Record(seconds, tags); + CollectionBuildCount?.Add(1, tags); + CollectionItemCount?.Add(itemCount, tags); + } + + /// + /// Records a property injection event. + /// + /// The concrete instance type. + /// The number of properties evaluated for injection. + /// The number of properties that were assigned. + /// The elapsed stopwatch ticks for the injection. + public static void RecordPropertyInjection(Type instanceType, int inspectedProperties, int assignedProperties, long elapsedTicks) + { + if (!MetricsEnabled || PropertyInjectionDuration is null || elapsedTicks <= 0) + { + return; + } + + var seconds = elapsedTicks / (double)Stopwatch.Frequency; + var tags = new TagList + { + { "autofac.property.type", instanceType.FullName ?? instanceType.Name }, + { "autofac.property.inspected", inspectedProperties }, + }; + + PropertyInjectionDuration.Record(seconds, tags); + PropertyInjectionCount?.Add(1, tags); + PropertyInjectionAssignments?.Add(assignedProperties, tags); + } + + /// + /// Records a reflection-based activation event. + /// + /// The activated implementation type. + /// The elapsed stopwatch ticks for activation. + public static void RecordReflectionActivation(Type implementationType, long elapsedTicks) + { + if (!MetricsEnabled || ReflectionActivationDuration is null || elapsedTicks <= 0) + { + return; + } + + var seconds = elapsedTicks / (double)Stopwatch.Frequency; + var tags = new TagList + { + { "autofac.reflection.type", implementationType.FullName ?? implementationType.Name }, + }; + + ReflectionActivationDuration.Record(seconds, tags); + } + + /// + /// Records the wait duration for a lock contention event. + /// + /// The lock category (e.g., service or lifetime scope). + /// Additional details about the lock, if any. + /// The time spent waiting, in stopwatch ticks. + public static void RecordLockContention(string category, string? detail, long elapsedTicks) + { + if (!MetricsEnabled || LockContentionDuration is null || elapsedTicks <= 0) + { + return; + } + + var seconds = elapsedTicks / (double)Stopwatch.Frequency; + var tags = new TagList + { + { "autofac.lock.category", category }, + { "autofac.lock.detail", detail ?? "" }, + }; + + LockContentionDuration.Record(seconds, tags); + LockContentionCount?.Add(1, tags); + LockContentionTotalTime?.Add(seconds, tags); + } + + /// + /// Records a resolve pipeline middleware execution event. + /// + /// The middleware name. + /// The elapsed stopwatch ticks for the execution. + public static void RecordMiddlewareExecution(string middlewareName, long elapsedTicks) + { + if (!MetricsEnabled || MiddlewareExecutionDuration is null || elapsedTicks <= 0) + { + return; + } + + var seconds = elapsedTicks / (double)Stopwatch.Frequency; + var tags = new TagList + { + { "autofac.middleware.name", middlewareName }, + }; + + MiddlewareExecutionDuration.Record(seconds, tags); + MiddlewareExecutionCount?.Add(1, tags); + } + + private static bool IsEnabled(string variableName) + { + var envValue = Environment.GetEnvironmentVariable(variableName); + return string.Equals(envValue, "1", StringComparison.OrdinalIgnoreCase) + || string.Equals(envValue, "true", StringComparison.OrdinalIgnoreCase); + } +} diff --git a/src/Autofac/Diagnostics/ValueStopwatch.cs b/src/Autofac/Diagnostics/ValueStopwatch.cs new file mode 100644 index 000000000..0360f88de --- /dev/null +++ b/src/Autofac/Diagnostics/ValueStopwatch.cs @@ -0,0 +1,46 @@ +// Copyright (c) Autofac Project. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using System.Diagnostics; + +namespace Autofac.Diagnostics; + +/// +/// Lightweight stopwatch with no heap allocations. +/// +internal readonly struct ValueStopwatch +{ + private readonly long _startTimestamp; + + private ValueStopwatch(long startTimestamp) + { + _startTimestamp = startTimestamp; + } + + /// + /// Gets the number of elapsed ticks. + /// + public long ElapsedTicks + { + get + { + if (_startTimestamp == 0) + { + return 0; + } + + return Stopwatch.GetTimestamp() - _startTimestamp; + } + } + + /// + /// Gets the elapsed duration in milliseconds. + /// + public double ElapsedMilliseconds => ElapsedTicks * 1000.0 / Stopwatch.Frequency; + + /// + /// Starts a new stopwatch instance. + /// + /// A running . + public static ValueStopwatch StartNew() => new(Stopwatch.GetTimestamp()); +} diff --git a/src/Autofac/Features/Collections/CollectionRegistrationSource.cs b/src/Autofac/Features/Collections/CollectionRegistrationSource.cs index f4f0a2731..0e51ab0d2 100644 --- a/src/Autofac/Features/Collections/CollectionRegistrationSource.cs +++ b/src/Autofac/Features/Collections/CollectionRegistrationSource.cs @@ -8,6 +8,7 @@ using Autofac.Core.Activators.Delegate; using Autofac.Core.Lifetime; using Autofac.Core.Registration; +using Autofac.Diagnostics; using Autofac.Features.Decorators; using Autofac.Util; using Autofac.Util.Cache; @@ -124,7 +125,11 @@ public IEnumerable RegistrationsFor(Service service, Fun .ConvertAll(static tuple => ((Service)tuple.KeyedService, tuple.Registration)) : BuildStandardRegistrationList(c.ComponentRegistry, elementTypeService); - return BuildCollection(c, factory, registrationTuples, p); + var collectionKind = isAnyKeyQuery ? "any-keyed" : "standard"; + var collectionDetail = isAnyKeyQuery + ? elementType.FullName ?? elementType.Name + : elementTypeService.ToString() ?? elementTypeService.GetType().Name; + return BuildCollection(c, factory, registrationTuples, p, collectionKind, collectionDetail); }); var registration = new ComponentRegistration( @@ -237,8 +242,17 @@ private static IList BuildCollection( IComponentContext context, Func factory, List<(Service Service, ServiceRegistration Registration)> registrations, - IEnumerable parameters) + IEnumerable parameters, + string collectionKind, + string collectionDetail) { + var recordMetrics = AutofacMetrics.MetricsEnabled; + ValueStopwatch instrumentationTimer = default; + if (recordMetrics) + { + instrumentationTimer = ValueStopwatch.StartNew(); + } + var output = factory(registrations.Count); var isFixedSize = output.IsFixedSize; @@ -258,6 +272,15 @@ private static IList BuildCollection( } } + if (recordMetrics) + { + AutofacMetrics.RecordCollectionBuild( + kind: collectionKind, + detail: collectionDetail, + itemCount: registrations.Count, + elapsedTicks: instrumentationTimer.ElapsedTicks); + } + return output; } } diff --git a/src/Autofac/Features/Decorators/DecoratorMiddleware.cs b/src/Autofac/Features/Decorators/DecoratorMiddleware.cs index ef0c398c1..b5bb563c7 100644 --- a/src/Autofac/Features/Decorators/DecoratorMiddleware.cs +++ b/src/Autofac/Features/Decorators/DecoratorMiddleware.cs @@ -1,10 +1,12 @@ // Copyright (c) Autofac Project. All rights reserved. // Licensed under the MIT License. See LICENSE in the project root for license information. +using System.Diagnostics; using Autofac.Core; using Autofac.Core.Registration; using Autofac.Core.Resolving; using Autofac.Core.Resolving.Pipeline; +using Autofac.Diagnostics; namespace Autofac.Features.Decorators; @@ -32,6 +34,28 @@ public DecoratorMiddleware(DecoratorService decoratorService, IComponentRegistra /// public void Execute(ResolveRequestContext context, Action next) + { + if (!AutofacMetrics.MetricsEnabled) + { + ExecuteCore(context, next); + return; + } + + var start = Stopwatch.GetTimestamp(); + try + { + ExecuteCore(context, next); + } + finally + { + AutofacMetrics.RecordMiddlewareExecution(nameof(DecoratorMiddleware), Stopwatch.GetTimestamp() - start); + } + } + + /// + public override string ToString() => nameof(DecoratorMiddleware) + " [" + _decoratorRegistration.Activator.LimitType.Name + "]"; + + private void ExecuteCore(ResolveRequestContext context, Action next) { // Go down the pipeline first, need that instance. next(context); @@ -128,7 +152,4 @@ public void Execute(ResolveRequestContext context, Action context.Instance = decoratedInstance; } } - - /// - public override string ToString() => nameof(DecoratorMiddleware) + " [" + _decoratorRegistration.Activator.LimitType.Name + "]"; } From cd06839f68c8f0118c63df4dbfdd32f0bb442f46 Mon Sep 17 00:00:00 2001 From: Travis Illig Date: Thu, 26 Feb 2026 14:57:54 -0800 Subject: [PATCH 08/15] Revert changes to the BenchmarkProfiling tool. --- bench/Autofac.BenchmarkProfiling/Program.cs | 58 +-------------------- 1 file changed, 1 insertion(+), 57 deletions(-) diff --git a/bench/Autofac.BenchmarkProfiling/Program.cs b/bench/Autofac.BenchmarkProfiling/Program.cs index 6e7a56412..cd2025b6e 100644 --- a/bench/Autofac.BenchmarkProfiling/Program.cs +++ b/bench/Autofac.BenchmarkProfiling/Program.cs @@ -1,8 +1,5 @@ -using System.Diagnostics; -using System.Reflection; -using BenchmarkDotNet.Running; +using BenchmarkDotNet.Running; using BenchmarkDotNet.Toolchains.InProcess.NoEmit; -using Autofac.Core; namespace Autofac.BenchmarkProfiling; @@ -13,11 +10,6 @@ class Program { static void Main(string[] args) { - if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable("AUTOFAC_SCOPE_DIAGNOSTICS"))) - { - AppContext.SetSwitch("Autofac.ScopeIsolatedDiagnostics", true); - } - // Pick a benchmark. var availableBenchmarks = Benchmarks.BenchmarkSet.All; @@ -106,20 +98,6 @@ void workloadAction(int repeat) // Warmup. workloadAction(100); - if (int.TryParse(Environment.GetEnvironmentVariable("AUTOFAC_MEASURE_ITERATIONS"), out var measurementIterations) && - measurementIterations > 0) - { - var sw = Stopwatch.StartNew(); - workloadAction(measurementIterations); - sw.Stop(); - var perIteration = sw.Elapsed.TotalMilliseconds / measurementIterations; - Console.WriteLine( - "[Profiling] Duration: {0} iterations took {1:F2} ms (avg {2:F4} ms)", - measurementIterations, - sw.Elapsed.TotalMilliseconds, - perIteration); - } - // Now start a new thread. var runThread = new Thread(new ThreadStart(() => { @@ -134,8 +112,6 @@ void workloadAction(int repeat) runThread.Join(); cleanupAction.InvokeSingle(); - - LogScopeDiagnosticsIfEnabled(); } private static void PrintBenchmarks(Type[] availableBenchmarks) @@ -161,36 +137,4 @@ private static void PrintCases(BenchmarkRunInfo benchRunInfo) } } } - - private static void LogScopeDiagnosticsIfEnabled() - { - if (!AppContext.TryGetSwitch("Autofac.ScopeIsolatedDiagnostics", out var enabled) || !enabled) - { - return; - } - - var diagnosticsType = typeof(IComponentRegistry).Assembly.GetType("Autofac.Core.Registration.ScopeIsolatedServiceDiagnostics"); - var snapshotProperty = diagnosticsType?.GetProperty( - "Snapshot", - BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static); - - if (snapshotProperty?.GetValue(null) is object snapshot) - { - var cacheHits = (long)(snapshot.GetType().GetProperty("CacheHits")?.GetValue(snapshot) ?? 0L); - var cacheMisses = (long)(snapshot.GetType().GetProperty("CacheMisses")?.GetValue(snapshot) ?? 0L); - var cacheAdds = (long)(snapshot.GetType().GetProperty("CacheAdds")?.GetValue(snapshot) ?? 0L); - var cacheRemovals = (long)(snapshot.GetType().GetProperty("CacheRemovals")?.GetValue(snapshot) ?? 0L); - var cachedInitializations = (long)(snapshot.GetType().GetProperty("CachedInitializations")?.GetValue(snapshot) ?? 0L); - var discardedInfos = (long)(snapshot.GetType().GetProperty("ServiceInfoDiscarded")?.GetValue(snapshot) ?? 0L); - - Console.WriteLine( - "[Profiling] Scope cache stats -> Hits={0}, Misses={1}, Adds={2}, Removes={3}, CachedInit={4}, Discarded={5}", - cacheHits, - cacheMisses, - cacheAdds, - cacheRemovals, - cachedInitializations, - discardedInfos); - } - } } From 34c995b287cd48320948a213cc7b2e1b7e39721c Mon Sep 17 00:00:00 2001 From: Travis Illig Date: Thu, 26 Feb 2026 15:00:07 -0800 Subject: [PATCH 09/15] Adjust method ordering. --- .../CircularDependencyDetectorMiddleware.cs | 52 +++++++++---------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/src/Autofac/Core/Resolving/Middleware/CircularDependencyDetectorMiddleware.cs b/src/Autofac/Core/Resolving/Middleware/CircularDependencyDetectorMiddleware.cs index dedfe8287..94ecb5f54 100644 --- a/src/Autofac/Core/Resolving/Middleware/CircularDependencyDetectorMiddleware.cs +++ b/src/Autofac/Core/Resolving/Middleware/CircularDependencyDetectorMiddleware.cs @@ -58,6 +58,32 @@ public void Execute(ResolveRequestContext context, Action } } + /// + public override string ToString() => nameof(CircularDependencyDetectorMiddleware); + + private static string CreateDependencyGraphTo(IComponentRegistration registration, IEnumerable requestStack) + { + if (registration == null) + { + throw new ArgumentNullException(nameof(registration)); + } + + if (requestStack == null) + { + throw new ArgumentNullException(nameof(requestStack)); + } + + var dependencyGraph = Display(registration); + + return requestStack.Select(a => a.Registration) + .Aggregate(dependencyGraph, (current, requestor) => Display(requestor) + " -> " + current); + } + + private static string Display(IComponentRegistration registration) + { + return registration.Activator.DisplayName(); + } + private void ExecuteCore(ResolveRequestContext context, Action next) { if (context.Operation is not IDependencyTrackingResolveOperation dependencyTrackingResolveOperation) @@ -116,30 +142,4 @@ private void ExecuteCore(ResolveRequestContext context, Action - public override string ToString() => nameof(CircularDependencyDetectorMiddleware); - - private static string CreateDependencyGraphTo(IComponentRegistration registration, IEnumerable requestStack) - { - if (registration == null) - { - throw new ArgumentNullException(nameof(registration)); - } - - if (requestStack == null) - { - throw new ArgumentNullException(nameof(requestStack)); - } - - var dependencyGraph = Display(registration); - - return requestStack.Select(a => a.Registration) - .Aggregate(dependencyGraph, (current, requestor) => Display(requestor) + " -> " + current); - } - - private static string Display(IComponentRegistration registration) - { - return registration.Activator.DisplayName(); - } } From 40759bab58bc4b0d58a8d97f59185ca4f01aa4ec Mon Sep 17 00:00:00 2001 From: Travis Illig Date: Thu, 26 Feb 2026 15:14:22 -0800 Subject: [PATCH 10/15] Refactored for cyclomatic complexity, added docs. --- .../DefaultRegisteredServicesTracker.cs | 245 ++++++++++++------ 1 file changed, 163 insertions(+), 82 deletions(-) diff --git a/src/Autofac/Core/Registration/DefaultRegisteredServicesTracker.cs b/src/Autofac/Core/Registration/DefaultRegisteredServicesTracker.cs index 6761991c5..044b3d021 100644 --- a/src/Autofac/Core/Registration/DefaultRegisteredServicesTracker.cs +++ b/src/Autofac/Core/Registration/DefaultRegisteredServicesTracker.cs @@ -264,6 +264,12 @@ protected override async ValueTask DisposeAsync(bool disposing) // Do not call the base, otherwise the standard Dispose will fire. } + /// + /// Filters registration sources to skip a single source. + /// + /// The source sequence to scan. + /// The source to exclude. + /// Sources that are not the excluded instance. [MethodImpl(MethodImplOptions.AggressiveInlining)] private static IEnumerable ExcludeSource(IEnumerable sources, IRegistrationSource exclude) { @@ -276,6 +282,33 @@ private static IEnumerable ExcludeSource(IEnumerable + /// Acquires the service info lock and records wait time when metrics are enabled. + /// + /// The service info lock target. + /// Optional detail about the service for metrics. + /// Tracks whether the lock was acquired. + private static void EnterServiceInfoLock(ServiceRegistrationInfo info, string? instrumentationService, ref bool lockTaken) + { + if (AutofacMetrics.MetricsEnabled) + { + var wait = ValueStopwatch.StartNew(); + Monitor.Enter(info, ref lockTaken); + AutofacMetrics.RecordLockContention("Service", instrumentationService, wait.ElapsedTicks); + } + else + { + Monitor.Enter(info, ref lockTaken); + } + } + + /// + /// Gets or creates an ephemeral service info entry used during pre-complete initialization. + /// + /// The ephemeral map for this initialization pass. + /// The service key. + /// The baseline service info to clone if needed. + /// An ephemeral service info entry for the service. private static ServiceRegistrationInfo GetEphemeralServiceInfo(Dictionary ephemeralSet, Service service, ServiceRegistrationInfo info) { if (ephemeralSet.TryGetValue(service, out var ephemeral)) @@ -290,19 +323,37 @@ private static ServiceRegistrationInfo GetEphemeralServiceInfo(Dictionary + /// Unwraps scope-isolated services and notes when isolation applies. + /// + /// The service to inspect. + /// Set to when the service is scope isolated. + /// The inner service to process. + private static Service ResolveScopeIsolation(Service service, ref bool isScopeIsolatedService) { - var createdEphemeralSet = false; - var isScopeIsolatedService = false; - if (service is ScopeIsolatedService scopeIsolatedService) { // This is an isolated service query; use the wrapped service instead and // remember that fact for later. isScopeIsolatedService = true; - service = scopeIsolatedService.Service; + return scopeIsolatedService.Service; } + return service; + } + + /// + /// Ensures the service info is initialized and returns it. + /// + /// The service being queried. + /// The initialized service info. + private ServiceRegistrationInfo GetInitializedServiceInfo(Service service) + { + var createdEphemeralSet = false; + var isScopeIsolatedService = false; + + service = ResolveScopeIsolation(service, ref isScopeIsolatedService); + var info = GetServiceInfo(service); var instrumentationService = AutofacMetrics.MetricsEnabled ? service.ToString() : null; if (info.IsInitialized) @@ -310,92 +361,21 @@ private ServiceRegistrationInfo GetInitializedServiceInfo(Service service) return info; } - if (!_trackerPopulationComplete) - { - // We need an ephemeral set for this pre-complete initialization. - if (_ephemeralServiceInfo is null) - { - _ephemeralServiceInfo = new Dictionary(); - createdEphemeralSet = true; - } - - info = GetEphemeralServiceInfo(_ephemeralServiceInfo, service, info); - } + info = GetServiceInfoForInitialization(service, info, ref createdEphemeralSet); var succeeded = false; var lockTaken = false; try { - if (AutofacMetrics.MetricsEnabled) - { - var wait = ValueStopwatch.StartNew(); - Monitor.Enter(info, ref lockTaken); - AutofacMetrics.RecordLockContention("Service", instrumentationService, wait.ElapsedTicks); - } - else - { - Monitor.Enter(info, ref lockTaken); - } + EnterServiceInfoLock(info, instrumentationService, ref lockTaken); if (info.IsInitialized) { return info; } - if (!info.IsInitializing) - { - BeginServiceInfoInitialization(service, info, _dynamicRegistrationSources); - } - - info.InitializationDepth++; - - while (info.HasSourcesToQuery) - { - var next = info.DequeueNextSource(); - - // Do not query per-scope registration sources - // for isolated services. - if (isScopeIsolatedService && next is IPerScopeRegistrationSource) - { - continue; - } - - foreach (var provided in next.RegistrationsFor(service, _registrationAccessor)) - { - // This ensures that multiple services provided by the same - // component share a single component (we don't re-query for them) - foreach (var additionalService in provided.Services) - { - var additionalInfo = GetServiceInfo(additionalService); - if (additionalInfo.IsInitialized || additionalInfo == info) - { - continue; - } - - if (_ephemeralServiceInfo is not null) - { - // Use ephemeral info for additional services. - additionalInfo = GetEphemeralServiceInfo(_ephemeralServiceInfo, service, info); - } - - if (!additionalInfo.IsInitializing) - { - BeginServiceInfoInitialization(additionalService, additionalInfo, ExcludeSource(_dynamicRegistrationSources, next)); - } - else - { - additionalInfo.SkipSource(next); - } - } - - AddRegistration( - provided, - preserveDefaults: true, - originatedFromDynamicSource: true); - } - } - - succeeded = true; + // PopulateServiceInfo increments InitializationDepth; the decrement is paired in finally. + succeeded = PopulateServiceInfo(service, info, isScopeIsolatedService); } finally { @@ -421,8 +401,8 @@ private ServiceRegistrationInfo GetInitializedServiceInfo(Service service) Monitor.Exit(info); } - // This method was the entry point to an ephemeral service info initialization. - // We need to discard it, so the next set of ephemeral service info is done from scratch. + // This method was the entry point to an ephemeral initialization pass. + // Discard the temporary map so later calls start with a clean slate. if (createdEphemeralSet) { _ephemeralServiceInfo?.Clear(); @@ -433,6 +413,102 @@ private ServiceRegistrationInfo GetInitializedServiceInfo(Service service) return info; } + /// + /// Returns the appropriate service info for initialization, swapping to an ephemeral copy when needed. + /// + /// The service being queried. + /// The current service info. + /// Set to when a new ephemeral set is created. + /// The service info to use for initialization. + private ServiceRegistrationInfo GetServiceInfoForInitialization(Service service, ServiceRegistrationInfo info, ref bool createdEphemeralSet) + { + if (!_trackerPopulationComplete) + { + // We need an ephemeral set for this pre-complete initialization. + if (_ephemeralServiceInfo is null) + { + _ephemeralServiceInfo = new Dictionary(); + createdEphemeralSet = true; + } + + info = GetEphemeralServiceInfo(_ephemeralServiceInfo, service, info); + } + + return info; + } + + /// + /// Populates service info by querying registration sources and adding derived registrations. + /// + /// The service being initialized. + /// The service info to populate. + /// when per-scope sources should be skipped. + /// when initialization completes. + private bool PopulateServiceInfo(Service service, ServiceRegistrationInfo info, bool isScopeIsolatedService) + { + if (!info.IsInitializing) + { + BeginServiceInfoInitialization(service, info, _dynamicRegistrationSources); + } + + info.InitializationDepth++; + + // Drain sources in-order; registrations can enqueue additional sources. + while (info.HasSourcesToQuery) + { + var next = info.DequeueNextSource(); + + // Do not query per-scope registration sources + // for isolated services. + if (isScopeIsolatedService && next is IPerScopeRegistrationSource) + { + continue; + } + + foreach (var provided in next.RegistrationsFor(service, _registrationAccessor)) + { + // This ensures that multiple services provided by the same + // component share a single component (we don't re-query for them) + foreach (var additionalService in provided.Services) + { + var additionalInfo = GetServiceInfo(additionalService); + if (additionalInfo.IsInitialized || additionalInfo == info) + { + continue; + } + + if (_ephemeralServiceInfo is not null) + { + // Use ephemeral info for additional services. + additionalInfo = GetEphemeralServiceInfo(_ephemeralServiceInfo, service, info); + } + + if (!additionalInfo.IsInitializing) + { + BeginServiceInfoInitialization(additionalService, additionalInfo, ExcludeSource(_dynamicRegistrationSources, next)); + } + else + { + additionalInfo.SkipSource(next); + } + } + + AddRegistration( + provided, + preserveDefaults: true, + originatedFromDynamicSource: true); + } + } + + return true; + } + + /// + /// Seeds service info with middleware and registration sources. + /// + /// The service being initialized. + /// The service info to update. + /// Sources to query for registrations. private void BeginServiceInfoInitialization(Service service, ServiceRegistrationInfo info, IEnumerable registrationSources) { // Add any additional service pipeline configuration from external sources. @@ -444,6 +520,11 @@ private void BeginServiceInfoInitialization(Service service, ServiceRegistration info.BeginInitialization(registrationSources); } + /// + /// Gets or creates the service info entry for a service key. + /// + /// The service key. + /// The service info entry. [MethodImpl(MethodImplOptions.AggressiveInlining)] private ServiceRegistrationInfo GetServiceInfo(Service service) { From 754848de028ec186c1d934fa543dce0aad1093b2 Mon Sep 17 00:00:00 2001 From: Travis Illig Date: Thu, 26 Feb 2026 15:34:32 -0800 Subject: [PATCH 11/15] Use ASP.NET ValueStopwatch. --- .../Reflection/AutowiringPropertyInjector.cs | 2 +- .../Reflection/ReflectionActivator.cs | 6 +- src/Autofac/Core/Lifetime/LifetimeScope.cs | 4 +- .../DefaultRegisteredServicesTracker.cs | 2 +- .../ActivatorErrorHandlingMiddleware.cs | 5 +- .../CircularDependencyDetectorMiddleware.cs | 5 +- .../Middleware/CoreEventMiddleware.cs | 5 +- .../Middleware/DelegateMiddleware.cs | 5 +- .../Middleware/DisposalTrackingMiddleware.cs | 5 +- .../RegistrationPipelineInvokeMiddleware.cs | 5 +- .../Middleware/ScopeSelectionMiddleware.cs | 5 +- .../Resolving/Middleware/SharingMiddleware.cs | 5 +- .../Middleware/StartableMiddleware.cs | 5 +- src/Autofac/Diagnostics/AutofacMetrics.cs | 47 ++++++-------- src/Autofac/Diagnostics/ValueStopwatch.cs | 64 +++++++++++++------ .../CollectionRegistrationSource.cs | 2 +- .../Decorators/DecoratorMiddleware.cs | 5 +- 17 files changed, 94 insertions(+), 83 deletions(-) diff --git a/src/Autofac/Core/Activators/Reflection/AutowiringPropertyInjector.cs b/src/Autofac/Core/Activators/Reflection/AutowiringPropertyInjector.cs index 93ff3caac..1ec6e3ff9 100644 --- a/src/Autofac/Core/Activators/Reflection/AutowiringPropertyInjector.cs +++ b/src/Autofac/Core/Activators/Reflection/AutowiringPropertyInjector.cs @@ -105,7 +105,7 @@ public static void InjectProperties(IComponentContext context, object instance, instanceType, injectableProperties.Count, injectedProperties, - instrumentationTimer.ElapsedTicks); + instrumentationTimer.GetElapsedTime()); } } diff --git a/src/Autofac/Core/Activators/Reflection/ReflectionActivator.cs b/src/Autofac/Core/Activators/Reflection/ReflectionActivator.cs index 8cb01e1bb..c761c345c 100644 --- a/src/Autofac/Core/Activators/Reflection/ReflectionActivator.cs +++ b/src/Autofac/Core/Activators/Reflection/ReflectionActivator.cs @@ -234,7 +234,7 @@ private void UseSingleConstructorActivation(IResolvePipelineBuilder pipelineBuil if (recordMetrics) { - AutofacMetrics.RecordReflectionActivation(_implementationType, instrumentationTimer.ElapsedTicks); + AutofacMetrics.RecordReflectionActivation(_implementationType, instrumentationTimer.GetElapsedTime()); } next(context); @@ -270,7 +270,7 @@ private void UseSingleConstructorActivation(IResolvePipelineBuilder pipelineBuil if (recordMetrics) { - AutofacMetrics.RecordReflectionActivation(_implementationType, instrumentationTimer.ElapsedTicks); + AutofacMetrics.RecordReflectionActivation(_implementationType, instrumentationTimer.GetElapsedTime()); } next(context); @@ -329,7 +329,7 @@ private object ActivateInstance(IComponentContext context, IEnumerable creator) { var wait = ValueStopwatch.StartNew(); Monitor.Enter(_synchRoot, ref lockTaken); - AutofacMetrics.RecordLockContention("LifetimeScopeSharedInstance", instrumentationDetail, wait.ElapsedTicks); + AutofacMetrics.RecordLockContention("LifetimeScopeSharedInstance", instrumentationDetail, wait.GetElapsedTime()); } else { @@ -336,7 +336,7 @@ public object CreateSharedInstance(Guid primaryId, Guid? qualifyingId, Func return; } - var start = Stopwatch.GetTimestamp(); + var timer = ValueStopwatch.StartNew(); try { ExecuteCore(context, next); } finally { - AutofacMetrics.RecordMiddlewareExecution(nameof(ActivatorErrorHandlingMiddleware), Stopwatch.GetTimestamp() - start); + AutofacMetrics.RecordMiddlewareExecution(nameof(ActivatorErrorHandlingMiddleware), timer.GetElapsedTime()); } } diff --git a/src/Autofac/Core/Resolving/Middleware/CircularDependencyDetectorMiddleware.cs b/src/Autofac/Core/Resolving/Middleware/CircularDependencyDetectorMiddleware.cs index 94ecb5f54..d5d7e2f34 100644 --- a/src/Autofac/Core/Resolving/Middleware/CircularDependencyDetectorMiddleware.cs +++ b/src/Autofac/Core/Resolving/Middleware/CircularDependencyDetectorMiddleware.cs @@ -1,7 +1,6 @@ // Copyright (c) Autofac Project. All rights reserved. // Licensed under the MIT License. See LICENSE in the project root for license information. -using System.Diagnostics; using System.Globalization; using System.Runtime.CompilerServices; using Autofac.Core.Resolving.Pipeline; @@ -47,14 +46,14 @@ public void Execute(ResolveRequestContext context, Action return; } - var start = Stopwatch.GetTimestamp(); + var timer = ValueStopwatch.StartNew(); try { ExecuteCore(context, next); } finally { - AutofacMetrics.RecordMiddlewareExecution(nameof(CircularDependencyDetectorMiddleware), Stopwatch.GetTimestamp() - start); + AutofacMetrics.RecordMiddlewareExecution(nameof(CircularDependencyDetectorMiddleware), timer.GetElapsedTime()); } } diff --git a/src/Autofac/Core/Resolving/Middleware/CoreEventMiddleware.cs b/src/Autofac/Core/Resolving/Middleware/CoreEventMiddleware.cs index 1ab8434aa..1bc5c3647 100644 --- a/src/Autofac/Core/Resolving/Middleware/CoreEventMiddleware.cs +++ b/src/Autofac/Core/Resolving/Middleware/CoreEventMiddleware.cs @@ -1,7 +1,6 @@ // Copyright (c) Autofac Project. All rights reserved. // Licensed under the MIT License. See LICENSE in the project root for license information. -using System.Diagnostics; using Autofac.Core.Resolving.Pipeline; using Autofac.Diagnostics; @@ -47,14 +46,14 @@ public void Execute(ResolveRequestContext context, Action return; } - var start = Stopwatch.GetTimestamp(); + var timer = ValueStopwatch.StartNew(); try { _callback(context, next); } finally { - AutofacMetrics.RecordMiddlewareExecution(ToString(), Stopwatch.GetTimestamp() - start); + AutofacMetrics.RecordMiddlewareExecution(ToString(), timer.GetElapsedTime()); } } } diff --git a/src/Autofac/Core/Resolving/Middleware/DelegateMiddleware.cs b/src/Autofac/Core/Resolving/Middleware/DelegateMiddleware.cs index b1410aab6..4220b7375 100644 --- a/src/Autofac/Core/Resolving/Middleware/DelegateMiddleware.cs +++ b/src/Autofac/Core/Resolving/Middleware/DelegateMiddleware.cs @@ -1,7 +1,6 @@ // Copyright (c) Autofac Project. All rights reserved. // Licensed under the MIT License. See LICENSE in the project root for license information. -using System.Diagnostics; using Autofac.Core.Resolving.Pipeline; using Autofac.Diagnostics; @@ -40,14 +39,14 @@ public void Execute(ResolveRequestContext context, Action return; } - var start = Stopwatch.GetTimestamp(); + var timer = ValueStopwatch.StartNew(); try { _callback(context, next); } finally { - AutofacMetrics.RecordMiddlewareExecution(ToString(), Stopwatch.GetTimestamp() - start); + AutofacMetrics.RecordMiddlewareExecution(ToString(), timer.GetElapsedTime()); } } diff --git a/src/Autofac/Core/Resolving/Middleware/DisposalTrackingMiddleware.cs b/src/Autofac/Core/Resolving/Middleware/DisposalTrackingMiddleware.cs index 4ada3ec10..d471b49a7 100644 --- a/src/Autofac/Core/Resolving/Middleware/DisposalTrackingMiddleware.cs +++ b/src/Autofac/Core/Resolving/Middleware/DisposalTrackingMiddleware.cs @@ -1,7 +1,6 @@ // Copyright (c) Autofac Project. All rights reserved. // Licensed under the MIT License. See LICENSE in the project root for license information. -using System.Diagnostics; using Autofac.Core.Resolving.Pipeline; using Autofac.Diagnostics; @@ -34,14 +33,14 @@ public void Execute(ResolveRequestContext context, Action return; } - var start = Stopwatch.GetTimestamp(); + var timer = ValueStopwatch.StartNew(); try { ExecuteCore(context, next); } finally { - AutofacMetrics.RecordMiddlewareExecution(nameof(DisposalTrackingMiddleware), Stopwatch.GetTimestamp() - start); + AutofacMetrics.RecordMiddlewareExecution(nameof(DisposalTrackingMiddleware), timer.GetElapsedTime()); } } diff --git a/src/Autofac/Core/Resolving/Middleware/RegistrationPipelineInvokeMiddleware.cs b/src/Autofac/Core/Resolving/Middleware/RegistrationPipelineInvokeMiddleware.cs index 90a1c0059..2b8244b84 100644 --- a/src/Autofac/Core/Resolving/Middleware/RegistrationPipelineInvokeMiddleware.cs +++ b/src/Autofac/Core/Resolving/Middleware/RegistrationPipelineInvokeMiddleware.cs @@ -1,7 +1,6 @@ // Copyright (c) Autofac Project. All rights reserved. // Licensed under the MIT License. See LICENSE in the project root for license information. -using System.Diagnostics; using Autofac.Core.Resolving.Pipeline; using Autofac.Diagnostics; @@ -33,14 +32,14 @@ public void Execute(ResolveRequestContext context, Action return; } - var start = Stopwatch.GetTimestamp(); + var timer = ValueStopwatch.StartNew(); try { context.Registration.ResolvePipeline.Invoke(context); } finally { - AutofacMetrics.RecordMiddlewareExecution(nameof(RegistrationPipelineInvokeMiddleware), Stopwatch.GetTimestamp() - start); + AutofacMetrics.RecordMiddlewareExecution(nameof(RegistrationPipelineInvokeMiddleware), timer.GetElapsedTime()); } } diff --git a/src/Autofac/Core/Resolving/Middleware/ScopeSelectionMiddleware.cs b/src/Autofac/Core/Resolving/Middleware/ScopeSelectionMiddleware.cs index 78e312090..4d467ccf8 100644 --- a/src/Autofac/Core/Resolving/Middleware/ScopeSelectionMiddleware.cs +++ b/src/Autofac/Core/Resolving/Middleware/ScopeSelectionMiddleware.cs @@ -1,7 +1,6 @@ // Copyright (c) Autofac Project. All rights reserved. // Licensed under the MIT License. See LICENSE in the project root for license information. -using System.Diagnostics; using System.Globalization; using System.Text; using Autofac.Core.Resolving.Pipeline; @@ -36,14 +35,14 @@ public void Execute(ResolveRequestContext context, Action return; } - var start = Stopwatch.GetTimestamp(); + var timer = ValueStopwatch.StartNew(); try { ExecuteCore(context, next); } finally { - AutofacMetrics.RecordMiddlewareExecution(nameof(ScopeSelectionMiddleware), Stopwatch.GetTimestamp() - start); + AutofacMetrics.RecordMiddlewareExecution(nameof(ScopeSelectionMiddleware), timer.GetElapsedTime()); } } diff --git a/src/Autofac/Core/Resolving/Middleware/SharingMiddleware.cs b/src/Autofac/Core/Resolving/Middleware/SharingMiddleware.cs index d22c05aca..e21ca07e4 100644 --- a/src/Autofac/Core/Resolving/Middleware/SharingMiddleware.cs +++ b/src/Autofac/Core/Resolving/Middleware/SharingMiddleware.cs @@ -1,7 +1,6 @@ // Copyright (c) Autofac Project. All rights reserved. // Licensed under the MIT License. See LICENSE in the project root for license information. -using System.Diagnostics; using Autofac.Core.Resolving.Pipeline; using Autofac.Diagnostics; @@ -29,14 +28,14 @@ public void Execute(ResolveRequestContext context, Action return; } - var start = Stopwatch.GetTimestamp(); + var timer = ValueStopwatch.StartNew(); try { ExecuteCore(context, next); } finally { - AutofacMetrics.RecordMiddlewareExecution(nameof(SharingMiddleware), Stopwatch.GetTimestamp() - start); + AutofacMetrics.RecordMiddlewareExecution(nameof(SharingMiddleware), timer.GetElapsedTime()); } } diff --git a/src/Autofac/Core/Resolving/Middleware/StartableMiddleware.cs b/src/Autofac/Core/Resolving/Middleware/StartableMiddleware.cs index 79f8df516..dd1623109 100644 --- a/src/Autofac/Core/Resolving/Middleware/StartableMiddleware.cs +++ b/src/Autofac/Core/Resolving/Middleware/StartableMiddleware.cs @@ -1,7 +1,6 @@ // Copyright (c) Autofac Project. All rights reserved. // Licensed under the MIT License. See LICENSE in the project root for license information. -using System.Diagnostics; using Autofac.Builder; using Autofac.Core.Resolving.Pipeline; using Autofac.Diagnostics; @@ -34,14 +33,14 @@ public void Execute(ResolveRequestContext context, Action return; } - var start = Stopwatch.GetTimestamp(); + var timer = ValueStopwatch.StartNew(); try { ExecuteCore(context, next); } finally { - AutofacMetrics.RecordMiddlewareExecution(nameof(StartableMiddleware), Stopwatch.GetTimestamp() - start); + AutofacMetrics.RecordMiddlewareExecution(nameof(StartableMiddleware), timer.GetElapsedTime()); } } diff --git a/src/Autofac/Diagnostics/AutofacMetrics.cs b/src/Autofac/Diagnostics/AutofacMetrics.cs index 462e27e23..4a41013c3 100644 --- a/src/Autofac/Diagnostics/AutofacMetrics.cs +++ b/src/Autofac/Diagnostics/AutofacMetrics.cs @@ -147,22 +147,21 @@ static AutofacMetrics() /// The kind of collection (e.g., standard, any-keyed). /// Additional detail such as the service description. /// The number of elements added to the collection. - /// The elapsed stopwatch ticks for the build. - public static void RecordCollectionBuild(string kind, string? detail, int itemCount, long elapsedTicks) + /// The elapsed time for the build. + public static void RecordCollectionBuild(string kind, string? detail, int itemCount, TimeSpan elapsed) { - if (!MetricsEnabled || CollectionBuildDuration is null || elapsedTicks <= 0) + if (!MetricsEnabled || CollectionBuildDuration is null || elapsed <= TimeSpan.Zero) { return; } - var seconds = elapsedTicks / (double)Stopwatch.Frequency; var tags = new TagList { { "autofac.collection.kind", kind }, { "autofac.collection.detail", detail ?? "" }, }; - CollectionBuildDuration.Record(seconds, tags); + CollectionBuildDuration.Record(elapsed.TotalSeconds, tags); CollectionBuildCount?.Add(1, tags); CollectionItemCount?.Add(itemCount, tags); } @@ -173,22 +172,21 @@ public static void RecordCollectionBuild(string kind, string? detail, int itemCo /// The concrete instance type. /// The number of properties evaluated for injection. /// The number of properties that were assigned. - /// The elapsed stopwatch ticks for the injection. - public static void RecordPropertyInjection(Type instanceType, int inspectedProperties, int assignedProperties, long elapsedTicks) + /// The elapsed time for the injection. + public static void RecordPropertyInjection(Type instanceType, int inspectedProperties, int assignedProperties, TimeSpan elapsed) { - if (!MetricsEnabled || PropertyInjectionDuration is null || elapsedTicks <= 0) + if (!MetricsEnabled || PropertyInjectionDuration is null || elapsed <= TimeSpan.Zero) { return; } - var seconds = elapsedTicks / (double)Stopwatch.Frequency; var tags = new TagList { { "autofac.property.type", instanceType.FullName ?? instanceType.Name }, { "autofac.property.inspected", inspectedProperties }, }; - PropertyInjectionDuration.Record(seconds, tags); + PropertyInjectionDuration.Record(elapsed.TotalSeconds, tags); PropertyInjectionCount?.Add(1, tags); PropertyInjectionAssignments?.Add(assignedProperties, tags); } @@ -197,21 +195,20 @@ public static void RecordPropertyInjection(Type instanceType, int inspectedPrope /// Records a reflection-based activation event. /// /// The activated implementation type. - /// The elapsed stopwatch ticks for activation. - public static void RecordReflectionActivation(Type implementationType, long elapsedTicks) + /// The elapsed time for activation. + public static void RecordReflectionActivation(Type implementationType, TimeSpan elapsed) { - if (!MetricsEnabled || ReflectionActivationDuration is null || elapsedTicks <= 0) + if (!MetricsEnabled || ReflectionActivationDuration is null || elapsed <= TimeSpan.Zero) { return; } - var seconds = elapsedTicks / (double)Stopwatch.Frequency; var tags = new TagList { { "autofac.reflection.type", implementationType.FullName ?? implementationType.Name }, }; - ReflectionActivationDuration.Record(seconds, tags); + ReflectionActivationDuration.Record(elapsed.TotalSeconds, tags); } /// @@ -219,45 +216,43 @@ public static void RecordReflectionActivation(Type implementationType, long elap /// /// The lock category (e.g., service or lifetime scope). /// Additional details about the lock, if any. - /// The time spent waiting, in stopwatch ticks. - public static void RecordLockContention(string category, string? detail, long elapsedTicks) + /// The time spent waiting. + public static void RecordLockContention(string category, string? detail, TimeSpan elapsed) { - if (!MetricsEnabled || LockContentionDuration is null || elapsedTicks <= 0) + if (!MetricsEnabled || LockContentionDuration is null || elapsed <= TimeSpan.Zero) { return; } - var seconds = elapsedTicks / (double)Stopwatch.Frequency; var tags = new TagList { { "autofac.lock.category", category }, { "autofac.lock.detail", detail ?? "" }, }; - LockContentionDuration.Record(seconds, tags); + LockContentionDuration.Record(elapsed.TotalSeconds, tags); LockContentionCount?.Add(1, tags); - LockContentionTotalTime?.Add(seconds, tags); + LockContentionTotalTime?.Add(elapsed.TotalSeconds, tags); } /// /// Records a resolve pipeline middleware execution event. /// /// The middleware name. - /// The elapsed stopwatch ticks for the execution. - public static void RecordMiddlewareExecution(string middlewareName, long elapsedTicks) + /// The elapsed time for the execution. + public static void RecordMiddlewareExecution(string middlewareName, TimeSpan elapsed) { - if (!MetricsEnabled || MiddlewareExecutionDuration is null || elapsedTicks <= 0) + if (!MetricsEnabled || MiddlewareExecutionDuration is null || elapsed <= TimeSpan.Zero) { return; } - var seconds = elapsedTicks / (double)Stopwatch.Frequency; var tags = new TagList { { "autofac.middleware.name", middlewareName }, }; - MiddlewareExecutionDuration.Record(seconds, tags); + MiddlewareExecutionDuration.Record(elapsed.TotalSeconds, tags); MiddlewareExecutionCount?.Add(1, tags); } diff --git a/src/Autofac/Diagnostics/ValueStopwatch.cs b/src/Autofac/Diagnostics/ValueStopwatch.cs index 0360f88de..ef9fa4ad1 100644 --- a/src/Autofac/Diagnostics/ValueStopwatch.cs +++ b/src/Autofac/Diagnostics/ValueStopwatch.cs @@ -1,15 +1,22 @@ // Copyright (c) Autofac Project. All rights reserved. // Licensed under the MIT License. See LICENSE in the project root for license information. +// Original version from https://github.com/dotnet/aspnetcore/blob/main/src/Shared/ValueStopwatch/ValueStopwatch.cs +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics; namespace Autofac.Diagnostics; /// -/// Lightweight stopwatch with no heap allocations. +/// Lightweight stopwatch for timing without heap allocations. /// -internal readonly struct ValueStopwatch +internal struct ValueStopwatch { +#if !NET7_0_OR_GREATER + private static readonly double TimestampToTicks = TimeSpan.TicksPerSecond / (double)Stopwatch.Frequency; +#endif + private readonly long _startTimestamp; private ValueStopwatch(long startTimestamp) @@ -18,29 +25,48 @@ private ValueStopwatch(long startTimestamp) } /// - /// Gets the number of elapsed ticks. + /// Gets a value indicating whether the stopwatch has been started. /// - public long ElapsedTicks - { - get - { - if (_startTimestamp == 0) - { - return 0; - } + public bool IsActive => _startTimestamp != 0; - return Stopwatch.GetTimestamp() - _startTimestamp; - } - } + /// + /// Starts a new stopwatch instance. + /// + /// A running . + public static ValueStopwatch StartNew() => new ValueStopwatch(Stopwatch.GetTimestamp()); /// - /// Gets the elapsed duration in milliseconds. + /// Computes elapsed time between two timestamps captured from . /// - public double ElapsedMilliseconds => ElapsedTicks * 1000.0 / Stopwatch.Frequency; + /// The starting timestamp. + /// The ending timestamp. + /// The elapsed time between the timestamps. + public static TimeSpan GetElapsedTime(long startingTimestamp, long endingTimestamp) + { +#if !NET7_0_OR_GREATER + var timestampDelta = endingTimestamp - startingTimestamp; + var ticks = (long)(TimestampToTicks * timestampDelta); + return new TimeSpan(ticks); +#else + return Stopwatch.GetElapsedTime(startingTimestamp, endingTimestamp); +#endif + } /// - /// Starts a new stopwatch instance. + /// Gets the elapsed time since the stopwatch started. /// - /// A running . - public static ValueStopwatch StartNew() => new(Stopwatch.GetTimestamp()); + /// The elapsed time. + public TimeSpan GetElapsedTime() + { + // Start timestamp can't be zero in an initialized ValueStopwatch. It would have to be literally the first thing executed when the machine boots to be 0. + // So it being 0 is a clear indication of default(ValueStopwatch) + if (!IsActive) + { + throw new InvalidOperationException("An uninitialized, or 'default', ValueStopwatch cannot be used to get elapsed time."); + } + + var end = Stopwatch.GetTimestamp(); + + return GetElapsedTime(_startTimestamp, end); + } } diff --git a/src/Autofac/Features/Collections/CollectionRegistrationSource.cs b/src/Autofac/Features/Collections/CollectionRegistrationSource.cs index 0e51ab0d2..26ea71705 100644 --- a/src/Autofac/Features/Collections/CollectionRegistrationSource.cs +++ b/src/Autofac/Features/Collections/CollectionRegistrationSource.cs @@ -278,7 +278,7 @@ private static IList BuildCollection( kind: collectionKind, detail: collectionDetail, itemCount: registrations.Count, - elapsedTicks: instrumentationTimer.ElapsedTicks); + elapsed: instrumentationTimer.GetElapsedTime()); } return output; diff --git a/src/Autofac/Features/Decorators/DecoratorMiddleware.cs b/src/Autofac/Features/Decorators/DecoratorMiddleware.cs index b5bb563c7..ef86d6d70 100644 --- a/src/Autofac/Features/Decorators/DecoratorMiddleware.cs +++ b/src/Autofac/Features/Decorators/DecoratorMiddleware.cs @@ -1,7 +1,6 @@ // Copyright (c) Autofac Project. All rights reserved. // Licensed under the MIT License. See LICENSE in the project root for license information. -using System.Diagnostics; using Autofac.Core; using Autofac.Core.Registration; using Autofac.Core.Resolving; @@ -41,14 +40,14 @@ public void Execute(ResolveRequestContext context, Action return; } - var start = Stopwatch.GetTimestamp(); + var timer = ValueStopwatch.StartNew(); try { ExecuteCore(context, next); } finally { - AutofacMetrics.RecordMiddlewareExecution(nameof(DecoratorMiddleware), Stopwatch.GetTimestamp() - start); + AutofacMetrics.RecordMiddlewareExecution(nameof(DecoratorMiddleware), timer.GetElapsedTime()); } } From b48caf314b3fb14c20b721003c97a92bbb368d34 Mon Sep 17 00:00:00 2001 From: Travis Illig Date: Thu, 26 Feb 2026 15:38:26 -0800 Subject: [PATCH 12/15] Clarification that Markdown comes out by default. --- bench/Autofac.Benchmarks/Program.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/bench/Autofac.Benchmarks/Program.cs b/bench/Autofac.Benchmarks/Program.cs index ad3b56edb..6e98befa4 100644 --- a/bench/Autofac.Benchmarks/Program.cs +++ b/bench/Autofac.Benchmarks/Program.cs @@ -20,6 +20,8 @@ internal static void Main(string[] args) // // Run the benchmark comparing the source code version to a specific package version: // dotnet run -c Release --project bench/Autofac.Benchmarks -- --baseline-version 9.0.0 --filter *Benchmarks* + // + // Markdown tables are emitted by default; see BenchmarkDotNet.Artifacts/.../results/*.md. var config = new BenchmarkConfig(); config.AddJob( From 104c718b132735e127bf7ea835d5a19fb822133c Mon Sep 17 00:00:00 2001 From: Travis Illig Date: Thu, 26 Feb 2026 15:45:01 -0800 Subject: [PATCH 13/15] Micro-optimization to skip calculations for metrics. --- .../CollectionRegistrationSource.cs | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/src/Autofac/Features/Collections/CollectionRegistrationSource.cs b/src/Autofac/Features/Collections/CollectionRegistrationSource.cs index 26ea71705..79b96e437 100644 --- a/src/Autofac/Features/Collections/CollectionRegistrationSource.cs +++ b/src/Autofac/Features/Collections/CollectionRegistrationSource.cs @@ -125,10 +125,17 @@ public IEnumerable RegistrationsFor(Service service, Fun .ConvertAll(static tuple => ((Service)tuple.KeyedService, tuple.Registration)) : BuildStandardRegistrationList(c.ComponentRegistry, elementTypeService); - var collectionKind = isAnyKeyQuery ? "any-keyed" : "standard"; - var collectionDetail = isAnyKeyQuery - ? elementType.FullName ?? elementType.Name - : elementTypeService.ToString() ?? elementTypeService.GetType().Name; + string? collectionKind = null; + string? collectionDetail = null; + if (AutofacMetrics.MetricsEnabled) + { + // The collection kind and detail are only used in recording metrics. + collectionKind = isAnyKeyQuery ? "any-keyed" : "standard"; + collectionDetail = isAnyKeyQuery + ? elementType.FullName ?? elementType.Name + : elementTypeService.ToString() ?? elementTypeService.GetType().Name; + } + return BuildCollection(c, factory, registrationTuples, p, collectionKind, collectionDetail); }); @@ -243,9 +250,10 @@ private static IList BuildCollection( Func factory, List<(Service Service, ServiceRegistration Registration)> registrations, IEnumerable parameters, - string collectionKind, - string collectionDetail) + string? collectionKind, + string? collectionDetail) { + // Collection kind and detail will be null unless metrics are enabled. var recordMetrics = AutofacMetrics.MetricsEnabled; ValueStopwatch instrumentationTimer = default; if (recordMetrics) @@ -275,7 +283,7 @@ private static IList BuildCollection( if (recordMetrics) { AutofacMetrics.RecordCollectionBuild( - kind: collectionKind, + kind: collectionKind!, detail: collectionDetail, itemCount: registrations.Count, elapsed: instrumentationTimer.GetElapsedTime()); From 43df76e85918ea61fb220a62f7d4a0d3abe06c41 Mon Sep 17 00:00:00 2001 From: Travis Illig Date: Fri, 27 Feb 2026 07:07:46 -0800 Subject: [PATCH 14/15] Excluding metrics items from code coverage. --- src/Autofac/Diagnostics/AutofacMetrics.cs | 1 + src/Autofac/Diagnostics/ValueStopwatch.cs | 1 + 2 files changed, 2 insertions(+) diff --git a/src/Autofac/Diagnostics/AutofacMetrics.cs b/src/Autofac/Diagnostics/AutofacMetrics.cs index 4a41013c3..80cbd4103 100644 --- a/src/Autofac/Diagnostics/AutofacMetrics.cs +++ b/src/Autofac/Diagnostics/AutofacMetrics.cs @@ -9,6 +9,7 @@ namespace Autofac.Diagnostics; /// /// Centralized metrics wiring for Autofac diagnostics. /// +[ExcludeFromCodeCoverage] internal static class AutofacMetrics { private const string DiagnosticsEnvironmentVariable = "AUTOFAC_METRICS"; diff --git a/src/Autofac/Diagnostics/ValueStopwatch.cs b/src/Autofac/Diagnostics/ValueStopwatch.cs index ef9fa4ad1..9d1331e6e 100644 --- a/src/Autofac/Diagnostics/ValueStopwatch.cs +++ b/src/Autofac/Diagnostics/ValueStopwatch.cs @@ -11,6 +11,7 @@ namespace Autofac.Diagnostics; /// /// Lightweight stopwatch for timing without heap allocations. /// +[ExcludeFromCodeCoverage] internal struct ValueStopwatch { #if !NET7_0_OR_GREATER From 5aaee7bbda8e72a6490303455a9930cd6f2fe9f5 Mon Sep 17 00:00:00 2001 From: Travis Illig Date: Fri, 27 Feb 2026 07:29:02 -0800 Subject: [PATCH 15/15] Update documentation on lock contention metrics. --- src/Autofac/Diagnostics/AutofacMetrics.cs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/Autofac/Diagnostics/AutofacMetrics.cs b/src/Autofac/Diagnostics/AutofacMetrics.cs index 80cbd4103..7583d2319 100644 --- a/src/Autofac/Diagnostics/AutofacMetrics.cs +++ b/src/Autofac/Diagnostics/AutofacMetrics.cs @@ -213,10 +213,17 @@ public static void RecordReflectionActivation(Type implementationType, TimeSpan } /// - /// Records the wait duration for a lock contention event. + /// Records the wait duration for a lock contention event. This may include + /// time spent waiting as well as acquiring the lock, but will be closely + /// correlated with contention time. /// /// The lock category (e.g., service or lifetime scope). - /// Additional details about the lock, if any. + /// + /// Additional details about the lock, if any. Note this will likely + /// generate high-cardinality metrics in a production environment since it + /// will track information about services and lifetime scopes acquiring + /// locks and include identities for each. + /// /// The time spent waiting. public static void RecordLockContention(string category, string? detail, TimeSpan elapsed) {