Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Performance Degradation in version 9.0.0 #118

Closed
andrewaramsay opened this issue Apr 9, 2024 · 2 comments · Fixed by #119
Closed

Performance Degradation in version 9.0.0 #118

andrewaramsay opened this issue Apr 9, 2024 · 2 comments · Fixed by #119

Comments

@andrewaramsay
Copy link

Describe the bug
There is a significant performance degradation introduced in version 9.0.0 that was not present in 8.0.0. My traces led me to KeyedServiceMiddleware line 22 as suspect. As further evidence that the issue comes from this change, if I do not call Populate and just use AutofacServiceProvider directly, this line of code is not executed and the performance issues are not observed. This is obviously, not a suitable workaround, as it defeats half the purpose of this library, but is useful troubleshooting information.

These reproduction steps are in a benchmark, but we also witnessed this issue as about a 2x increase in CPU utilization and eventual application failure in a full fledged application while undergoing performance stress testing.

To reproduce
Reproduction of the issue, ideally in a unit test format.

  • Wire up 1000 services in a dependency graph.
  • Register each of these services with Autofac's ContainerBuilder.
  • Create a ServiceCollection, register some arbitrary service, and call Populate to load it into the container builder.
    • NOTE: The service loaded in this fashion isn't of importance, we just need the KeyedServiceMiddleware to be added to the resolve pipeline.
  • Create an AutofacServiceProvider using the built container.
  • Resolve the top-level service of this 1000 service graph in a benchmark.
  • Run this benchmark for version 9.0.0 and prior versions (listed here are the old version we were upgrading from, as well as the version immediately below 9.0.0)
public interface ISomeInterface
{
    void DoWork();
}

public class OuterMostService(KayleighVaughan _dep) : ISomeInterface { public void DoWork() => _dep.DoWork(); }
public class OuterMostServiceShallow : ISomeInterface { public void DoWork() { } }

[Config(typeof(Config))]
public class AutofacBenchmarks
{
    private IServiceProvider _serviceProvider = null!;

    [GlobalSetup]
    public void Setup()
    {
        _serviceProvider = new AutofacServiceProvider(GetContainer().Build());
    }

    private ContainerBuilder GetContainer()
    {
        var services = new ServiceCollection();

        services.AddSingleton<OuterMostServiceShallow>();

        var builder = new ContainerBuilder();
        // not present in benchmark, but omitting this line results in 8.0.0 level performance
        builder.Populate(services);

        builder.RegisterType<OuterMostService>().AsSelf().AsImplementedInterfaces().InstancePerDependency();
        // Extension method, registers 1000 services in a tree structure
        builder.RegisterFakeServices();

        return builder;
    }

    [Benchmark]
    public void DirectResolveDeep()
    {
        _serviceProvider.GetRequiredService<OuterMostService>();
    }

    private class Config : ManualConfig
    {
        public Config()
        {
            var baseJob = Job.Default;
            AddJob(baseJob.WithNuGet([
                new NuGetReference("Autofac", "4.9.1"),
                new NuGetReference("Autofac.Extensions.DependencyInjection", "5.0.1"),
                new NuGetReference("Microsoft.Extensions.DependencyInjection", "3.1.14"),
            ]).AsBaseline());

            AddJob(baseJob.WithNuGet([
                new NuGetReference("Autofac", "8.0.0"),
                new NuGetReference("Autofac.Extensions.DependencyInjection", "8.0.0"),
                new NuGetReference("Microsoft.Extensions.DependencyInjection", "8.0.0"),
            ]));

            AddJob(baseJob.WithNuGet([
                new NuGetReference("Autofac", "8.0.0"),
                new NuGetReference("Autofac.Extensions.DependencyInjection", "9.0.0"),
                new NuGetReference("Microsoft.Extensions.DependencyInjection", "8.0.0"),
            ]));
        }
    }
}

public static class RegistrationExtensions
{
    public static void RegisterFakeServices(this ContainerBuilder builder)
    {
        builder.RegisterType<KayleighVaughan>().AsSelf().InstancePerDependency();
        builder.RegisterType<CastielQuintana>().AsSelf().InstancePerDependency();
        // 1000 services registered in this fashion
    }
}
public class KayleighVaughan(CastielQuintana _dep0, KeniaBennett _dep1, LeonardoFrost _dep2, PaulaGregory _dep3) { public void DoWork() { _dep0.DoWork(); _dep1.DoWork(); _dep2.DoWork(); _dep3.DoWork(); } }
public class CastielQuintana(TravisHuff _dep0, KarsynBradshaw _dep1, EmoryCrane _dep2, DellaFlynn _dep3) { public void DoWork() { _dep0.DoWork(); _dep1.DoWork(); _dep2.DoWork(); _dep3.DoWork(); } }
// ... more services wired up in a 1 parent, 4 children structure until I run out of dependencies, then the rest are leaf nodes.
public class SarahTruong { public void DoWork() { } }
public class AyanHobbs { public void DoWork() { } }

Full exception with stack trace:

No Exception - see benchmark results

Assembly/dependency versions:

<!-- See Config private class in C# code to multi-nuget-package version benchmarks -->
    <PackageReference Include="Autofac" Version="4.9.1" />
    <PackageReference Include="Autofac.Extensions.DependencyInjection" Version="5.0.1" />
    <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.1.14" />
    <PackageReference Include="BenchmarkDotNet" Version="0.13.12" />

Additional context
Benchmark results (sorry it's a screenshot, I don't have the console output on hand right now)
image

@andrewaramsay
Copy link
Author

Here's a zip of the benchmark project I used. The code above was copy/pasted piecemeal and might have small issues here or there.
AutofacBenchmark.zip

@alistairjevans
Copy link
Member

Hi @andrewaramsay, just to confirm that I can recreate your results locally;

Method Job NuGetReferences Mean Error StdDev Ratio RatioSD
DirectResolveDeep Job-XQHMUC Autofac 4.9.1,Autofac.Extensions.DependencyInjection 5.0.1,Microsoft.Extensions.DependencyInjection 3.1.14 479.8 us 7.88 us 9.07 us 1.00 0.00
DirectResolveDeep Job-VKISHO Autofac 8.0.0,Autofac.Extensions.DependencyInjection 8.0.0,Microsoft.Extensions.DependencyInjection 8.0.0 475.1 us 2.32 us 2.06 us 0.99 0.02
DirectResolveDeep Job-YSFDHO Autofac 8.0.0,Autofac.Extensions.DependencyInjection 9.0.0,Microsoft.Extensions.DependencyInjection 8.0.0 941.8 us 9.18 us 7.67 us 1.95 0.04

Good find, and thank you for raising; I'll try to investigate this, and suspect you're right about keyed services being to blame; the challenge we'll likely have is simply that adding support for MSDI-style keyed services will slow down Autofac, because we are required to do more work.

In addition to the list allocation, likely candidates for the resolve overhead are the call to GetCustomAttributes in the ResolvedParameter call.

I'll have to experiment here with options for reducing the overhead.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
2 participants