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

Pool the underlying list and dictionary in scopes. #50463

Merged
merged 5 commits into from
Apr 2, 2021

Conversation

davidfowl
Copy link
Member

@davidfowl davidfowl commented Mar 31, 2021

  • Today DI scopes allocate a dictionary and an optional list of disposables per request. That dictionary starts off with the default capacity and resizes as scoped services are resolved. Depending on how many scoped services are resolved, this can end up with many resizes per request which result in new array allocations. This change pools those 2 lists assuming that short lived scopes look mostly the same (same number of services resolved and disposed per scope instance).
  • This change pools a set of scopes assuming they are short lived.
  • One breaking change is that after disposal, pooled scopes will throw if services are accessed afterwards on the scope.
  • We could make this an opt in flag but, I'm not sure if it it makes sense. Accessing a scope after it's disposed is broken anyways.

PS: Needs testing, getting early feedback.
TODO: Performance tests

cc @sebastienros

Fixes #47607

Update: @sebastienros confirmed it fixes the issue.

- This change pools a set of scopes assuming they are short lived.
- One breaking change is that after disposal, pooled scopes will throw if services are accessed afterwards on the scope.
@ghost
Copy link

ghost commented Mar 31, 2021

Tagging subscribers to this area: @eerhardt, @maryamariyan
See info in area-owners.md if you want to be subscribed.

Issue Details
  • This change pools a set of scopes assuming they are short lived.
  • One breaking change is that after disposal, pooled scopes will throw if services are accessed afterwards on the scope.

PS: Needs testing, getting early feedback.

Author: davidfowl
Assignees: -
Labels:

area-Extensions-DependencyInjection

Milestone: -

- Allocate the lock. Scoped services can be resolved without a scope when the scope validation flag is off.
- Assert the correct lock
- Modified test to throw after dispose

namespace Microsoft.Extensions.DependencyInjection
{
internal class ScopePool
Copy link
Contributor

Choose a reason for hiding this comment

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

How many similar pool implementations do we have across the codebase? Worth having something reusable?

Copy link
Member

@halter73 halter73 Apr 1, 2021

Choose a reason for hiding this comment

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

Getting something that we can actually share internally is probably a good first test for any proposed ObjectPool<T> API. (#49680)

@pakrym
Copy link
Contributor

pakrym commented Mar 31, 2021

Did you consider pooling entire ServiceProviderEngineScope objects? Or is that too scary?

@pakrym
Copy link
Contributor

pakrym commented Mar 31, 2021

LGTM, nice and clean.

@davidfowl
Copy link
Member Author

Did you consider pooling entire ServiceProviderEngineScope objects? Or is that too scary?

I started that way, but it did seem a little scary and might need to be opt-in if we went that far. I'd like to reduce the object size event more (if possible) so scopes are basically a super lightweight wrapper around this possibly reusable state.

@davidfowl davidfowl marked this pull request as ready for review March 31, 2021 23:33
{
// We lock here since ResolvedServices is always accessed in the scope lock, this means we'll never
// try to return to the pool while somebody is trying to access ResolvedServices.
lock (_scopeLock)
Copy link
Member

Choose a reason for hiding this comment

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

this method seems to be a noop for root scope, would it make sense to skip for it before taking the lock?

@@ -134,9 +138,11 @@ public ValueTask DisposeAsync()
}
}

ClearState();
Copy link
Member

@maryamariyan maryamariyan Apr 1, 2021

Choose a reason for hiding this comment

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

Would it make sense to add this ClearState() call on a finally block to the try-catch block above it or it doesnt make a difference?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes it would.

Copy link
Member Author

Choose a reason for hiding this comment

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

Actually, it's probably better not to try and return this scope to the pool if dispose throws.

return false;
}

state.Clear();
Copy link
Member

Choose a reason for hiding this comment

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

Should we always call Clear?

Copy link
Member Author

Choose a reason for hiding this comment

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

Why?

// This will return false if the pool is full or if this state object is the root scope
if (_state.Return())
{
_state = null;
Copy link
Member

Choose a reason for hiding this comment

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

What's the reason for not setting this to null in all cases?

Copy link
Member Author

Choose a reason for hiding this comment

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

Singletons, and the background compilation thread that uses them. Also it isn't pooled.

Copy link
Member

@eerhardt eerhardt left a comment

Choose a reason for hiding this comment

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

I think this looks good. Nice work here!

@davidfowl davidfowl merged commit c24bf80 into main Apr 2, 2021
@davidfowl davidfowl added this to the 6.0.0 milestone Apr 2, 2021
@jkotas
Copy link
Member

jkotas commented Apr 2, 2021

TODO: Performance tests

Any performance numbers?

@sebastienros
Copy link
Member

@jkotas on the same application as in the issue I filed, the allocations disappeared from the list. They represented 13% of the total allocations of the app (150MB /sec), and with this branch there were less than 1% AFAIR.

@davidfowl
Copy link
Member Author

davidfowl commented Apr 2, 2021

We (@sebastienros and I) ran a couple of the TE benchmarks to make sure nothing regressed. We saw a very minor RPS improvement and a reduction in allocation rate. In orchard, the allocations basically disappeared from the trace from being the top one but RPS didn't improve much.

Here's a microbenchmark with 10 dependent scoped services:

Before

|         Method |     Mean |   Error |  StdDev |  Gen 0 | Gen 1 | Gen 2 | Allocated |
|--------------- |---------:|--------:|--------:|-------:|------:|------:|----------:|
| ScopedServices | 667.8 ns | 9.00 ns | 8.42 ns | 0.3662 |     - |     - |    1.5 KB |

After

|         Method |     Mean |    Error |   StdDev |  Gen 0 | Gen 1 | Gen 2 | Allocated |
|--------------- |---------:|---------:|---------:|-------:|------:|------:|----------:|
| ScopedServices | 599.1 ns | 10.60 ns | 12.20 ns | 0.0763 |     - |     - |     320 B |

This benchmark needs more concurrency to represent what ends up happening per request in ASP.NET Core though (where we've mostly removed scoped services).

@jkotas
Copy link
Member

jkotas commented Apr 2, 2021

Thanks.

one but RPS didn't improve much.

This is fairly common for pools like this - they replace GC allocations with cache trashing.

@jkotas jkotas deleted the davidfowl/pool-scoped-dict branch April 2, 2021 01:45
@davidfowl
Copy link
Member Author

@jkotas do we have an easy way to measure that? Or do we need a tool like vtune?

@jkotas
Copy link
Member

jkotas commented Apr 2, 2021

Yep, vtune would be a tool to investigate this.

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

Successfully merging this pull request may close these issues.

DI allocations improvements
8 participants