From 7d950cbf13b455d1adccc9193bdba705df785f30 Mon Sep 17 00:00:00 2001 From: rcj1 Date: Wed, 28 Jan 2026 11:20:27 -0800 Subject: [PATCH 1/5] e --- .../CompilerServices/AsyncHelpers.CoreCLR.cs | 68 +++++++++++++++++-- .../src/System/Threading/Tasks/Task.cs | 45 +++++++++++- 2 files changed, 108 insertions(+), 5 deletions(-) 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..427ed676fa334e 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. @@ -237,6 +260,7 @@ private static unsafe Continuation AllocContinuation(Continuation prevContinuati Continuation newContinuation = (Continuation)RuntimeTypeHandle.InternalAllocNoChecks(contMT); #endif prevContinuation.Next = newContinuation; + Task.SetRuntimeAsyncContinuationTicks(newContinuation, Environment.TickCount64); return newContinuation; } @@ -247,6 +271,7 @@ private static unsafe Continuation AllocContinuationMethod(Continuation prevCont Continuation newContinuation = (Continuation)RuntimeTypeHandle.InternalAllocNoChecks(contMT); Unsafe.As(ref Unsafe.Add(ref RuntimeHelpers.GetRawData(newContinuation), keepAliveOffset)) = loaderAllocator; prevContinuation.Next = newContinuation; + Task.SetRuntimeAsyncContinuationTicks(newContinuation, Environment.TickCount64); return newContinuation; } @@ -260,6 +285,7 @@ private static unsafe Continuation AllocContinuationClass(Continuation prevConti { Unsafe.As(ref Unsafe.Add(ref RuntimeHelpers.GetRawData(newContinuation), keepAliveOffset)) = GCHandle.FromIntPtr(loaderAllocatorHandle).Target; } + Task.SetRuntimeAsyncContinuationTicks(newContinuation, Environment.TickCount64); return newContinuation; } #endif @@ -336,6 +362,7 @@ private void SetContinuationState(Continuation value) { Debug.Assert(m_stateObject == null); m_stateObject = value; + Task.SetRuntimeAsyncContinuationTicks(value, Environment.TickCount64); } internal void HandleSuspended() @@ -445,6 +472,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 +489,20 @@ private unsafe void DispatchContinuations() asyncDispatcherInfo.NextContinuation = nextContinuation; ref byte resultLoc = ref nextContinuation != null ? ref nextContinuation.GetResultStorageOrNull() : ref GetResultStorage(); + long tickCount = Task.GetRuntimeAsyncContinuationTicks(curContinuation, out long tickCountVal) ? tickCountVal : Environment.TickCount64; + Task.UpdateRuntimeAsyncTaskTicks(this, tickCount); Continuation? newContinuation = curContinuation.ResumeInfo->Resume(curContinuation, ref resultLoc); + Task.RemoveRuntimeAsyncContinuationTicks(curContinuation); + if (newContinuation != null) { + Task.SetRuntimeAsyncContinuationTicks(newContinuation, tickCount); newContinuation.Next = nextContinuation; HandleSuspended(); contexts.Pop(); AsyncDispatcherInfo.t_current = asyncDispatcherInfo.Next; - return; + break; } } catch (Exception ex) @@ -486,7 +524,7 @@ private unsafe void DispatchContinuations() ThrowHelper.ThrowInvalidOperationException(ExceptionResource.TaskT_TransitionToFinal_AlreadyCompleted); } - return; + break; } handlerContinuation.SetException(ex); @@ -495,6 +533,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 +550,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); @@ -614,6 +662,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 +675,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..0921f3e7eb5515 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,12 @@ 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; + + // Dictionary that relates Continuations to their creation tick count for debugging purposes + internal static System.Collections.Concurrent.ConcurrentDictionary? s_runtimeAsyncContinuationTicks; internal static bool AddToActiveTasks(Task task) { Debug.Assert(task != null, "Null Task objects can't be added to the ActiveTasks collection"); @@ -211,6 +216,44 @@ internal static void RemoveFromActiveTasks(Task task) } } + internal static void SetRuntimeAsyncContinuationTicks(Continuation continuation, long tickCount) + { + if (s_asyncDebuggingEnabled) + { + s_runtimeAsyncContinuationTicks ??= new Collections.Concurrent.ConcurrentDictionary(ContinuationEqualityComparer.Instance); + s_runtimeAsyncContinuationTicks.TryAdd(continuation, tickCount); + } + } + + internal static bool GetRuntimeAsyncContinuationTicks(Continuation continuation, out long tickCount) + { + if (s_asyncDebuggingEnabled && s_runtimeAsyncContinuationTicks != null && s_runtimeAsyncContinuationTicks.TryGetValue(continuation, out tickCount)) + { + return true; + } + tickCount = 0; + return false; + } + + 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 _); + } + // 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 From 788fb654a71f579f93a9027017ad6fa0e8bd0017 Mon Sep 17 00:00:00 2001 From: rcj1 Date: Thu, 29 Jan 2026 10:37:25 -0800 Subject: [PATCH 2/5] TBD: remove unneeded updates --- .../CompilerServices/AsyncHelpers.CoreCLR.cs | 2 +- .../src/System/Threading/Tasks/Task.cs | 14 +++++++++++++- 2 files changed, 14 insertions(+), 2 deletions(-) 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 427ed676fa334e..46abe32bf85146 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 @@ -497,7 +497,7 @@ private unsafe void DispatchContinuations() if (newContinuation != null) { - Task.SetRuntimeAsyncContinuationTicks(newContinuation, tickCount); + Task.UpdateRuntimeAsyncContinuationTicks(newContinuation, tickCount); newContinuation.Next = nextContinuation; HandleSuspended(); contexts.Pop(); 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 0921f3e7eb5515..5c6af31a918a0d 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 @@ -182,9 +182,10 @@ internal enum TaskStateFlags // 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"); @@ -216,6 +217,7 @@ internal static void RemoveFromActiveTasks(Task task) } } +#if !MONO internal static void SetRuntimeAsyncContinuationTicks(Continuation continuation, long tickCount) { if (s_asyncDebuggingEnabled) @@ -235,6 +237,15 @@ internal static bool GetRuntimeAsyncContinuationTicks(Continuation continuation, return false; } + internal static void UpdateRuntimeAsyncContinuationTicks(Continuation continuation, long tickCount) + { + if (s_asyncDebuggingEnabled) + { + s_runtimeAsyncContinuationTicks ??= new Collections.Concurrent.ConcurrentDictionary(ContinuationEqualityComparer.Instance); + s_runtimeAsyncContinuationTicks[continuation] = tickCount; + } + } + internal static void RemoveRuntimeAsyncContinuationTicks(Continuation continuation) { s_runtimeAsyncContinuationTicks?.Remove(continuation, out _); @@ -253,6 +264,7 @@ 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 From 908059e1f0c69108bb4be8278bf2fae7877f22f1 Mon Sep 17 00:00:00 2001 From: rcj1 Date: Fri, 30 Jan 2026 10:51:23 -0800 Subject: [PATCH 3/5] adding IDs etc --- .../CompilerServices/AsyncHelpers.CoreCLR.cs | 19 +++++++----- .../src/System/Threading/Tasks/Task.cs | 30 +++++++++++++------ 2 files changed, 33 insertions(+), 16 deletions(-) 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 46abe32bf85146..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 @@ -260,7 +260,6 @@ private static unsafe Continuation AllocContinuation(Continuation prevContinuati Continuation newContinuation = (Continuation)RuntimeTypeHandle.InternalAllocNoChecks(contMT); #endif prevContinuation.Next = newContinuation; - Task.SetRuntimeAsyncContinuationTicks(newContinuation, Environment.TickCount64); return newContinuation; } @@ -271,7 +270,6 @@ private static unsafe Continuation AllocContinuationMethod(Continuation prevCont Continuation newContinuation = (Continuation)RuntimeTypeHandle.InternalAllocNoChecks(contMT); Unsafe.As(ref Unsafe.Add(ref RuntimeHelpers.GetRawData(newContinuation), keepAliveOffset)) = loaderAllocator; prevContinuation.Next = newContinuation; - Task.SetRuntimeAsyncContinuationTicks(newContinuation, Environment.TickCount64); return newContinuation; } @@ -285,7 +283,6 @@ private static unsafe Continuation AllocContinuationClass(Continuation prevConti { Unsafe.As(ref Unsafe.Add(ref RuntimeHelpers.GetRawData(newContinuation), keepAliveOffset)) = GCHandle.FromIntPtr(loaderAllocatorHandle).Target; } - Task.SetRuntimeAsyncContinuationTicks(newContinuation, Environment.TickCount64); return newContinuation; } #endif @@ -362,7 +359,7 @@ private void SetContinuationState(Continuation value) { Debug.Assert(m_stateObject == null); m_stateObject = value; - Task.SetRuntimeAsyncContinuationTicks(value, Environment.TickCount64); + Task.SetRuntimeAsyncContinuationTicks(value, Stopwatch.GetTimestamp()); } internal void HandleSuspended() @@ -398,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) @@ -489,15 +493,15 @@ private unsafe void DispatchContinuations() asyncDispatcherInfo.NextContinuation = nextContinuation; ref byte resultLoc = ref nextContinuation != null ? ref nextContinuation.GetResultStorageOrNull() : ref GetResultStorage(); - long tickCount = Task.GetRuntimeAsyncContinuationTicks(curContinuation, out long tickCountVal) ? tickCountVal : Environment.TickCount64; - Task.UpdateRuntimeAsyncTaskTicks(this, tickCount); + 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.UpdateRuntimeAsyncContinuationTicks(newContinuation, tickCount); + Task.UpdateRuntimeAsyncContinuationDebugInfo(newContinuation, debugInfo); newContinuation.Next = nextContinuation; HandleSuspended(); contexts.Pop(); @@ -575,6 +579,7 @@ private unsafe void DispatchContinuations() if (continuation == null || (continuation.Flags & ContinuationFlags.HasException) != 0) return continuation; + RemoveRuntimeAsyncContinuationTicks(continuation); continuation = continuation.Next; } } 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 5c6af31a918a0d..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 @@ -184,7 +184,7 @@ internal enum TaskStateFlags 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; + internal static System.Collections.Concurrent.ConcurrentDictionary? s_runtimeAsyncContinuationTicks; #endif internal static bool AddToActiveTasks(Task task) { @@ -222,27 +222,27 @@ internal static void SetRuntimeAsyncContinuationTicks(Continuation continuation, { if (s_asyncDebuggingEnabled) { - s_runtimeAsyncContinuationTicks ??= new Collections.Concurrent.ConcurrentDictionary(ContinuationEqualityComparer.Instance); - s_runtimeAsyncContinuationTicks.TryAdd(continuation, tickCount); + s_runtimeAsyncContinuationTicks ??= new Collections.Concurrent.ConcurrentDictionary(ContinuationEqualityComparer.Instance); + s_runtimeAsyncContinuationTicks.TryAdd(continuation, new RuntimeAsyncContinuationDebugInfo(tickCount)); } } - internal static bool GetRuntimeAsyncContinuationTicks(Continuation continuation, out long tickCount) + internal static bool GetRuntimeAsyncContinuationDebugInfo(Continuation continuation, out RuntimeAsyncContinuationDebugInfo debugInfo) { - if (s_asyncDebuggingEnabled && s_runtimeAsyncContinuationTicks != null && s_runtimeAsyncContinuationTicks.TryGetValue(continuation, out tickCount)) + if (s_asyncDebuggingEnabled && s_runtimeAsyncContinuationTicks != null && s_runtimeAsyncContinuationTicks.TryGetValue(continuation, out debugInfo)) { return true; } - tickCount = 0; + debugInfo = null; return false; } - internal static void UpdateRuntimeAsyncContinuationTicks(Continuation continuation, long tickCount) + internal static void UpdateRuntimeAsyncContinuationDebugInfo(Continuation continuation, RuntimeAsyncContinuationDebugInfo debugInfo) { if (s_asyncDebuggingEnabled) { - s_runtimeAsyncContinuationTicks ??= new Collections.Concurrent.ConcurrentDictionary(ContinuationEqualityComparer.Instance); - s_runtimeAsyncContinuationTicks[continuation] = tickCount; + s_runtimeAsyncContinuationTicks ??= new Collections.Concurrent.ConcurrentDictionary(ContinuationEqualityComparer.Instance); + s_runtimeAsyncContinuationTicks[continuation] = debugInfo; } } @@ -7631,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(); + } + } } From 6bc980b75f1a6378a59bb723cdc97f5b530c1b7e Mon Sep 17 00:00:00 2001 From: rcj1 Date: Sat, 31 Jan 2026 12:57:13 -0800 Subject: [PATCH 4/5] comments and restructure etc --- .../CompilerServices/AsyncHelpers.CoreCLR.cs | 39 +++++++++++++------ .../src/System/Threading/Tasks/Task.cs | 34 +++++++--------- 2 files changed, 43 insertions(+), 30 deletions(-) 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 496df6b7e016b6..2adc568390710f 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 @@ -147,6 +147,8 @@ 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 { internal static readonly ContinuationEqualityComparer Instance = new ContinuationEqualityComparer(); @@ -183,6 +185,7 @@ internal unsafe ref struct AsyncDispatcherInfo #else [FieldOffset(8)] #endif + // The runtime async Task being dispatched. public Task Task; // Information about current task dispatching, to be used for async @@ -359,7 +362,6 @@ private void SetContinuationState(Continuation value) { Debug.Assert(m_stateObject == null); m_stateObject = value; - Task.SetRuntimeAsyncContinuationTicks(value, Stopwatch.GetTimestamp()); } internal void HandleSuspended() @@ -395,11 +397,15 @@ internal void HandleSuspended() SetContinuationState(headContinuation); - Continuation? nc = headContinuation.Next; - while (nc != null) + Continuation? nc = headContinuation; + if (Task.s_asyncDebuggingEnabled) { - Task.SetRuntimeAsyncContinuationTicks(nc, Stopwatch.GetTimestamp()); - nc = nc.Next; + 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 @@ -477,8 +483,9 @@ private unsafe void DispatchContinuations() asyncDispatcherInfo.NextContinuation = MoveContinuationState(); AsyncDispatcherInfo.t_current = &asyncDispatcherInfo; asyncDispatcherInfo.Task = this; + bool isTplEnabled = TplEventSource.Log.IsEnabled(); - if (TplEventSource.Log.IsEnabled()) + if (isTplEnabled) { TplEventSource.Log.TraceSynchronousWorkBegin(this.Id, CausalitySynchronousWork.Execution); } @@ -493,15 +500,22 @@ 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); + if (Task.s_asyncDebuggingEnabled) + { + RuntimeAsyncContinuationDebugInfo 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); - Task.RemoveRuntimeAsyncContinuationTicks(curContinuation); + if (Task.s_asyncDebuggingEnabled) + Task.RemoveRuntimeAsyncContinuationTicks(curContinuation); if (newContinuation != null) { - Task.UpdateRuntimeAsyncContinuationDebugInfo(newContinuation, debugInfo); + // 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(); @@ -540,6 +554,9 @@ private unsafe void DispatchContinuations() if (TplEventSource.Log.IsEnabled()) { TplEventSource.Log.TraceOperationEnd(this.Id, AsyncCausalityStatus.Completed); + } + if (Task.s_asyncDebuggingEnabled) + { Task.RemoveFromActiveTasks(this); Task.RemoveRuntimeAsyncTaskTicks(this); } @@ -564,7 +581,7 @@ private unsafe void DispatchContinuations() break; } } - if (TplEventSource.Log.IsEnabled()) + if (isTplEnabled) { TplEventSource.Log.TraceSynchronousWorkEnd(CausalitySynchronousWork.Execution); } 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 b9537165938f19..55b6ab88de5632 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 @@ -177,15 +177,17 @@ internal enum TaskStateFlags // task. This is to be used by the debugger ONLY. Task in this dictionary represent current active tasks. private static Dictionary? s_currentActiveTasks; - // 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 + // 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? 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? 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) { Debug.Assert(task != null, "Null Task objects can't be added to the ActiveTasks collection"); @@ -220,16 +222,13 @@ 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)); - } + s_runtimeAsyncContinuationTicks ??= new Collections.Concurrent.ConcurrentDictionary(ContinuationEqualityComparer.Instance); + s_runtimeAsyncContinuationTicks.TryAdd(continuation, new RuntimeAsyncContinuationDebugInfo(tickCount)); } - internal static bool GetRuntimeAsyncContinuationDebugInfo(Continuation continuation, out RuntimeAsyncContinuationDebugInfo debugInfo) + internal static bool GetRuntimeAsyncContinuationDebugInfo(Continuation continuation, [NotNullWhen(true)] out RuntimeAsyncContinuationDebugInfo? debugInfo) { - if (s_asyncDebuggingEnabled && s_runtimeAsyncContinuationTicks != null && s_runtimeAsyncContinuationTicks.TryGetValue(continuation, out debugInfo)) + if (s_runtimeAsyncContinuationTicks != null && s_runtimeAsyncContinuationTicks.TryGetValue(continuation, out debugInfo)) { return true; } @@ -239,11 +238,8 @@ internal static bool GetRuntimeAsyncContinuationDebugInfo(Continuation continuat internal static void UpdateRuntimeAsyncContinuationDebugInfo(Continuation continuation, RuntimeAsyncContinuationDebugInfo debugInfo) { - if (s_asyncDebuggingEnabled) - { - s_runtimeAsyncContinuationTicks ??= new Collections.Concurrent.ConcurrentDictionary(ContinuationEqualityComparer.Instance); - s_runtimeAsyncContinuationTicks[continuation] = debugInfo; - } + s_runtimeAsyncContinuationTicks ??= new Collections.Concurrent.ConcurrentDictionary(ContinuationEqualityComparer.Instance); + s_runtimeAsyncContinuationTicks[continuation] = debugInfo; } internal static void RemoveRuntimeAsyncContinuationTicks(Continuation continuation) @@ -7632,7 +7628,7 @@ private void ProcessInnerTask(Task? task) public bool InvokeMayRunArbitraryCode => true; } - internal class RuntimeAsyncContinuationDebugInfo + internal sealed class RuntimeAsyncContinuationDebugInfo { public long TickCount; public int Id; From 79ced182f43ca3faff10dbdfb74e149f7b50715e Mon Sep 17 00:00:00 2001 From: Rachel Date: Sat, 31 Jan 2026 13:04:15 -0800 Subject: [PATCH 5/5] Enhance comments for Task field in AsyncHelpers Added comments to clarify the purpose of the Task field. --- .../CompilerServices/AsyncHelpers.CoreCLR.cs | 13 ++++++++----- .../src/System/Threading/Tasks/Task.cs | 8 +++++--- 2 files changed, 13 insertions(+), 8 deletions(-) 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 2adc568390710f..674d5989b38ee0 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 @@ -186,6 +186,8 @@ internal unsafe ref struct AsyncDispatcherInfo [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; // Information about current task dispatching, to be used for async @@ -500,9 +502,10 @@ 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) { - RuntimeAsyncContinuationDebugInfo debugInfo = Task.GetRuntimeAsyncContinuationDebugInfo(curContinuation, out RuntimeAsyncContinuationDebugInfo? debugInfoVal) ? debugInfoVal : new RuntimeAsyncContinuationDebugInfo(Stopwatch.GetTimestamp()); + 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); } @@ -515,7 +518,7 @@ private unsafe void DispatchContinuations() { // 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); + Task.UpdateRuntimeAsyncContinuationDebugInfo(newContinuation, debugInfo!); newContinuation.Next = nextContinuation; HandleSuspended(); contexts.Pop(); @@ -551,7 +554,7 @@ private unsafe void DispatchContinuations() if (asyncDispatcherInfo.NextContinuation == null) { - if (TplEventSource.Log.IsEnabled()) + if (isTplEnabled) { TplEventSource.Log.TraceOperationEnd(this.Id, AsyncCausalityStatus.Completed); } @@ -595,8 +598,8 @@ private unsafe void DispatchContinuations() { if (continuation == null || (continuation.Flags & ContinuationFlags.HasException) != 0) return continuation; - - RemoveRuntimeAsyncContinuationTicks(continuation); + if (Task.s_asyncDebuggingEnabled) + Task.RemoveRuntimeAsyncContinuationTicks(continuation); continuation = continuation.Next; } } 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 55b6ab88de5632..028f1d9b9c8470 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 @@ -7628,15 +7628,17 @@ private void ProcessInnerTask(Task? task) public bool InvokeMayRunArbitraryCode => true; } +#if !MONO internal sealed class RuntimeAsyncContinuationDebugInfo { - public long TickCount; - public int Id; + internal long TickCount; + internal int Id; - public RuntimeAsyncContinuationDebugInfo(long tickCount) + internal RuntimeAsyncContinuationDebugInfo(long tickCount) { TickCount = tickCount; Id = Task.NewId(); } } +#endif }