-
Notifications
You must be signed in to change notification settings - Fork 4.7k
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
Conversation
- 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.
Tagging subscribers to this area: @eerhardt, @maryamariyan Issue Details
PS: Needs testing, getting early feedback.
|
...ies/Microsoft.Extensions.DependencyInjection/src/ServiceLookup/ServiceProviderEngineScope.cs
Outdated
Show resolved
Hide resolved
- 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
...s/Microsoft.Extensions.DependencyInjection/tests/DI.Tests/ServiceProviderEngineScopeTests.cs
Show resolved
Hide resolved
...ies/Microsoft.Extensions.DependencyInjection/src/ServiceLookup/ServiceProviderEngineScope.cs
Show resolved
Hide resolved
|
||
namespace Microsoft.Extensions.DependencyInjection | ||
{ | ||
internal class ScopePool |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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)
...ies/Microsoft.Extensions.DependencyInjection/src/ServiceLookup/ServiceProviderEngineScope.cs
Show resolved
Hide resolved
...s/Microsoft.Extensions.DependencyInjection/src/ServiceLookup/ILEmit/ILEmitResolverBuilder.cs
Show resolved
Hide resolved
...s/Microsoft.Extensions.DependencyInjection/src/ServiceLookup/ILEmit/ILEmitResolverBuilder.cs
Outdated
Show resolved
Hide resolved
Did you consider pooling entire ServiceProviderEngineScope objects? Or is that too scary? |
LGTM, nice and clean. |
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. |
...ies/Microsoft.Extensions.DependencyInjection/src/ServiceLookup/ServiceProviderEngineScope.cs
Outdated
Show resolved
Hide resolved
src/libraries/Microsoft.Extensions.DependencyInjection/src/ScopePool.cs
Outdated
Show resolved
Hide resolved
...ies/Microsoft.Extensions.DependencyInjection/src/ServiceLookup/ServiceProviderEngineScope.cs
Outdated
Show resolved
Hide resolved
...ies/Microsoft.Extensions.DependencyInjection/src/ServiceLookup/ServiceProviderEngineScope.cs
Outdated
Show resolved
Hide resolved
...s/Microsoft.Extensions.DependencyInjection/tests/DI.Tests/ServiceProviderEngineScopeTests.cs
Show resolved
Hide resolved
{ | ||
// 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) |
There was a problem hiding this comment.
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?
...ies/Microsoft.Extensions.DependencyInjection/src/ServiceLookup/ServiceProviderEngineScope.cs
Show resolved
Hide resolved
@@ -134,9 +138,11 @@ public ValueTask DisposeAsync() | |||
} | |||
} | |||
|
|||
ClearState(); |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes it would.
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this 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!
Any performance numbers? |
@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. |
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
After
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). |
Thanks.
This is fairly common for pools like this - they replace GC allocations with cache trashing. |
@jkotas do we have an easy way to measure that? Or do we need a tool like vtune? |
Yep, vtune would be a tool to investigate this. |
PS: Needs testing, getting early feedback.TODO: Performance testscc @sebastienros
Fixes #47607
Update: @sebastienros confirmed it fixes the issue.