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,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
{
Expand All @@ -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.
Expand Down Expand Up @@ -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;
}

Expand All @@ -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;
}

Expand All @@ -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
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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)
{
Expand All @@ -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;
Comment on lines 498 to +505
Copy link

Copilot AI Jan 29, 2026

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.

Copilot uses AI. Check for mistakes.
}
}
catch (Exception ex)
Expand All @@ -486,7 +524,7 @@ private unsafe void DispatchContinuations()
ThrowHelper.ThrowInvalidOperationException(ExceptionResource.TaskT_TransitionToFinal_AlreadyCompleted);
}

return;
break;
}

handlerContinuation.SetException(ex);
Expand All @@ -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
Copy link

Copilot AI Jan 29, 2026

Choose a reason for hiding this comment

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

The calls to TraceOperationEnd, RemoveFromActiveTasks, and RemoveRuntimeAsyncTaskTicks are placed inside the TplEventSource.Log.IsEnabled() check. However, these cleanup operations should be performed regardless of whether tracing is enabled. Looking at the pattern in Task.cs (lines 2110-2143), RemoveFromActiveTasks is called when s_asyncDebuggingEnabled is true, not when TplEventSource.Log.IsEnabled() is true. The RemoveRuntimeAsyncTaskTicks call should also be conditional on s_asyncDebuggingEnabled and should be executed outside the TplEventSource conditional block.

Suggested change
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 uses AI. Check for mistakes.
bool successfullySet = TrySetResult(m_result);

contexts.Pop();
Expand All @@ -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;
Comment on lines 556 to +560
Copy link

Copilot AI Jan 29, 2026

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.

Copilot uses AI. Check for mistakes.
}
}
if (TplEventSource.Log.IsEnabled())
{
TplEventSource.Log.TraceSynchronousWorkEnd(CausalitySynchronousWork.Execution);
}
}

private ref byte GetResultStorage() => ref Unsafe.As<T?, byte>(ref m_result);
Expand Down Expand Up @@ -614,13 +662,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 @@ -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");
Expand Down Expand Up @@ -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);
Copy link

Copilot AI Jan 29, 2026

Choose a reason for hiding this comment

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

Inconsistent namespace qualification: The ConcurrentDictionary type should be fully qualified as System.Collections.Concurrent.ConcurrentDictionary to match the field declarations on lines 184 and 187. The file does not have a using System.Collections.Concurrent; directive.

Copilot uses AI. Check for mistakes.
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);
Copy link

Copilot AI Jan 29, 2026

Choose a reason for hiding this comment

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

Inconsistent namespace qualification: The ConcurrentDictionary type should be fully qualified as System.Collections.Concurrent.ConcurrentDictionary to match the field declarations on lines 184 and 187. The file does not have a using System.Collections.Concurrent; directive.

Copilot uses AI. Check for mistakes.
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
Expand Down
Loading