Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit a1982e2

Browse files
authored
Reduce size of async state machine by a reference field (#83696)
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.
1 parent e50b41e commit a1982e2

File tree

3 files changed

+26
-18
lines changed

3 files changed

+26
-18
lines changed

src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncTaskMethodBuilderT.cs

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,7 @@ private sealed class DebugFinalizableAsyncStateMachineBox<TStateMachine> : // SO
269269

270270
/// <summary>A strongly-typed box for Task-based async state machines.</summary>
271271
/// <typeparam name="TStateMachine">Specifies the type of the state machine.</typeparam>
272+
[DebuggerDisplay("{DebuggerDisplay,nq}")]
272273
private class AsyncStateMachineBox<TStateMachine> : // SOS DumpAsync command depends on this name
273274
Task<TResult>, IAsyncStateMachineBox
274275
where TStateMachine : IAsyncStateMachine
@@ -285,8 +286,6 @@ private static void ExecutionContextCallback(object? s)
285286
Unsafe.As<AsyncStateMachineBox<TStateMachine>>(s).StateMachine!.MoveNext();
286287
}
287288

288-
/// <summary>A delegate to the <see cref="MoveNext()"/> method.</summary>
289-
private Action? _moveNextAction;
290289
/// <summary>The state machine itself.</summary>
291290
public TStateMachine? StateMachine; // mutable struct; do not make this readonly. SOS DumpAsync command depends on this name.
292291

@@ -299,8 +298,29 @@ public AsyncStateMachineBox()
299298
m_stateFlags |= (int)InternalTaskOptions.HiddenState;
300299
}
301300

301+
/// <summary>Debugger-only display string for the async state machine.</summary>
302+
private string DebuggerDisplay
303+
{
304+
get
305+
{
306+
// Ideally we just use the type of the TStateMachine as the "method" name. However, in certain use in the
307+
// debugger, TStateMachine might actually be a weakly-typed IAsyncStateMachine, in which case we can ToString
308+
// the state machine instance. But in debug builds the state machine type could also be a class, in which case
309+
// the field could be null, so worst case we just fall back to using "IAsyncStateMachine".
310+
string stateMachineName = typeof(TStateMachine) != typeof(IAsyncStateMachine) ?
311+
typeof(TStateMachine).Name :
312+
StateMachine?.ToString() ??
313+
nameof(IAsyncStateMachine);
314+
315+
// Keep the shape of this message in sync with that of the base Task<TResult>.
316+
return IsCompletedSuccessfully && typeof(TResult) != typeof(VoidTaskResult) ?
317+
$"Id = {Id}, Status = {Status}, Method = {stateMachineName}, Result = {m_result}" :
318+
$"Id = {Id}, Status = {Status}, Method = {stateMachineName}";
319+
}
320+
}
321+
302322
/// <summary>A delegate to the <see cref="MoveNext()"/> method.</summary>
303-
public Action MoveNextAction => _moveNextAction ??= new Action(MoveNext);
323+
public Action MoveNextAction => (Action)(m_action ??= new Action(MoveNext));
304324

305325
/// <summary>Captured ExecutionContext with which to invoke <see cref="MoveNextAction"/>; may be null.</summary>
306326
/// <remarks>

src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Future.cs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -369,8 +369,6 @@ internal static Task<TResult> StartNew(Task? parent, Func<object?, TResult> func
369369
// internal helper function breaks out logic used by TaskCompletionSource
370370
internal bool TrySetResult(TResult? result)
371371
{
372-
Debug.Assert(m_action == null, "Task<T>.TrySetResult(): non-null m_action");
373-
374372
bool returnValue = false;
375373

376374
// "Reserve" the completion for this task, while making sure that: (1) No prior reservation

src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Task.cs

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -123,13 +123,9 @@ public class Task : IAsyncResult, IDisposable
123123

124124
private int m_taskId; // this task's unique ID. initialized only if it is ever requested
125125

126-
internal Delegate? m_action; // The body of the task. Might be Action<object>, Action<TState> or Action. Or possibly a Func.
127-
// If m_action is set to null it will indicate that we operate in the
128-
// "externally triggered completion" mode, which is exclusively meant
129-
// for the signalling Task<TResult> (aka. promise). In this mode,
130-
// we don't call InnerInvoke() in response to a Wait(), but simply wait on
131-
// the completion event which will be set when the Future class calls Finish().
132-
// But the event would now be signalled if Cancel() is called
126+
// The delegate to invoke for a delegate-backed Task.
127+
// This field also may be used by async state machines to cache an Action.
128+
internal Delegate? m_action;
133129

134130
private protected object? m_stateObject; // A state object that can be optionally supplied, passed to action.
135131
internal TaskScheduler? m_taskScheduler; // The task scheduler this task runs under.
@@ -3270,8 +3266,6 @@ private void SetCancellationAcknowledged()
32703266
/// <returns>true if the task was transitioned to ran to completion; false if it was already completed.</returns>
32713267
internal bool TrySetResult()
32723268
{
3273-
Debug.Assert(m_action == null, "Task<T>.TrySetResult(): non-null m_action");
3274-
32753269
if (AtomicStateUpdate(
32763270
(int)TaskStateFlags.CompletionReserved | (int)TaskStateFlags.RanToCompletion,
32773271
(int)TaskStateFlags.CompletionReserved | (int)TaskStateFlags.RanToCompletion | (int)TaskStateFlags.Faulted | (int)TaskStateFlags.Canceled))
@@ -3298,8 +3292,6 @@ internal bool TrySetResult()
32983292
// Called from TaskCompletionSource<T>.SetException(IEnumerable<Exception>).
32993293
internal bool TrySetException(object exceptionObject)
33003294
{
3301-
Debug.Assert(m_action == null, "Task<T>.TrySetException(): non-null m_action");
3302-
33033295
// TCS.{Try}SetException() should have checked for this
33043296
Debug.Assert(exceptionObject != null, "Expected non-null exceptionObject argument");
33053297

@@ -3346,7 +3338,6 @@ internal bool TrySetCanceled(CancellationToken tokenToRecord)
33463338
// This method is only valid for promise tasks.
33473339
internal bool TrySetCanceled(CancellationToken tokenToRecord, object? cancellationException)
33483340
{
3349-
Debug.Assert(m_action == null, "Task<T>.TrySetCanceled(): non-null m_action");
33503341
Debug.Assert(
33513342
cancellationException == null ||
33523343
cancellationException is OperationCanceledException ||
@@ -6570,7 +6561,6 @@ internal static Task<TResult> CreateUnwrapPromise<TResult>(Task outerTask, bool
65706561

65716562
if (continuationObject is Task continuationTask)
65726563
{
6573-
Debug.Assert(continuationTask.m_action == null);
65746564
Delegate[]? delegates = continuationTask.GetDelegateContinuationsForDebugger();
65756565
if (delegates != null)
65766566
return delegates;

0 commit comments

Comments
 (0)