-
Notifications
You must be signed in to change notification settings - Fork 5.2k
Reduce size of async state machine by a reference field #83696
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
Conversation
Every async state machine today has a field for an Action. That field is used to cache an Action that's lazily created to point to its MoveNext method. It's only needed, however, if the state machine awaits something that's not a known awaiter. Interestingly, Task itself has a field for storing a delegate, which is only used today when the Task is created to invoke a delegate (e.g. Task.Run, ContinueWith, etc.). I've considered that a liability, but I just realized we can use that same field for this async method cached Action as well, making it relevant to almost all tasks, and avoiding the need to have an extra field on the state machine box. As the m_action on a task impacts the DebuggerDisplay rendering, I've also added a DebuggerDisplay to the state machine box type. We can improve this further in the future, and also add a DebuggerTypeProxy later if desired.
Tagging subscribers to this area: @dotnet/area-system-threading-tasks Issue DetailsEvery async state machine today has a field for an Action. That field is used to cache an Action that's lazily created to point to its MoveNext method. It's only needed, however, if the state machine awaits something that's not a known awaiter. Interestingly, Task itself has a field for storing a delegate, which is only used today when the Task is created to invoke a delegate (e.g. Task.Run, ContinueWith, etc.). I've considered that a liability, but I just realized we can use that same field for this async method cached Action as well, making it relevant to almost all tasks, and avoiding the need to have an extra field on the state machine box. As the m_action on a task impacts the DebuggerDisplay rendering, I've also added a DebuggerDisplay to the state machine box type. We can improve this further in the future, and also add a DebuggerTypeProxy later if desired.
|
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.
Wild 💜
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 is awesome!
Can you show a before and after of the debugger display after this change? We might also need to tweak any tools that we’re making assumptions about m_action being set in certain situations (parallel tasks tooling comes to mind) |
using System.Diagnostics;
Task t = SomethingAsync();
Debugger.Break();
static async Task SomethingAsync()
{
await Task.Delay(1_000_000);
}
We might, though the delegate does accurately reflect what will be invoked. @gregg-miskelly, do you expect this will have any debugger impact? |
Do you mean the DebuggerDisplay change? Or just this PR as a whole? Aside from tests, I can't imagine that changing the DebuggerDisplay will break anything. The debugger does sometimes inspect the result of The debugger also will inspect Task.m_action in various places. It isn't super obvious to me if this will be a problem, but I am assuming even if it is, we could handle this by checking if the task is a What scenarios are we awaiting something that isn't a known awaiter so we can make sure we test this? |
Thanks, Gregg.
The part where we store something into m_action.
Inspecting for display is fine; the delegate is meaningful in that it does in fact point to what will next be executed. What might be a problem, for example, is if the debugger makes a decision based on this having been null, since it won't be in some cases.
For example: using System.Diagnostics;
using System.Runtime.CompilerServices;
var mres = new ManualResetEventSlim();
Task t = ExampleAsync(mres);
mres.Wait();
Debugger.Break();
GC.KeepAlive(t);
async Task ExampleAsync(ManualResetEventSlim mres)
{
await default(SwitchToThreadPool);
mres.Set();
Thread.Sleep(1_000_000);
}
struct SwitchToThreadPool : ICriticalNotifyCompletion
{
public SwitchToThreadPool GetAwaiter() => default;
public bool IsCompleted => false;
public void GetResult() { }
public void OnCompleted(Action continuation) => ThreadPool.QueueUserWorkItem(s => ((Action)s!)(), continuation);
public void UnsafeOnCompleted(Action continuation) => ThreadPool.UnsafeQueueUserWorkItem(s => ((Action)s!)(), continuation);
} At that break, the Task |
Every async state machine today has a field for an Action. That field is used to cache an Action that's lazily created to point to its MoveNext method. It's only needed, however, if the state machine awaits something that's not a known awaiter.
Interestingly, Task itself has a field for storing a delegate, which is only used today when the Task is created to invoke a delegate (e.g. Task.Run, ContinueWith, etc.). I've considered that a liability, but I just realized we can use that same field for this async method cached Action as well, making it relevant to almost all tasks, and avoiding the need to have an extra field on the state machine box.
As the m_action on a task impacts the DebuggerDisplay rendering, I've also added a DebuggerDisplay to the state machine box type. We can improve this further in the future, and also add a DebuggerTypeProxy later if desired.