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.
How many similar pool implementations do we have across the codebase? Worth having something reusable?
There was a problem hiding this comment.
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.
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
| } | ||
| } | ||
|
|
||
| ClearState(); |
There was a problem hiding this comment.
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.
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.
Should we always call Clear?
| // 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.
What's the reason for not setting this to null in all cases?
There was a problem hiding this comment.
Singletons, and the background compilation thread that uses them. Also it isn't pooled.
eerhardt
left a comment
There was a problem hiding this comment.
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: BeforeAfterThis 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.