Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
using System.Runtime.Versioning;
using System.Threading;
using System.Threading.Tasks;
using System.Collections.Generic;
using System.Threading.Tasks.Sources;

#if NATIVEAOT
Expand Down Expand Up @@ -146,6 +147,24 @@ public ref byte GetResultStorageOrNull()
}
}

// Equality comparer for Continuations. Needed because virtual methods do not work on Continuations
// So to use them as keys in dictionaries we need a comparer instead of using their GetHashCode/Equals.
internal class ContinuationEqualityComparer : IEqualityComparer<Continuation>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What difference does it make specifying this comparer? Is this a workaround for missing functionality in the Continuation class where most objects have this default behavior but Continuations are special?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, the virtual functions do not work on Continuations.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It can use a comment

{
internal static readonly ContinuationEqualityComparer Instance = new ContinuationEqualityComparer();

public bool Equals(Continuation? x, Continuation? y)
{
return ReferenceEquals(x, y);
}

public unsafe int GetHashCode([DisallowNull] Continuation obj)
{
object o = (object)obj;
return RuntimeHelpers.GetHashCode(o);
}
}

[StructLayout(LayoutKind.Explicit)]
internal unsafe ref struct AsyncDispatcherInfo
{
Expand All @@ -161,6 +180,15 @@ internal unsafe ref struct AsyncDispatcherInfo
[FieldOffset(4)]
#endif
public Continuation? NextContinuation;
#if TARGET_64BIT
[FieldOffset(16)]
#else
[FieldOffset(8)]
#endif
// The runtime async Task being dispatched.
// This is used by debuggers in the case of nested dispatcher info (multiple runtime-async Tasks on the same thread)
// to match an inflight Task to the corresponding Continuation chain.
public Task Task;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A comment describing which Task gets stored here and when would be helpful.


// Information about current task dispatching, to be used for async
// stackwalking.
Expand Down Expand Up @@ -371,6 +399,17 @@ internal void HandleSuspended()

SetContinuationState(headContinuation);

Continuation? nc = headContinuation;
if (Task.s_asyncDebuggingEnabled)
{
while (nc != null)
{
// On suspension we set tick info for all continuations that have not yet had it set.
Task.SetRuntimeAsyncContinuationTicks(nc, Stopwatch.GetTimestamp());
nc = nc.Next;
}
}

try
{
if (critNotifier != null)
Expand Down Expand Up @@ -445,6 +484,13 @@ private unsafe void DispatchContinuations()
asyncDispatcherInfo.Next = AsyncDispatcherInfo.t_current;
asyncDispatcherInfo.NextContinuation = MoveContinuationState();
AsyncDispatcherInfo.t_current = &asyncDispatcherInfo;
asyncDispatcherInfo.Task = this;
bool isTplEnabled = TplEventSource.Log.IsEnabled();

if (isTplEnabled)
{
TplEventSource.Log.TraceSynchronousWorkBegin(this.Id, CausalitySynchronousWork.Execution);
}

while (true)
{
Expand All @@ -456,15 +502,28 @@ private unsafe void DispatchContinuations()
asyncDispatcherInfo.NextContinuation = nextContinuation;

ref byte resultLoc = ref nextContinuation != null ? ref nextContinuation.GetResultStorageOrNull() : ref GetResultStorage();
RuntimeAsyncContinuationDebugInfo? debugInfo = null;
if (Task.s_asyncDebuggingEnabled)
{
debugInfo = Task.GetRuntimeAsyncContinuationDebugInfo(curContinuation, out RuntimeAsyncContinuationDebugInfo? debugInfoVal) ? debugInfoVal : new RuntimeAsyncContinuationDebugInfo(Stopwatch.GetTimestamp());
// we have dequeued curContinuation; update task tick info so that we can track its start time from a debugger
Task.UpdateRuntimeAsyncTaskTicks(this, debugInfo.TickCount);
}
Continuation? newContinuation = curContinuation.ResumeInfo->Resume(curContinuation, ref resultLoc);

if (Task.s_asyncDebuggingEnabled)
Task.RemoveRuntimeAsyncContinuationTicks(curContinuation);

if (newContinuation != null)
{
// we have a new Continuation that belongs to the same logical invocation as the previous; propagate debug info from previous continuation
if (Task.s_asyncDebuggingEnabled)
Task.UpdateRuntimeAsyncContinuationDebugInfo(newContinuation, debugInfo!);
newContinuation.Next = nextContinuation;
HandleSuspended();
contexts.Pop();
AsyncDispatcherInfo.t_current = asyncDispatcherInfo.Next;
return;
break;
}
}
catch (Exception ex)
Expand All @@ -486,7 +545,7 @@ private unsafe void DispatchContinuations()
ThrowHelper.ThrowInvalidOperationException(ExceptionResource.TaskT_TransitionToFinal_AlreadyCompleted);
}

return;
break;
}

