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

Add API to analyse object composition time #722

Open
dotnetjunkie opened this issue Jun 4, 2019 · 4 comments
Open

Add API to analyse object composition time #722

dotnetjunkie opened this issue Jun 4, 2019 · 4 comments

Comments

@dotnetjunkie
Copy link
Collaborator

@dotnetjunkie dotnetjunkie commented Jun 4, 2019

When it comes to constructing object graphs of large applications, it can get hard to keep track of what developers check in and spot when they accidentally create constructors that do too much.

Simple Injector could help in this regard by doing an optional scan of the complete object graph to measure the time it takes construct graphs. This analysis can be reported back to the user in some form. This is especially useful in integration testing or exploratory testing scenarios, where a developer likes to detect or prevent performance problems, caused by constructors that do I/O or other heavy lifting.

I'm still unsure what the correct API for this would be and that the APIs entry point should be.

Simple Injector currently contains one interception point that allows you to measure performance, which is Container.Options.RegisterResolveInterceptor, but this interception point allows only root objects to be measured, for instance:

var resolves = new List<(InitializationContext context, TimeSpan elapsed)>();

container.Options.RegisterResolveInterceptor((context, producer) =>
    {
        var watch = Stopwatch.StartNew();
        try
        {
            return producer();
        }
        finally
        {
            resolves.Add((context, watch.Elapsed));
        }
    },
    c => true);

container.Verify();

var slowestResolves =
    from resolve in resolves
    where resolve.elapsed > TimeSpan.FromMilliseconds(10)
    orderby resolve.elapsed descending
    select resolve;

if (slowestResolves.Any())
{
    Assert.Fail("The following components where slow in their creation:\n * " +
        string.Join(Environment.NewLine + " * ",
            from resolve in slowestResolves
            select resolve.context.Registration.ImplementationType.ToFriendlyName() +
                $" ({resolve.elapsed.TotalMilliseconds} ms.)"));
}

This code, however, only plugs into the root objects, while the real culprit might lie in an object very deep in the object graph. The real cause will be hard to track.

There is a Container.RegisterInitializer overload that allows to hook into individual registrations, but this method, however, only gets triggered after the object is created. This, therefore, isn't helpful.

The only feasible way to plug in, is to hook onto the ExpressionBuilding event, and replace the built Expression with one that adds measurement. This, however, is cumbersome. It would be much nicer if such feature is supported out-of-the-box.

@dotnetjunkie
Copy link
Collaborator Author

@dotnetjunkie dotnetjunkie commented Jul 29, 2019

feature-722 branch created.

@dotnetjunkie dotnetjunkie removed this from the v4.7 milestone Aug 29, 2019
@dotnetjunkie dotnetjunkie added this to the v4.8 milestone Aug 29, 2019
@dotnetjunkie
Copy link
Collaborator Author

@dotnetjunkie dotnetjunkie commented Sep 4, 2019

Open questions:

  • Should measurement include initial construction of expression trees? This is a one-time cost.
  • Should measurement include the time it takes to create a singleton? This is a one-time cost.
  • Should measurement be part of the Verify() process, or a completely separate operation?
  • Should measurement take possible garbage collects into consideration, as they might influence the time it takes to construct a single component.
  • How should the results be presented? Should it return a list of "slow" components, ordered by their slowness? Should this include total creation cost and cost without dependencies? Should the result include the number of instances created (the total creation count)? What else to include?
  • Should the results be integrated into string representation of graphs as returned from InstanceProducer.VisualizeObjectGraph()? For instance:
    UserListController( // 0.1-10.4 ms
        SqlRepository<User>( // 10.3-10.3 ms
            SqlConnectionFactory()), // 0.0-0.0 ms
        FileLogger()) // 0.0-0.0 ms
    

Things to consider:

  • Construction of expression trees is a one-time operation, in which the user has little influence. The creation of an expression tree, however, might trigger the creation of singletons.
  • Although creation of a singleton is a one-time cost, it can still cause delays, when that singleton is slow to create (for instance when it establishing a connection of some sort)
  • Integration with Verify can be difficult, as a slow-created object graph is not an error that Verify should throw on. Separating the API, however, might make it less discoverable.
  • A complication is the fact that creation of non-root types are hard to measure, because their expression trees are shared by possibly many consumers. So only GetInstance is called on root types, but not on dependencies, making it hard to measure anything else but the root types.
  • If object composition time measurement takes place as part of the call to Verify(), information could be severely skewed when time measurement happens within a single scope (the Verify() process currently runs in its own (single) "verification scope"), because a possibly slow scoped component would only be resolved once for the complete configuration, while in reality it would likely give a performance hit per root-type resolve (assuming that in a single scope only one root type is resolved, of course).

dotnetjunkie added a commit that referenced this issue Sep 18, 2019
…HelperActivator from the IServiceProvider instead of creating the DefaultTagHelperActivor manually; that breaks in ASP.NET Core 3.0 as both DefaultTagHelperActivator and ITypeActivatorCache are now internal types. Fixes #722.
dotnetjunkie added a commit that referenced this issue Nov 17, 2019
…ult ITagHelperActivator from the IServiceProvider instead of creating the DefaultTagHelperActivor manually; that breaks in ASP.NET Core 3.0 as both DefaultTagHelperActivator and ITypeActivatorCache are now internal types. Fixes #722."

This reverts commit 49145df.
@dotnetjunkie dotnetjunkie removed this from the v4.8 milestone Nov 17, 2019
@dotnetjunkie dotnetjunkie added this to the v4.9 milestone Nov 17, 2019
@dotnetjunkie dotnetjunkie removed the task label Dec 5, 2019
@dotnetjunkie dotnetjunkie removed this from the v4.9 milestone Jan 5, 2020
@dotnetjunkie dotnetjunkie added this to the v4.10 milestone Jan 5, 2020
@dotnetjunkie dotnetjunkie removed this from the v4.10 milestone Apr 24, 2020
@dotnetjunkie dotnetjunkie added this to the Backlog milestone Apr 24, 2020
@dotnetjunkie
Copy link
Collaborator Author

@dotnetjunkie dotnetjunkie commented Apr 24, 2020

Moved this back to the backlog. There are too many open questions and caveats at this point.

@dotnetjunkie dotnetjunkie removed this from the Backlog milestone Dec 22, 2020
@dotnetjunkie dotnetjunkie added this to the Simple Injector v6.0 milestone Dec 22, 2020
@onyxmaster
Copy link

@onyxmaster onyxmaster commented Mar 12, 2021

I was going to create a similar issue but luckily found this one. I would very much be interested in the ability to intercept all object creation invocations for a similar purpose: profiling startup performance of a large project to aid in finding the "only assignments in constructor" rule violations.

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

Successfully merging a pull request may close this issue.

None yet
2 participants