-
Notifications
You must be signed in to change notification settings - Fork 5.3k
Managed fixes for async task tracking #123727
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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<Continuation> | ||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||
| 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<byte, object?>(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<byte, object?>(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.UpdateRuntimeAsyncContinuationTicks(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); | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
Comment on lines
+538
to
+541
|
||||||||||||||||||||||||||
| TplEventSource.Log.TraceOperationEnd(this.Id, AsyncCausalityStatus.Completed); | |
| Task.RemoveFromActiveTasks(this); | |
| Task.RemoveRuntimeAsyncTaskTicks(this); | |
| } | |
| TplEventSource.Log.TraceOperationEnd(this.Id, AsyncCausalityStatus.Completed); | |
| } | |
| if (Task.s_asyncDebuggingEnabled) | |
| { | |
| Task.RemoveFromActiveTasks(this); | |
| Task.RemoveRuntimeAsyncTaskTicks(this); | |
| } |
Copilot
AI
Jan 29, 2026
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.
Missing TraceSynchronousWorkEnd event when queuing continuation follow-up action. When QueueContinuationFollowUpActionIfNecessary returns true (lines 556-561), the execution exits the while loop without calling TraceSynchronousWorkEnd. This creates an asymmetry with the TraceSynchronousWorkBegin call at line 479, potentially leading to incorrect tracing behavior. The TraceSynchronousWorkEnd should be called before the break statement to properly close the synchronous work region.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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<int, long>? s_runtimeAsyncTaskTicks; | ||
| #if !MONO | ||
| // Dictionary that relates Continuations to their creation tick count for debugging purposes | ||
| internal static System.Collections.Concurrent.ConcurrentDictionary<Continuation, long>? 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<Continuation, long>(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 UpdateRuntimeAsyncContinuationTicks(Continuation continuation, long tickCount) | ||
| { | ||
| if (s_asyncDebuggingEnabled) | ||
| { | ||
| s_runtimeAsyncContinuationTicks ??= new Collections.Concurrent.ConcurrentDictionary<Continuation, long>(ContinuationEqualityComparer.Instance); | ||
|
||
| s_runtimeAsyncContinuationTicks[continuation] = tickCount; | ||
| } | ||
| } | ||
|
|
||
| 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 | ||
|
|
||
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.
Missing cleanup operation when task suspends. When a newContinuation is returned (lines 498-506), the task is being suspended and will resume later. However, the TraceOperationEnd and TraceSynchronousWorkEnd events are not being called before the suspension. Based on the TraceSynchronousWorkBegin call at line 479, there should be a corresponding TraceSynchronousWorkEnd call here to properly close the synchronous work region. This asymmetry could lead to incorrect tracing behavior.