handlerContinuation.SetException(ex);
Expand All @@ -495,6 +554,15 @@ private unsafe void DispatchContinuations()

if (asyncDispatcherInfo.NextContinuation == null)
{
if (isTplEnabled)
{
TplEventSource.Log.TraceOperationEnd(this.Id, AsyncCausalityStatus.Completed);
}
if (Task.s_asyncDebuggingEnabled)
{
Task.RemoveFromActiveTasks(this);
Task.RemoveRuntimeAsyncTaskTicks(this);
}
bool successfullySet = TrySetResult(m_result);

contexts.Pop();
Expand All @@ -506,16 +574,20 @@ private unsafe void DispatchContinuations()
ThrowHelper.ThrowInvalidOperationException(ExceptionResource.TaskT_TransitionToFinal_AlreadyCompleted);
}

return;
break;
}

if (QueueContinuationFollowUpActionIfNecessary(asyncDispatcherInfo.NextContinuation))
{
contexts.Pop();
AsyncDispatcherInfo.t_current = asyncDispatcherInfo.Next;
return;
break;
}
}
if (isTplEnabled)
{
TplEventSource.Log.TraceSynchronousWorkEnd(CausalitySynchronousWork.Execution);
}
}

private ref byte GetResultStorage() => ref Unsafe.As<T?, byte>(ref m_result);
Expand All @@ -526,7 +598,8 @@ private unsafe void DispatchContinuations()
{
if (continuation == null || (continuation.Flags & ContinuationFlags.HasException) != 0)
return continuation;

if (Task.s_asyncDebuggingEnabled)
Task.RemoveRuntimeAsyncContinuationTicks(continuation);
continuation = continuation.Next;
}
}
Expand Down Expand Up @@ -614,13 +687,25 @@ private bool QueueContinuationFollowUpActionIfNecessary(Continuation continuatio
private static Task<T?> FinalizeTaskReturningThunk<T>()
{
RuntimeAsyncTask<T?> result = new();
if (Task.s_asyncDebuggingEnabled)
Task.AddToActiveTasks(result);
if (TplEventSource.Log.IsEnabled())
{
TplEventSource.Log.TraceOperationBegin(result.Id, "System.Runtime.CompilerServices.AsyncHelpers+RuntimeAsyncTask", 0);
}
result.HandleSuspended();
return result;
}

private static Task FinalizeTaskReturningThunk()
{
RuntimeAsyncTask<VoidTaskResult> result = new();
if (Task.s_asyncDebuggingEnabled)
Task.AddToActiveTasks(result);
if (TplEventSource.Log.IsEnabled())
{
TplEventSource.Log.TraceOperationBegin(result.Id, "System.Runtime.CompilerServices.AsyncHelpers+RuntimeAsyncTask", 0);
}
result.HandleSuspended();
return result;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,6 @@ public class Task : IAsyncResult, IDisposable
// The delegate to invoke for a delegate-backed Task.
// This field also may be used by async state machines to cache an Action.
internal Delegate? m_action;

private protected object? m_stateObject; // A state object that can be optionally supplied, passed to action.
internal TaskScheduler? m_taskScheduler; // The task scheduler this task runs under.

Expand Down Expand Up @@ -178,6 +177,15 @@ internal enum TaskStateFlags
// task. This is to be used by the debugger ONLY. Task in this dictionary represent current active tasks.
private static Dictionary<int, Task>? s_currentActiveTasks;

#if !MONO
// Dictionary that relates a runtime-async Task's ID to the QPC tick count when the current inflight invocation started.
// Needed because Continuations that are inflight have already been dequeued from the chain.
internal static System.Collections.Concurrent.ConcurrentDictionary<int, long>? s_runtimeAsyncTaskTicks;
// Dictionary to store debug info about runtime-async Continuations.
// The TickCount field stores the QPC tick count when the logical invocation to which the Continuation belongs started.
// The ID field stores a unique ID for the Continuation, similar to Task IDs.
internal static System.Collections.Concurrent.ConcurrentDictionary<Continuation, RuntimeAsyncContinuationDebugInfo>? s_runtimeAsyncContinuationTicks;
#endif
// These methods are a way to access the dictionary both from this class and for other classes that also
// activate dummy tasks. Specifically the AsyncTaskMethodBuilder and AsyncTaskMethodBuilder<>
internal static bool AddToActiveTasks(Task task)
Expand Down Expand Up @@ -211,6 +219,49 @@ internal static void RemoveFromActiveTasks(Task task)
}
}

