-
Notifications
You must be signed in to change notification settings - Fork 2.6k
Clear unnecessary state from completed Tasks #20294
Conversation
When Tasks backed by delegates are created, an ExecutionContext is captured. When the task completes, its delegate is being cleared, but its ExecutionContext is not, which means if the Task is subsequently kept alive (e.g. stored in a cache), so too is its ExecutionContext, which can capture an arbitrary amount of ambient state via async locals. This commit augments the clearing of the delegate to similarly clear the ExecutionContext. Related, async methods can also capture ExecutionContext when awaits yield, so this clears out that context as well. And as long as we're doing that, we may as well also clear the state machine state, so that any hoisted locals in the state machine aren't kept alive if the resulting task is kept alive. Not doing so previously was a conscious choice, in order to aid in debugging, but as we've heard of at least a couple of cases where it unexpectedly caused a leak, I'm going ahead and changing it.
|
I wonder if we should backport this to 2.1? |
|
Add unit tests? |
In corefx, not in coreclr. |
For Task? I don't think so. This behavior has been here for close to a decade. For async methods, I've only heard two comments about it, and it wasn't a showstopper for either. I don't think that meets the bar. |
|
@dotnet-bot test Ubuntu arm Cross Checked no_tiered_compilation_innerloop Build and Test please (https://github.com/dotnet/coreclr/issues/20296) |
|
Unrelated build failures: and |
|
@dotnet-bot test this please |
Yea, but async locals didn't exist back then. I agree that we should wait for complaints though. |
They sort of did, they were just called |
Yea, I wonder if they stored complex objects in there... The HttpContext was stored in the IllogicalCallContext 😄 |
| // In case this is a state machine box with a finalizer, suppress its finalization | ||
| // as it's now complete. We only need the finalizer to run if the box is collected | ||
| // without having been completed. | ||
| if (AsyncMethodBuilderCore.TrackAsyncMethodCompletion) |
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.
(Unrelated to this change) Would it make sense to move this into DebugFinalizableAsyncStateMachineBox so that this is not on the golden path? And also move new DebugFinalizableAsyncStateMachineBox into its own non-inlineable method so that the DebugFinalizableAsyncStateMachineBox type is built only if you have the tracing on.
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 move this into DebugFinalizableAsyncStateMachineBox so that this is not on the golden path?
Ideally, yes. I'd looked at that previously and didn't find a great way to do it without introducing another virtual onto the golden path (and I preferred the extra branch to the virtual invocation), but several things have changed since then, so there might be a better way now. I can look again.
And also move creation of new DebugFinalizableAsyncStateMachineBox into its own non-inlineable method so that the DebugFinalizableAsyncStateMachineBox type is built only if you have the tracing on.
You mean at https://github.com/dotnet/coreclr/pull/20294/files/08e4e4f6f222919ed6d0f0430fb6922dc9973508#diff-68dc11e2c63ba209e0f903d3824d8464R488? By "built", you're referring to JIT work? Sure, I can move that. I'll do it separately.
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, this one. The JIT triggers type building for all types directly referenced by the method even if they are on the cold path. It takes both time and memory - each DebugFinalizableAsyncStateMachineBox type costs several hundred bytes.
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.
Makes sense. I'll move it out separately. Thanks.
|
@dotnet-bot test OSX10.12 x64 Checked Innerloop Build and Test please |
|
@dotnet-bot test CentOS7.1 x64 Checked Innerloop Build and Test please (build never started) |
When Tasks backed by delegates are created, an ExecutionContext is captured. When the task completes, its delegate is being cleared, but its ExecutionContext is not, which means if the Task is subsequently kept alive (e.g. stored in a cache), so too is its ExecutionContext, which can capture an arbitrary amount of ambient state via async locals. This commit augments the clearing of the delegate to similarly clear the ExecutionContext. Related, async methods can also capture ExecutionContext when awaits yield, so this clears out that context as well. And as long as we're doing that, we may as well also clear the state machine state, so that any hoisted locals in the state machine aren't kept alive if the resulting task is kept alive. Not doing so previously was a conscious choice, in order to aid in debugging, but as we've heard of at least a couple of cases where it unexpectedly caused a leak, I'm going ahead and changing it.
When Tasks backed by delegates are created, an ExecutionContext is captured. When the task completes, its delegate is being cleared, but its ExecutionContext is not, which means if the Task is subsequently kept alive (e.g. stored in a cache), so too is its ExecutionContext, which can capture an arbitrary amount of ambient state via async locals. This commit augments the clearing of the delegate to similarly clear the ExecutionContext.
Related, async methods can also capture ExecutionContext when awaits yield, so this clears out that context as well. And as long as we're doing that, we may as well also clear the state machine state, so that any hoisted locals in the state machine aren't kept alive if the resulting task is kept alive. Not doing so previously was a conscious choice, in order to aid in debugging, but as we've heard of at least a couple of cases where it unexpectedly caused a leak, I'm going ahead and changing it.
Fixes https://github.com/dotnet/coreclr/issues/20273
Fixes https://github.com/dotnet/coreclr/issues/20244
cc: @kouvel, @tarekgh, @davidfowl, @benaadams