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

WIP: Pipelines #1121

Merged
merged 11 commits into from May 27, 2020
Merged

WIP: Pipelines #1121

merged 11 commits into from May 27, 2020

Conversation

alistairjevans
Copy link
Member

I've started very early stuff on using pipelines internally, for #1096. Thought I'd raise a WIP pull request to give some visibility of what's happening in my fork.

You can expect plenty of churn as I go...

@alistairjevans
Copy link
Member Author

Tests are passing; refactoring and additional tests needed.

Also some performance investigation needs to happen; I'm seeing about a 300ns additional cost on all resolves. I'll take a look at it.

@alistairjevans
Copy link
Member Author

Had to rework some stuff, including some improvements in DefaultRegisteredServicesTracker, but difference is down to 100ns per-resolve for simple cases. Also managed to reduce the number of allocations:

Develop - Container Resolve

|                     Method |       Mean |     Error |    StdDev |  Ratio | RatioSD |  Gen 0 | Gen 1 | Gen 2 | Allocated |
|--------------------------- |-----------:|----------:|----------:|-------:|--------:|-------:|------:|------:|----------:|
|                OperatorNew |   3.514 ns | 0.0132 ns | 0.0110 ns |   1.00 |    0.00 | 0.0057 |     - |     - |      24 B |
| NonSharedReflectionResolve | 481.038 ns | 4.0133 ns | 3.7540 ns | 136.92 |    1.38 | 0.1640 |     - |     - |     688 B |
|   NonSharedDelegateResolve | 390.197 ns | 2.0279 ns | 1.8969 ns | 111.05 |    0.56 | 0.1278 |     - |     - |     536 B |
|              SharedResolve | 348.588 ns | 1.3737 ns | 1.1471 ns |  99.19 |    0.44 | 0.1221 |     - |     - |     512 B |

Pipelines - Container Resolve

|                     Method |       Mean |     Error |    StdDev |  Ratio | RatioSD |  Gen 0 | Gen 1 | Gen 2 | Allocated |
|--------------------------- |-----------:|----------:|----------:|-------:|--------:|-------:|------:|------:|----------:|
|                OperatorNew |   3.544 ns | 0.0282 ns | 0.0263 ns |   1.00 |    0.00 | 0.0057 |     - |     - |      24 B |
| NonSharedReflectionResolve | 585.601 ns | 2.7999 ns | 2.3380 ns | 165.17 |    1.63 | 0.1621 |     - |     - |     680 B |
|   NonSharedDelegateResolve | 487.122 ns | 3.2514 ns | 2.8823 ns | 137.41 |    0.75 | 0.1259 |     - |     - |     528 B |
|              SharedResolve | 426.385 ns | 2.8360 ns | 2.3682 ns | 120.26 |    0.72 | 0.1202 |     - |     - |     504 B |

A new thing I realised; every time you reference a method (instance of static), it creates a new Action or Func, so in the super hot paths in a few places I've cached a method reference in the constructor:

// The RegistrationsFor is an instance method reference.
foreach (var provided in next.RegistrationsFor(service, RegistrationsFor))
{
   // do stuff
}

That compiles to:

Func<Service, IEnumerable<IComponentRegistration>> registrationAccessor = new Func<Service, IEnumerable<IComponentRegistration>>((object) registeredServicesTracker, __vmethodptr(registeredServicesTracker, RegistrationsFor));
foreach (IComponentRegistration registration in next.RegistrationsFor(service1, registrationAccessor))
{
   // do stuff
}

So I changed it to use a Func instance created in the constructor to save the additional allocation memory and time.


Child Scope resolve is still a bit problematic:

Develop
|  Method |     Mean |    Error |   StdDev |   Gen 0 | Gen 1 | Gen 2 | Allocated |
|-------- |---------:|---------:|---------:|--------:|------:|------:|----------:|
| Resolve | 51.49 us | 0.342 us | 0.303 us | 11.9019 |     - |     - |  48.84 KB |

Pipelines
|  Method |     Mean |    Error |   StdDev |   Gen 0 | Gen 1 | Gen 2 | Allocated |
|-------- |---------:|---------:|---------:|--------:|------:|------:|----------:|
| Resolve | 58.84 us | 0.415 us | 0.389 us | 11.4746 |     - |     - |  47.29 KB |

That's because a pipeline needs to be built in each nested lifetime scope for any dependencies bought in via the ExternalRegistrySource. I'm going to have to see if there's a way to reference the outer component pipeline without re-building it.

Before I do that though I'm going to get everything ready for the first review.

@tillig
Copy link
Member

tillig commented May 18, 2020

This is exciting stuff! I've been loosely following as the push notifications come through and to see you have this working to within a pretty reasonable set of tolerances to the original is sweet. I'll pull it down local and try it out when it's ready.

From a design perspective, have you noticed anything interesting in comparing/contrasting with the original design about how things are... easier/harder to work with? more/less extensible?

@alistairjevans
Copy link
Member Author

The design makes it much easier to insert things 'in the middle', as it where. For example, attaching something that runs before circular dependency detection, or between sharing return and decorator lookup.

The pipeline mechanism also makes it easy to 'bail' at any point (for example, in the sharing middleware) and not continue down the path.

The extensibility is great; should make it easier to do some of the more interesting stuff that we would like to do in the integrations.

