diff --git a/src/coreclr/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncHelpers.CoreCLR.cs b/src/coreclr/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncHelpers.CoreCLR.cs index e77eb6409cce51..496df6b7e016b6 100644 --- a/src/coreclr/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncHelpers.CoreCLR.cs +++ b/src/coreclr/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncHelpers.CoreCLR.cs @@ -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 @@ -146,6 +147,22 @@ public ref byte GetResultStorageOrNull() } } + internal class ContinuationEqualityComparer : IEqualityComparer + { + 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 { @@ -161,6 +178,12 @@ internal unsafe ref struct AsyncDispatcherInfo [FieldOffset(4)] #endif public Continuation? NextContinuation; +#if TARGET_64BIT + [FieldOffset(16)] +#else + [FieldOffset(8)] +#endif + public Task Task; // Information about current task dispatching, to be used for async // stackwalking. @@ -336,6 +359,7 @@ private void SetContinuationState(Continuation value) { Debug.Assert(m_stateObject == null); m_stateObject = value; + Task.SetRuntimeAsyncContinuationTicks(value, Stopwatch.GetTimestamp()); } internal void HandleSuspended() @@ -371,6 +395,13 @@ internal void HandleSuspended() SetContinuationState(headContinuation); + Continuation? nc = headContinuation.Next; + while (nc != null) + { + Task.SetRuntimeAsyncContinuationTicks(nc, Stopwatch.GetTimestamp()); + nc = nc.Next; + } + try { if (critNotifier != null) @@ -445,6 +476,12 @@ private unsafe void DispatchContinuations() asyncDispatcherInfo.Next = AsyncDispatcherInfo.t_current; asyncDispatcherInfo.NextContinuation = MoveContinuationState(); AsyncDispatcherInfo.t_current = &asyncDispatcherInfo; + asyncDispatcherInfo.Task = this; + + if (TplEventSource.Log.IsEnabled()) + { + TplEventSource.Log.TraceSynchronousWorkBegin(this.Id, CausalitySynchronousWork.Execution); + } while (true) { @@ -456,15 +493,20 @@ private unsafe void DispatchContinuations() asyncDispatcherInfo.NextContinuation = nextContinuation; ref byte resultLoc = ref nextContinuation != null ? ref nextContinuation.GetResultStorageOrNull() : ref GetResultStorage(); + RuntimeAsyncContinuationDebugInfo debugInfo = Task.GetRuntimeAsyncContinuationDebugInfo(curContinuation, out RuntimeAsyncContinuationDebugInfo debugInfoVal) ? debugInfoVal : new RuntimeAsyncContinuationDebugInfo(Stopwatch.GetTimestamp()); + Task.UpdateRuntimeAsyncTaskTicks(this, debugInfo.TickCount); Continuation? newContinuation = curContinuation.ResumeInfo->Resume(curContinuation, ref resultLoc); + Task.RemoveRuntimeAsyncContinuationTicks(curContinuation); + if (newContinuation != null) { + Task.UpdateRuntimeAsyncContinuationDebugInfo(newContinuation, debugInfo); newContinuation.Next = nextContinuation; HandleSuspended(); contexts.Pop(); AsyncDispatcherInfo.t_current = asyncDispatcherInfo.Next; - return; + break; } } catch (Exception ex) @@ -486,7 +528,7 @@ private unsafe void DispatchContinuations() ThrowHelper.ThrowInvalidOperationException(ExceptionResource.TaskT_TransitionToFinal_AlreadyCompleted); } - return; + break; } handlerContinuation.SetException(ex); @@ -495,6 +537,12 @@ private unsafe void DispatchContinuations() if (asyncDispatcherInfo.NextContinuation == null) { + if (TplEventSource.Log.IsEnabled()) + { + TplEventSource.Log.TraceOperationEnd(this.Id, AsyncCausalityStatus.Completed); + Task.RemoveFromActiveTasks(this); + Task.RemoveRuntimeAsyncTaskTicks(this); + } bool successfullySet = TrySetResult(m_result); contexts.Pop(); @@ -506,16 +554,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 (TplEventSource.Log.IsEnabled()) + { + TplEventSource.Log.TraceSynchronousWorkEnd(CausalitySynchronousWork.Execution); + } } private ref byte GetResultStorage() => ref Unsafe.As(ref m_result); @@ -527,6 +579,7 @@ private unsafe void DispatchContinuations() if (continuation == null || (continuation.Flags & ContinuationFlags.HasException) != 0) return continuation; + RemoveRuntimeAsyncContinuationTicks(continuation); continuation = continuation.Next; } } @@ -614,6 +667,12 @@ private bool QueueContinuationFollowUpActionIfNecessary(Continuation continuatio private static Task FinalizeTaskReturningThunk() { RuntimeAsyncTask 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; } @@ -621,6 +680,12 @@ private bool QueueContinuationFollowUpActionIfNecessary(Continuation continuatio private static Task FinalizeTaskReturningThunk() { RuntimeAsyncTask 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; } diff --git a/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Task.cs b/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Task.cs index 1e64b0c17a51e2..b9537165938f19 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Task.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Task.cs @@ -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. @@ -180,6 +179,13 @@ internal enum TaskStateFlags // 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<> + + // Dictionary that relates Tasks to the tick count of the inflight task for debugging purposes + internal static System.Collections.Concurrent.ConcurrentDictionary? s_runtimeAsyncTaskTicks; +#if !MONO + // Dictionary that relates Continuations to their creation tick count for debugging purposes + internal static System.Collections.Concurrent.ConcurrentDictionary? s_runtimeAsyncContinuationTicks; +#endif internal static bool AddToActiveTasks(Task task) { Debug.Assert(task != null, "Null Task objects can't be added to the ActiveTasks collection"); @@ -211,6 +217,55 @@ internal static void RemoveFromActiveTasks(Task task) } } +#if !MONO + internal static void SetRuntimeAsyncContinuationTicks(Continuation continuation, long tickCount) + { + if (s_asyncDebuggingEnabled) + { + s_runtimeAsyncContinuationTicks ??= new Collections.Concurrent.ConcurrentDictionary(ContinuationEqualityComparer.Instance); + s_runtimeAsyncContinuationTicks.TryAdd(continuation, new RuntimeAsyncContinuationDebugInfo(tickCount)); + } + } + + internal static bool GetRuntimeAsyncContinuationDebugInfo(Continuation continuation, out RuntimeAsyncContinuationDebugInfo debugInfo) + { + if (s_asyncDebuggingEnabled && s_runtimeAsyncContinuationTicks != null && s_runtimeAsyncContinuationTicks.TryGetValue(continuation, out debugInfo)) + { + return true; + } + debugInfo = null; + return false; + } + + internal static void UpdateRuntimeAsyncContinuationDebugInfo(Continuation continuation, RuntimeAsyncContinuationDebugInfo debugInfo) + { + if (s_asyncDebuggingEnabled) + { + s_runtimeAsyncContinuationTicks ??= new Collections.Concurrent.ConcurrentDictionary(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 @@ -7576,4 +7631,16 @@ private void ProcessInnerTask(Task? task) public bool InvokeMayRunArbitraryCode => true; } + + internal class RuntimeAsyncContinuationDebugInfo + { + public long TickCount; + public int Id; + + public RuntimeAsyncContinuationDebugInfo(long tickCount) + { + TickCount = tickCount; + Id = Task.NewId(); + } + } }