#if !MONO
internal static void SetRuntimeAsyncContinuationTicks(Continuation continuation, long tickCount)
{
s_runtimeAsyncContinuationTicks ??= new Collections.Concurrent.ConcurrentDictionary<Continuation, RuntimeAsyncContinuationDebugInfo>(ContinuationEqualityComparer.Instance);
s_runtimeAsyncContinuationTicks.TryAdd(continuation, new RuntimeAsyncContinuationDebugInfo(tickCount));
}

internal static bool GetRuntimeAsyncContinuationDebugInfo(Continuation continuation, [NotNullWhen(true)] out RuntimeAsyncContinuationDebugInfo? debugInfo)
{
if (s_runtimeAsyncContinuationTicks != null && s_runtimeAsyncContinuationTicks.TryGetValue(continuation, out debugInfo))
{
return true;
}
debugInfo = null;
return false;
}

internal static void UpdateRuntimeAsyncContinuationDebugInfo(Continuation continuation, RuntimeAsyncContinuationDebugInfo debugInfo)
{
s_runtimeAsyncContinuationTicks ??= new Collections.Concurrent.ConcurrentDictionary<Continuation, RuntimeAsyncContinuationDebugInfo>(ContinuationEqualityComparer.Instance);
s_runtimeAsyncContinuationTicks[continuation] = debugInfo;
}

internal static void RemoveRuntimeAsyncContinuationTicks(Continuation continuation)
{
s_runtimeAsyncContinuationTicks?.Remove(continuation, out _);
}

internal static void UpdateRuntimeAsyncTaskTicks(Task task, long inflightTickCount)
{
if (s_asyncDebuggingEnabled)
{
s_runtimeAsyncTaskTicks ??= [];
s_runtimeAsyncTaskTicks[task.Id] = inflightTickCount;
}
}

internal static void RemoveRuntimeAsyncTaskTicks(Task task)
{
s_runtimeAsyncTaskTicks?.Remove(task.Id, out _);
}
#endif

// We moved a number of Task properties into this class. The idea is that in most cases, these properties never
// need to be accessed during the life cycle of a Task, so we don't want to instantiate them every time. Once
// one of these properties needs to be written, we will instantiate a ContingentProperties object and set
Expand Down Expand Up @@ -7576,4 +7627,18 @@ private void ProcessInnerTask(Task? task)

public bool InvokeMayRunArbitraryCode => true;
}

#if !MONO
internal sealed class RuntimeAsyncContinuationDebugInfo
{
internal long TickCount;
internal int Id;

internal RuntimeAsyncContinuationDebugInfo(long tickCount)
{
TickCount = tickCount;
Id = Task.NewId();
}
}
#endif
}
Loading