Personally I find the execution path slightly easier to parse in my head now, but that might be because I wrote it.

There are some additional risks with the added flexibility this grants you; technically users could insert a pipeline stage that really breaks things. There's a couple of mitigations to that:

  • I've attempted to reserve such risks for advanced users by putting the direct middleware configuration behind a ComponentPipeline property in the RegistrationBuilder, so the intellisense doesn't immediately give them dangerous tools at the same level as As<>, OnActivating, etc.
  • Because the default stages are inserted just before pipeline construction, they pretty much take priority, and prevent users from accidentally replacing the activator.

One exciting thing, I don't know if you've seen, but early traceability is already in there (I had to add it to figure out a problem I was having...).

For example, the following code:

var builder = new ContainerBuilder();
builder.RegisterType<A>().InstancePerLifetimeScope();

var container = builder.Build();

var lifetime = container.BeginLifetimeScope();

// Create a tracer.
var tracer = new DefaultDiagnosticTracer();

// Get notified when operation trace content is ready.
tracer.OperationCompleted += (sender, args) =>
{
    Console.WriteLine(args.TraceContent);
};

// Use the tracer.
lifetime.TraceWith(tracer);

var ctxA = lifetime.Resolve<A>();
var ctxA2 = lifetime.Resolve<A>();

Tracing is inherited between lifetime scopes; so if you add tracing at the container level, all scopes get it, but you can apply it per-scope as I've done here.

This gives you some output:

Resolve Operation Starting
{
  Resolve Request Starting
  {
    Service: Autofac.Specification.Test.Lifetime.InstancePerLifetimeScopeTests+A
    Component: Autofac.Specification.Test.Lifetime.InstancePerLifetimeScopeTests+A

    Pipeline:
    -> CircularDependencyDetectorStage
      -> ScopeSelectionStage
        -> DecoratorResolvingStage
          -> SharingStage
            -> ActivatorErrorHandlingStage
              -> DisposalTrackingStage
                -> A (ReflectionActivator)
                <- A (ReflectionActivator)
              <- DisposalTrackingStage
            <- ActivatorErrorHandlingStage
          <- SharingStage
        <- DecoratorResolvingStage
      <- ScopeSelectionStage
    <- CircularDependencyDetectorStage
  }
  Resolve Request Succeeded; result instance was Autofac.Specification.Test.Lifetime.InstancePerLifetimeScopeTests+A
}
Operation Succeeded; result instance was Autofac.Specification.Test.Lifetime.InstancePerLifetimeScopeTests+A

Resolve Operation Starting
{
  Resolve Request Starting
  {
    Service: Autofac.Specification.Test.Lifetime.InstancePerLifetimeScopeTests+A
    Component: Autofac.Specification.Test.Lifetime.InstancePerLifetimeScopeTests+A

    Pipeline:
    -> CircularDependencyDetectorStage
      -> ScopeSelectionStage
        -> DecoratorResolvingStage
          -> SharingStage
          <- SharingStage
        <- DecoratorResolvingStage
      <- ScopeSelectionStage
    <- CircularDependencyDetectorStage
  }
  Resolve Request Succeeded; result instance was Autofac.Specification.Test.Lifetime.InstancePerLifetimeScopeTests+A
}
Operation Succeeded; result instance was Autofac.Specification.Test.Lifetime.InstancePerLifetimeScopeTests+A

Note that the second request ended at the sharing stage.

@tillig
Copy link
Member

tillig commented May 18, 2020

Wow, that's so cool! I saw there was some tracing but since I hadn't really executed anything with it yet I didn't entirely grasp what all was going on. Nice!

@alistairjevans
Copy link
Member Author

Yeah, I'm fairly pleased with it; people can write their own tracers if they want, and I can imagine the benefits of adding things like a TimingTracer or something, either as part of the main library, or an integration package.

I've got some renames and tidy-up to do, but the whole thing should be ready for consumption in a day or two.

Squashed commit of previous related work because there was a lot of discarded changes early on that won't make sense.
@alistairjevans
Copy link
Member Author

Ok; I'm happy with naming, I've squashed most of the commits for pipelines, because the early ones were off base and would have caused confusion. Any subsequent changes (as an output of review) will not be squashed.

This is ready for consumption now; I'm going to take this out of draft, then I'll write some more words in here.

@alistairjevans alistairjevans marked this pull request as ready for review May 20, 2020 15:12
@alistairjevans
Copy link
Member Author

Slight addendum; there are still some additional tests I need to add by the way, but thought I'd get it ready for people to look at.

There are also additional changes for pipelines that I'd like to make before the v6 release (namely around how decorators work). I wanted to get the first batch of changes signed off though before I do that.

@alistairjevans alistairjevans changed the base branch from develop to v6 May 20, 2020 15:24
@alistairjevans
Copy link
Member Author

Pipelines

I'm going to have a go at describing how the new pipelines functionality works here.

Concepts

A 'Resolve Pipeline' is made up of a sequence of middleware. Each middleware item defines:

  • A phase, which defines rough ordering within the pipeline.
  • The Execute method, which is invoked when the middleware executes.

The execute method takes a context object for the request, and a callback to invoke that proceeds to the 'next' piece of middleware.

Here's what a piece of middleware looks like:

/// <summary>
/// A middleware example.
/// </summary>
internal class MyCustomMiddleware : IResolveMiddleware
{
    public PipelinePhase Phase => PipelinePhase.Activation;

    public void Execute(IResolveRequestContext context, Action<IResolveRequestContext> next)
    {
        // Do something before subsequent pipeline stages.
        // Manipulate the context and access content.
        // Some sample properties:
        //  - context.Registration
        //  - context.Parameters
        //  - more...

        try
        {
            // context.Instance will be null

            next(context);

            // context.Instance should be populated now
        }
        catch(Exception ex)
        {
            // Handle errors from later in the pipeline.
        }
        finally
        {
            // Do stuff whether the pipeline starts or fails.
        }
    }
}

When all the middleware in the pipeline has been executed, the next method call in each piece of middleware will exit, running back up the pipeline.
So the first middleware that executes is also the last one to inspect/modify the request outcome.

Registration and Pipeline Building

Pipeline Construction

During registration, there is a ResolvePipeline property in the RegistrationBuilder. This property is an instance of IResolvePipelineBuilder,
backed by the ResolvePipelineBuilder class. This class is responsible for maintaining a mutable set of middleware declarations, that allows insertion into the
appropriate place in the pipeline.

At registration time, prior to Container.Build being called, consumers of Autofac can add custom middleware to the pipeline by accessing one of the Use methods on IResolvePipelineBuilder to insert a piece of middleware:

var builder = new ContainerBuilder();

var registration = builder.Register(ctxt => new A()).As<IA>();

// Instance middleware.
registration.ResolvePipeline.Use(new MyCustomMiddleware());

// Delegate middleware.
registration.ResolvePipeline.Use(PipelinePhase.ParameterSelection, (ctxt, next) =>
{
    // Do your middleware stuff.
    next(ctxt);
});

var container = registration.Build();

The existing OnPreparing, OnActivating and OnActivated handlers are now actually implemented as middleware insertions.

Built-in Middleware

The 'default' Autofac behaviour, around sharing, activation, etc, is all added last, when the user calls Build on the container.

This is done for two reasons:

  1. By inserting our middleware at the end, we can control more precisely the order in which it runs.
  2. We can use the services declared in the built container when making decisions about which middleware to add (if we need to).

When the container Build operation is invoked, we iterate over all added registrations, and invoke the IComponentRegistration.BuildResolvePipeline method.

This method does the following:

  1. Raises a PipelineBuilding event, which allows things like modules that attach to registrations (or anything that watches the 'Registered' event) to add their own middleware.
  2. Adds our default stages. These are mostly static singletons (they have no state of their own). Some of these are conditional.
  3. Asks the Activator to add its own pipeline middleware (more on that in a second).
  4. Builds a concrete pipeline for the registration.

Activators have a slightly special case, because of changes I think it would be good to have in the future. IInstanceActivator no longer has an ActivateInstance method!

Instead, it has a ConfigurePipeline method. In this method the activator can add whatever stages it wants to. In addition, the activator receives an interface, IComponentRegistryServices that exposes the complete set of available services at the moment of container build.

This will enable activators to make choices about what stages to add at container build time,
and do pre-processing.

Check out the updated ReflectionActivator for the constructor processing it does in the ConfigurePipelines method, whereas before it had to take a lock during the first Activation.

Dynamic Registrations

Dynamic registrations (from registration sources at execution time) also build the component pipeline in the same way, only instead of building pipelines when the container is built, they
are built when a registration is added to the DefaultRegisteredServicesTracker. The pipeline build process works exactly the same way, but it runs inside the scope of adding that dynamic registration.

Generating the Concrete Pipeline

Calling Build on the IResolvePipelineBuilder takes the middleware declarations and generates a concrete Action -> Action -> Action callchain that invokes each middleware in turn, and outputs an IResolvePipeline instance, which is just a wrapper around a single callback which represents the entry point into the pipeline.

When building the pipeline, the last middleware in the pipeline receives a special 'terminating' action as its next method, that ends the pipeline, but can also invoke a Continuation pipeline and keep running a different pipeline entirely.

I think this mechanism is going to be used to be able to define a service-specific pipeline that will continue executing a registration's pipeline inside the scope of the service-specific middleware. Decorators may end up being like this before 6.0 goes out.

Resolving

The resolve process is probably the easy bit!

The ResolveOperation class has been replaced with a mechanism that, when GetOrCreateInstance is called:

  • Retrieves the built IResolvePipeline from the registration.
  • Instantiates a ResolveRequestContext, populates it with the initial context.
  • Tracks the active request context and its initial scope.
  • Invokes the pipeline.
  • Returns the IResolveRequestContext.Instance value when the pipeline has finished.

The resolve operation does not do any circular dependency detection. This is now done inside
a built-in middleware. This allows middleware to execute before circular dependency validation, which I can imagine would be handy.

The IResolveRequestContext also implements IComponentContext, so additional services can be resolved from it. When a middleware stage calls ResolveComponent on the IResolveRequestContext, it creates a new IResolveRequestContext and executes it within the context of the operation, but using whichever ILifetimeScope has been determined at that point in the pipeline.

In addition, to handle scenarios that need to resolve a service while using a completely different circular dependency stack (decorators need to do this), there is a IResolveRequestContext.ResolveComponentWithNewOperation that creates a new ResolveOperation with a completely blank stack. This is considered 'advanced' usage, but the reason someone should use this instead of ILifetimeScope.Resolve is that tracing will maintain the relationship between the operations.

Speaking of tracing....

Traceability

One of the great features that have come out of this pipelines work is the ability to trace resolve operations. For diagnostics it's going to be super useful,
especially in confusing issues, where we can ask people to supply a trace of the pipeline.

It's easy to add our default tracing:

var builder = new ContainerBuilder();
builder.RegisterType<ImplementorA>().As<IDecoratedService>();
builder.RegisterDecorator<DecoratorA, IDecoratedService>();
var container = builder.Build();

container.AttachTrace((req, trace) =>
{
    Console.WriteLine(trace);
});

var factory = container.Resolve<Func<IDecoratedService>>();

var decoratedService = factory();

That will give us two trace events, with the following content (one for the Func resolve and one when the factory method is invoked):

Resolve Operation Starting
{
  Resolve Request Starting
  {
    Service: System.Func`1[[Autofac.Specification.Test.Features.DecoratorTests+IDecoratedService, Autofac.Specification.Test, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null]]
    Component: λ:System.Func`1[[Autofac.Specification.Test.Features.DecoratorTests+IDecoratedService, Autofac.Specification.Test, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null]]

    Pipeline:
    -> CircularDependencyDetectorMiddleware
      -> ScopeSelectionMiddleware
        -> DecoratorMiddleware
          -> SharingMiddleware
            -> ActivatorErrorHandlingMiddleware
              -> λ:System.Func`1[[Autofac.Specification.Test.Features.DecoratorTests+IDecoratedService, Autofac.Specification.Test, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null]]
                Resolve Request Starting
                {
                  Service: Autofac.ILifetimeScope
                  Component: λ:Autofac.Core.Lifetime.LifetimeScope

                  Pipeline:
                  -> CircularDependencyDetectorMiddleware
                    -> ScopeSelectionMiddleware
                      -> DecoratorMiddleware
                        -> SharingMiddleware
                        <- SharingMiddleware
                      <- DecoratorMiddleware
                    <- ScopeSelectionMiddleware
                  <- CircularDependencyDetectorMiddleware
                }
                Resolve Request Succeeded; result instance was Autofac.Core.Lifetime.LifetimeScope
              <- λ:System.Func`1[[Autofac.Specification.Test.Features.DecoratorTests+IDecoratedService, Autofac.Specification.Test, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null]]
            <- ActivatorErrorHandlingMiddleware
          <- SharingMiddleware
        <- DecoratorMiddleware
      <- ScopeSelectionMiddleware
    <- CircularDependencyDetectorMiddleware
  }
  Resolve Request Succeeded; result instance was System.Func`1[Autofac.Specification.Test.Features.DecoratorTests+IDecoratedService]
}
Operation Succeeded; result instance was System.Func`1[Autofac.Specification.Test.Features.DecoratorTests+IDecoratedService]


Resolve Operation Starting
{
  Resolve Request Starting
  {
    Service: Autofac.Specification.Test.Features.DecoratorTests+IDecoratedService
    Component: Autofac.Specification.Test.Features.DecoratorTests+ImplementorA

    Pipeline:
    -> CircularDependencyDetectorMiddleware
      -> ScopeSelectionMiddleware
        -> DecoratorMiddleware
          -> SharingMiddleware
            -> ActivatorErrorHandlingMiddleware
              -> DisposalTrackingMiddleware
                -> ImplementorA (ReflectionActivator)
                <- ImplementorA (ReflectionActivator)
              <- DisposalTrackingMiddleware
            <- ActivatorErrorHandlingMiddleware
          <- SharingMiddleware
          Resolve Operation Starting
          {
            Resolve Request Starting
            {
              Service: Decorator (Autofac.Specification.Test.Features.DecoratorTests+IDecoratedService)
              Component: Autofac.Specification.Test.Features.DecoratorTests+DecoratorA
              Target: Autofac.Specification.Test.Features.DecoratorTests+ImplementorA

              Pipeline:
              -> CircularDependencyDetectorMiddleware
                -> ScopeSelectionMiddleware
                  -> DecoratorMiddleware
                    -> SharingMiddleware
                      -> ActivatorErrorHandlingMiddleware
                        -> DisposalTrackingMiddleware
                          -> DecoratorA (ReflectionActivator)
                          <- DecoratorA (ReflectionActivator)
                        <- DisposalTrackingMiddleware
                      <- ActivatorErrorHandlingMiddleware
                    <- SharingMiddleware
                  <- DecoratorMiddleware
                <- ScopeSelectionMiddleware
              <- CircularDependencyDetectorMiddleware
            }
            Resolve Request Succeeded; result instance was Autofac.Specification.Test.Features.DecoratorTests+DecoratorA
          }
          Operation Succeeded; result instance was Autofac.Specification.Test.Features.DecoratorTests+DecoratorA
        <- DecoratorMiddleware
      <- ScopeSelectionMiddleware
    <- CircularDependencyDetectorMiddleware
  }
  Resolve Request Succeeded; result instance was Autofac.Specification.Test.Features.DecoratorTests+DecoratorA
}
Operation Succeeded; result instance was Autofac.Specification.Test.Features.DecoratorTests+DecoratorA

Pretty cool, there's plenty of detail included.

There's also error handling in the tracer; here's what you get for a circular dependency error:

Resolve Operation Starting
{
  Resolve Request Starting
  {
    Service: Autofac.Specification.Test.Features.CircularDependency.ID
    Component: Autofac.Specification.Test.Features.CircularDependency.D

    Pipeline:
    -> CircularDependencyDetectorMiddleware
      -> ScopeSelectionMiddleware
        -> DecoratorMiddleware
          -> SharingMiddleware
            -> ActivatorErrorHandlingMiddleware
              -> DisposalTrackingMiddleware
                -> D (ReflectionActivator)
                  Resolve Request Starting
                  {
                    Service: Autofac.Specification.Test.Features.CircularDependency.IA
                    Component: Autofac.Specification.Test.Features.CircularDependency.A

                    Pipeline:
                    -> CircularDependencyDetectorMiddleware
                      -> ScopeSelectionMiddleware
                        -> DecoratorMiddleware
                          -> SharingMiddleware
                            -> ActivatorErrorHandlingMiddleware
                              -> DisposalTrackingMiddleware
                                -> A (ReflectionActivator)
                                  Resolve Request Starting
                                  {
                                    Service: Autofac.Specification.Test.Features.CircularDependency.IC
                                    Component: Autofac.Specification.Test.Features.CircularDependency.BC

                                    Pipeline:
                                    -> CircularDependencyDetectorMiddleware
                                      -> ScopeSelectionMiddleware
                                        -> DecoratorMiddleware
                                          -> SharingMiddleware
                                            -> ActivatorErrorHandlingMiddleware
                                              -> DisposalTrackingMiddleware
                                                -> BC (ReflectionActivator)
                                                  Resolve Request Starting
                                                  {
                                                    Service: Autofac.Specification.Test.Features.CircularDependency.IA
                                                    Component: Autofac.Specification.Test.Features.CircularDependency.A

                                                    Pipeline:
                                                    -> CircularDependencyDetectorMiddleware
                                                    X- CircularDependencyDetectorMiddleware
                                                  }
                                                  Resolve Request FAILED
                                                    Autofac.Core.DependencyResolutionException: Circular component dependency detected: Autofac.Specification.Test.Features.CircularDependency.D -> Autofac.Specification.Test.Features.CircularDependency.A -> Autofac.Specification.Test.Features.CircularDependency.BC -> Autofac.Specification.Test.Features.CircularDependency.A.
                                                       at Autofac.Core.Resolving.Middleware.CircularDependencyDetectorMiddleware.Execute(IResolveRequestContext context, Action`1 next) in C:\Work\GitHub\Autofac\src\Autofac\Core\Resolving\Middleware\CircularDependencyDetectorMiddleware.cs:line 79
                                                       at Autofac.Core.Resolving.Pipeline.ResolvePipelineBuilder.<>c__DisplayClass15_0.<BuildPipeline>b__1(IResolveRequestContext ctxt) in C:\Work\GitHub\Autofac\src\Autofac\Core\Resolving\Pipeline\ResolvePipelineBuilder.cs:line 278
                                                       at Autofac.Core.Pipeline.ResolvePipeline.Invoke(IResolveRequestContext ctxt) in C:\Work\GitHub\Autofac\src\Autofac\Core\Resolving\ResolvePipeline.cs:line 25
                                                       at Autofac.Core.Resolving.ResolveOperation.GetOrCreateInstance(ISharingLifetimeScope currentOperationScope, ResolveRequest request) in C:\Work\GitHub\Autofac\src\Autofac\Core\Resolving\ResolveOperation.cs:line 194
                                                X- BC (ReflectionActivator)
                                              X- DisposalTrackingMiddleware
                                            X- ActivatorErrorHandlingMiddleware
                                          X- SharingMiddleware
                                        X- DecoratorMiddleware
                                      X- ScopeSelectionMiddleware
                                    X- CircularDependencyDetectorMiddleware
                                  }
                                  Resolve Request FAILED: Nested Resolve Failed
                                X- A (ReflectionActivator)
                              X- DisposalTrackingMiddleware
                            X- ActivatorErrorHandlingMiddleware
                          X- SharingMiddleware
                        X- DecoratorMiddleware
                      X- ScopeSelectionMiddleware
                    X- CircularDependencyDetectorMiddleware
                  }
                  Resolve Request FAILED: Nested Resolve Failed
                X- D (ReflectionActivator)
              X- DisposalTrackingMiddleware
            X- ActivatorErrorHandlingMiddleware
          X- SharingMiddleware
        X- DecoratorMiddleware
      X- ScopeSelectionMiddleware
    X- CircularDependencyDetectorMiddleware
  }
  Resolve Request FAILED: Nested Resolve Failed
}
Operation FAILED
  Autofac.Core.DependencyResolutionException: An exception was thrown while activating Autofac.Specification.Test.Features.CircularDependency.D -> Autofac.Specification.Test.Features.CircularDependency.A -> Autofac.Specification.Test.Features.CircularDependency.BC.
   ---> Autofac.Core.DependencyResolutionException: Circular component dependency detected: Autofac.Specification.Test.Features.CircularDependency.D -> Autofac.Specification.Test.Features.CircularDependency.A -> Autofac.Specification.Test.Features.CircularDependency.BC -> Autofac.Specification.Test.Features.CircularDependency.A.
     at Autofac.Core.Resolving.Middleware.CircularDependencyDetectorMiddleware.Execute(IResolveRequestContext context, Action`1 next) in C:\Work\GitHub\Autofac\src\Autofac\Core\Resolving\Middleware\CircularDependencyDetectorMiddleware.cs:line 79
     at Autofac.Core.Resolving.Pipeline.ResolvePipelineBuilder.<>c__DisplayClass15_0.<BuildPipeline>b__1(IResolveRequestContext ctxt) in C:\Work\GitHub\Autofac\src\Autofac\Core\Resolving\Pipeline\ResolvePipelineBuilder.cs:line 278
     at Autofac.Core.Pipeline.ResolvePipeline.Invoke(IResolveRequestContext ctxt) in C:\Work\GitHub\Autofac\src\Autofac\Core\Resolving\ResolvePipeline.cs:line 25
     at Autofac.Core.Resolving.ResolveOperation.GetOrCreateInstance(ISharingLifetimeScope currentOperationScope, ResolveRequest request) in C:\Work\GitHub\Autofac\src\Autofac\Core\Resolving\ResolveOperation.cs:line 194
     at Autofac.Core.Resolving.Pipeline.ResolveRequestContext.ResolveComponent(ResolveRequest request) in C:\Work\GitHub\Autofac\src\Autofac\Core\Resolving\Pipeline\ResolveRequestContext.cs:line 109
     at Autofac.Core.Activators.Reflection.AutowiringParameter.<>c__DisplayClass0_0.<CanSupplyValue>b__0() in C:\Work\GitHub\Autofac\src\Autofac\Core\Activators\Reflection\AutowiringParameter.cs:line 58
     at Autofac.Core.Activators.Reflection.ConstructorParameterBinding.Instantiate() in C:\Work\GitHub\Autofac\src\Autofac\Core\Activators\Reflection\ConstructorParameterBinding.cs:line 117
     at Autofac.Core.Activators.Reflection.ReflectionActivator.ActivateInstance(IComponentContext context, IEnumerable`1 parameters) in C:\Work\GitHub\Autofac\src\Autofac\Core\Activators\Reflection\ReflectionActivator.cs:line 130
     at Autofac.Core.Activators.Reflection.ReflectionActivator.<ConfigurePipeline>b__11_0(IResolveRequestContext ctxt, Action`1 next) in C:\Work\GitHub\Autofac\src\Autofac\Core\Activators\Reflection\ReflectionActivator.cs:line 103
     at Autofac.Core.Resolving.Middleware.DelegateMiddleware.Execute(IResolveRequestContext context, Action`1 next) in C:\Work\GitHub\Autofac\src\Autofac\Core\Resolving\Middleware\DelegateMiddleware.cs:line 58
     at Autofac.Core.Resolving.Pipeline.ResolvePipelineBuilder.<>c__DisplayClass15_0.<BuildPipeline>b__1(IResolveRequestContext ctxt) in C:\Work\GitHub\Autofac\src\Autofac\Core\Resolving\Pipeline\ResolvePipelineBuilder.cs:line 278
     at Autofac.Core.Resolving.Middleware.DisposalTrackingMiddleware.Execute(IResolveRequestContext context, Action`1 next) in C:\Work\GitHub\Autofac\src\Autofac\Core\Resolving\Middleware\DisposalTrackingMiddleware.cs:line 48
     at Autofac.Core.Resolving.Pipeline.ResolvePipelineBuilder.<>c__DisplayClass15_0.<BuildPipeline>b__1(IResolveRequestContext ctxt) in C:\Work\GitHub\Autofac\src\Autofac\Core\Resolving\Pipeline\ResolvePipelineBuilder.cs:line 278
     at Autofac.Core.Resolving.Middleware.ActivatorErrorHandlingMiddleware.Execute(IResolveRequestContext context, Action`1 next) in C:\Work\GitHub\Autofac\src\Autofac\Core\Resolving\Middleware\ActivatorErrorHandlingMiddleware.cs:line 56
     --- End of inner exception stack trace ---
     at Autofac.Core.Resolving.Middleware.ActivatorErrorHandlingMiddleware.Execute(IResolveRequestContext context, Action`1 next) in C:\Work\GitHub\Autofac\src\Autofac\Core\Resolving\Middleware\ActivatorErrorHandlingMiddleware.cs:line 70
     at Autofac.Core.Resolving.Pipeline.ResolvePipelineBuilder.<>c__DisplayClass15_0.<BuildPipeline>b__1(IResolveRequestContext ctxt) in C:\Work\GitHub\Autofac\src\Autofac\Core\Resolving\Pipeline\ResolvePipelineBuilder.cs:line 278
     at Autofac.Core.Resolving.Middleware.SharingMiddleware.Execute(IResolveRequestContext context, Action`1 next) in C:\Work\GitHub\Autofac\src\Autofac\Core\Resolving\Middleware\SharingMiddleware.cs:line 77
     at Autofac.Core.Resolving.Pipeline.ResolvePipelineBuilder.<>c__DisplayClass15_0.<BuildPipeline>b__1(IResolveRequestContext ctxt) in C:\Work\GitHub\Autofac\src\Autofac\Core\Resolving\Pipeline\ResolvePipelineBuilder.cs:line 278
     at Autofac.Core.Resolving.Middleware.DecoratorMiddleware.Execute(IResolveRequestContext context, Action`1 next) in C:\Work\GitHub\Autofac\src\Autofac\Core\Resolving\Middleware\DecoratorMiddleware.cs:line 56
     at Autofac.Core.Resolving.Pipeline.ResolvePipelineBuilder.<>c__DisplayClass15_0.<BuildPipeline>b__1(IResolveRequestContext ctxt) in C:\Work\GitHub\Autofac\src\Autofac\Core\Resolving\Pipeline\ResolvePipelineBuilder.cs:line 278
     at Autofac.Core.Resolving.Middleware.ScopeSelectionMiddleware.Execute(IResolveRequestContext context, Action`1 next) in C:\Work\GitHub\Autofac\src\Autofac\Core\Resolving\Middleware\ScopeSelectionMiddleware.cs:line 66
     at Autofac.Core.Resolving.Pipeline.ResolvePipelineBuilder.<>c__DisplayClass15_0.<BuildPipeline>b__1(IResolveRequestContext ctxt) in C:\Work\GitHub\Autofac\src\Autofac\Core\Resolving\Pipeline\ResolvePipelineBuilder.cs:line 278
     at Autofac.Core.Resolving.Middleware.CircularDependencyDetectorMiddleware.Execute(IResolveRequestContext context, Action`1 next) in C:\Work\GitHub\Autofac\src\Autofac\Core\Resolving\Middleware\CircularDependencyDetectorMiddleware.cs:line 87
     at Autofac.Core.Resolving.Pipeline.ResolvePipelineBuilder.<>c__DisplayClass15_0.<BuildPipeline>b__1(IResolveRequestContext ctxt) in C:\Work\GitHub\Autofac\src\Autofac\Core\Resolving\Pipeline\ResolvePipelineBuilder.cs:line 278
     at Autofac.Core.Pipeline.ResolvePipeline.Invoke(IResolveRequestContext ctxt) in C:\Work\GitHub\Autofac\src\Autofac\Core\Resolving\ResolvePipeline.cs:line 25
     at Autofac.Core.Resolving.ResolveOperation.GetOrCreateInstance(ISharingLifetimeScope currentOperationScope, ResolveRequest request) in C:\Work\GitHub\Autofac\src\Autofac\Core\Resolving\ResolveOperation.cs:line 194
     at Autofac.Core.Resolving.ResolveOperation.Execute(ResolveRequest request) in C:\Work\GitHub\Autofac\src\Autofac\Core\Resolving\ResolveOperation.cs:line 126

Note that it distinguishes the failing middleware stages, indents the exception and all that.

Custom Tracing

Someone can implement their own tracer by implementing IResolvePipelineTracer and calling AttachTrace that takes an instance rather than a callback.

@alsami
Copy link
Member

alsami commented May 21, 2020

Astonishing work! :)

The ASP.NET Core Pipeline works very similar, if I am not mistaking, right?

@alistairjevans
Copy link
Member Author

Thanks! Pretty much, yeah, except the ASP.NET Core pipeline is fully async, whereas this is not.

I picked the 'Use' method name to line up with ASP.NET Core middleware terms.

@tillig
Copy link
Member

tillig commented May 21, 2020

I'll do what I can to check this out and play with it in the next few days. I'm pretty excited about the potential to solve some of the tracing and circular dependency challenges we've seen in the past. #788 could be solved with some of this, for example. Or integration with a profiler UI. Hooking into stuff like DiagnosticSource might also be interesting.

@alistairjevans
Copy link
Member Author

Let me know if you have questions.

There should be lots of tracing options available to us now. I feel like a full-on profiler UI might be above-and-beyond, but a generated report might be handy, that includes analysis results and suggestions? Like the Autofac.Analysis library by @nblumhardt that followed that profiler UI. Not sure how much we want to ship Autofac with, and how much we ship separately.

The DiagnosticSource library is intended for production use, right? I guess if we're not building big trace strings, or not tracing every middleware, that might be fine? Just wary of adding time to the resolve operation.

Copy link
Member

@tillig tillig left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm still working through here but wanted to get some fast feedback out. I've been trying to stick dotTrace and dotMemory on it to see if I can find any obvious things, but the way BenchmarkDotNet spawns off a different process isn't really profiler-friendly. If you have ideas on how to get that to happen without just writing a separate app that mimics the benchmarks in a single-process console app... I'm open.

test/Autofac.Test/Core/Pipeline/PipelineBuilderTests.cs Outdated Show resolved Hide resolved
test/Autofac.Test/Mocks.cs Outdated Show resolved Hide resolved
test/Autofac.Test/Mocks.cs Outdated Show resolved Hide resolved
test/Autofac.Test/Mocks.cs Show resolved Hide resolved
@alistairjevans
Copy link
Member Author

Regarding the benchmarks, you can run BenchmarkDotNet with ETW capture enabled, and it will capture traces during the actual run (pass --profiler ETW to the benchmark CLI to output that). However, the output files can only really be used in PerfView, which works, but is nowhere near as easy to consume as dotTrace.

For my trace attaching I did what you said, added a little console app that ran a few warmup steps, then ran a big loop in a separate thread so I could isolate it in dotTrace. Seemed to work ok for me (though obviously not as scientific as the benchmarks).

    static void Main(string[] args)
    {
      var bench = new ChildScopeResolveBenchmark();

      bench.Resolve();
      bench.Resolve();

      Task.Run(() =>
      {
        for (int idx = 0; idx < 10000; idx++)
        {
          bench.Resolve();
        }
      }).Wait();
    }

@tillig
Copy link
Member

tillig commented May 26, 2020

I'm working on a second pass review, hitting things with a profiler.

I noticed Autofac.Core.Activators.Reflection.ConstructorParameterBinding - this gets created in Autofac.Core.Activators.Reflection.ReflectionActivator.GetConstructorBindings(). In the constructor of ConstructorParameterBinding a lot of work happens that may not need to happen if the binding isn't actually used. I'm curious if we could:

  • Cache the ConstructorParameterBinding objects so we don't have to re-create them for every constructor on an object on every resolution - this could happen during the pipeline build.
  • Switch the stuff that happens in the constructor of ConstructorParameterBinding to happen in method calls. So instead of passing parameters and context as constructor params, you'd pass them to the methods CanInstantiate or whatever.

That might reduce some allocations and speed things up just a little.

Doesn't have to be part of this PR, could be a separate issue for later.

@alistairjevans
Copy link
Member Author

Yes, there's definitely things we can do with the constructor bindings to optimise them.

An additional optimisation is that if you have multiple constructors, that have overlapping service types, currently each of those constructors will go and check for a registration for each constructor/type combo.

It may be more efficient to determine a set of unique type dependencies at pipeline build time, determine which of the type dependencies we can satisfy at resolve time, and pick the constructor that satisfies the most dependencies.

As you say, we can do this in a subsequent PR.

Copy link
Member

@tillig tillig left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, I think what I'm starting to find are little microperf things, where we could improve tiny places and possibly get an overall improvement, but none of that is enough to stop the PR from going through. I'm not finding anything super obvious to change or improve - this is really good stuff. Let me know what you think about the remaining comments and I think we can call it good.

src/Autofac/Autofac.csproj Outdated Show resolved Hide resolved
src/Autofac/Core/Container.cs Show resolved Hide resolved
src/Autofac/Core/IComponentRegistration.cs Show resolved Hide resolved
src/Autofac/Core/IInstanceActivator.cs Outdated Show resolved Hide resolved
src/Autofac/Core/Lifetime/LifetimeScope.cs Show resolved Hide resolved
@alistairjevans
Copy link
Member Author

A note on branches; I've currently got this targeting the v6 branch.

@tillig, if you think our next release will be 6.0 I can retarget to develop, or we can leave it in v6 for now.

I've got changes to how decorators work in the pipelines world still planned, plus it would be good to get composite support (#970) in and resolve some other long-standing issues we can resolve now, so the 6.0 date might be a way off.

@tillig
Copy link
Member

tillig commented May 27, 2020

I'm thinking we should keep the v6 branch a little longer to leave room for interim bug fixes that may need to happen as we get 6.0 ready to go. I am pretty wary of long-lived branches, though, since it's so hard to keep them up to date.

I noted down four things that I think we need to dive a little deeper on - let me know if these sound right:

  • Turn on CS1591 (ensure everything is documented)
  • Update .editorconfig and Roslyn analyzers to require open/close braces
  • Investigate DiagnosticSource as an alternative trace mechanism
  • Profile for perf optimizations
    • Run profiler on all the various benchmark cases to see if there are additional optimizations
    • CircularDependencyMiddleware.Execute stack sees PushWithResize a bit, might be worth allocating some memory up front
    • ResolveOperationBase.ResetSuccessfulRequests resets the list with new, but would it be better to Clear() or something else?

Those can all be separate issues that happen after this PR. (Or one "checklist" sort of issue for pre-v6 release.)

This is pretty sweet. I'm stoked to see some pretty big overhauls to the Autofac internals that will help us maintain it and take it forward. Great work! 🏆

If you're happy with it, let's merge this bad boy into the v6 branch. We can probably set up a milestone for v6 to tag issues specific to that branch. Might be good to have some sort of master tracking issue to count down to v6 and determine when we think it's ready.

@alistairjevans alistairjevans added this to the v6.0 milestone May 27, 2020
@alistairjevans
Copy link
Member Author

Yep, good to go, I'll press the big button. 😊

The items you mentioned look good, but I'll add:

  • Service-specific pipelines, to provide better and faster decorator support, plus composite support.
  • Changing how external registrations are built to reduce the re-creation of pipelines in nested scopes.

I can raise issues for each of those to track them.

There's already a v6 milestone, I'll tag this PR, and we can associate anything else of relevance to this.

@alistairjevans alistairjevans merged commit ff22873 into autofac:v6 May 27, 2020
@alistairjevans alistairjevans linked an issue May 27, 2020 that may be closed by this pull request
@alistairjevans alistairjevans deleted the pipelines branch May 28, 2020 06:31
@alexmg
Copy link
Member

alexmg commented Jun 1, 2020

Just catching up on the activity after emerging from months of quarantine and home schooling.

I've just read through the thread and it's really exciting to see this come together. Amazing work @alistairjevans.

I will pull the branch down as soon as possible and take a closer look.

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

Successfully merging this pull request may close these issues.

Pipeline discussion
4 participants