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 dc0fafd0a0e4cb..2230aa1e8b008f 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 @@ -1,14 +1,10 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Buffers.Binary; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; -using System.Numerics; using System.Reflection; -using System.Runtime.CompilerServices; using System.Runtime.InteropServices; -using System.Runtime.Serialization; using System.Threading; using System.Threading.Tasks; using System.Threading.Tasks.Sources; @@ -358,7 +354,7 @@ private void SetContinuationState(Continuation value) m_stateObject = value; } - internal void HandleSuspended() + internal bool HandleSuspended() { ref RuntimeAsyncAwaitState state = ref t_runtimeAsyncAwaitState; @@ -391,18 +387,6 @@ internal void HandleSuspended() SetContinuationState(headContinuation); - Continuation? nc = headContinuation; - if (Task.s_asyncDebuggingEnabled) - { - long timestamp = Stopwatch.GetTimestamp(); - while (nc != null) - { - // On suspension we set timestamp for all continuations that have not yet had it set. - Task.SetRuntimeAsyncContinuationTimestamp(nc, timestamp); - nc = nc.Next; - } - } - try { if (critNotifier != null) @@ -461,23 +445,34 @@ internal void HandleSuspended() Debug.Assert(notifier != null); notifier.OnCompleted(GetContinuationAction()); } + + return true; } catch (Exception ex) { - if (Task.s_asyncDebuggingEnabled) + Task.ThrowAsync(ex, targetContext: null); + } + + return false; + } + + internal void InstrumentedHandleSuspended(AsyncInstrumentation.Flags flags, Continuation? newContinuation = null) + { + if (AsyncInstrumentation.IsEnabled.AsyncDebugger(flags)) + { + Continuation? nextContinuation = t_runtimeAsyncAwaitState.SentinelContinuation!.Next; + + AsyncDebugger.HandleSuspended(nextContinuation, newContinuation); + + if (!HandleSuspended()) { - Task.RemoveFromActiveTasks(this); - Task.RemoveRuntimeAsyncTaskTimestamp(this); - Continuation? nextCont = headContinuation; - while (nextCont != null) - { - Task.RemoveRuntimeAsyncContinuationTimestamp(nextCont); - nextCont = nextCont.Next; - } + AsyncDebugger.HandleSuspendedFailed(this, nextContinuation); } - Task.ThrowAsync(ex, targetContext: null); + return; } + + HandleSuspended(); } #pragma warning disable CA1822 // Mark members as static @@ -488,68 +483,54 @@ public void NotifyDebuggerOfRuntimeAsyncState() #pragma warning restore CA1822 [StackTraceHidden] + // NOTE, any changes done to this method need to be replicated in InstrumentedDispatchContinuations as well. private unsafe void DispatchContinuations() { + if (RuntimeAsyncInstrumentationHelpers.InstrumentCheckPoint) + { + AsyncInstrumentation.Flags flags = AsyncInstrumentation.SyncActiveFlags(); + if (flags != AsyncInstrumentation.Flags.Disabled) + { + InstrumentedDispatchContinuations(flags); + return; + } + } + ExecutionAndSyncBlockStore contexts = default; contexts.Push(); AsyncDispatcherInfo asyncDispatcherInfo; asyncDispatcherInfo.Next = AsyncDispatcherInfo.t_current; asyncDispatcherInfo.NextContinuation = MoveContinuationState(); - asyncDispatcherInfo.CurrentTask = this; AsyncDispatcherInfo.t_current = &asyncDispatcherInfo; - bool isTplEnabled = TplEventSource.Log.IsEnabled(); - - if (isTplEnabled) - { - TplEventSource.Log.TraceSynchronousWorkBegin(this.Id, CausalitySynchronousWork.Execution); - } while (true) { Debug.Assert(asyncDispatcherInfo.NextContinuation != null); - Continuation curContinuation = asyncDispatcherInfo.NextContinuation; try { + Continuation curContinuation = asyncDispatcherInfo.NextContinuation; Continuation? nextContinuation = curContinuation.Next; asyncDispatcherInfo.NextContinuation = nextContinuation; ref byte resultLoc = ref nextContinuation != null ? ref nextContinuation.GetResultStorageOrNull() : ref GetResultStorage(); - long timestamp = 0; - if (Task.s_asyncDebuggingEnabled) - { - timestamp = Task.GetRuntimeAsyncContinuationTimestamp(curContinuation, out long timestampVal) ? timestampVal : Stopwatch.GetTimestamp(); - // we have dequeued curContinuation; update task tick info so that we can track its start time from a debugger - Task.UpdateRuntimeAsyncTaskTimestamp(this, timestamp); - } - Continuation? newContinuation = curContinuation.ResumeInfo->Resume(curContinuation, ref resultLoc); - if (Task.s_asyncDebuggingEnabled) - { - Task.RemoveRuntimeAsyncContinuationTimestamp(curContinuation); - } + Continuation? newContinuation = curContinuation.ResumeInfo->Resume(curContinuation, ref resultLoc); 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.SetRuntimeAsyncContinuationTimestamp(newContinuation, timestamp); - } newContinuation.Next = nextContinuation; HandleSuspended(); + contexts.Pop(); AsyncDispatcherInfo.t_current = asyncDispatcherInfo.Next; - break; + return; } } catch (Exception ex) { - if (Task.s_asyncDebuggingEnabled) - { - Task.RemoveRuntimeAsyncContinuationTimestamp(curContinuation); - } - Continuation? handlerContinuation = UnwindToPossibleHandler(asyncDispatcherInfo.NextContinuation, ex); + uint unwindedFrames = 1; // Count current frame. + Continuation? handlerContinuation = UnwindToPossibleHandler(asyncDispatcherInfo.NextContinuation, ex, ref unwindedFrames); if (handlerContinuation == null) { // Tail of AsyncTaskMethodBuilderT.SetException @@ -561,23 +542,12 @@ private unsafe void DispatchContinuations() AsyncDispatcherInfo.t_current = asyncDispatcherInfo.Next; - if (isTplEnabled) - { - TplEventSource.Log.TraceOperationEnd(this.Id, ex is OperationCanceledException ? AsyncCausalityStatus.Canceled : AsyncCausalityStatus.Error); - } - - if (Task.s_asyncDebuggingEnabled) - { - Task.RemoveFromActiveTasks(this); - Task.RemoveRuntimeAsyncTaskTimestamp(this); - } - if (!successfullySet) { ThrowHelper.ThrowInvalidOperationException(ExceptionResource.TaskT_TransitionToFinal_AlreadyCompleted); } - break; + return; } handlerContinuation.SetException(ex); @@ -586,15 +556,115 @@ private unsafe void DispatchContinuations() if (asyncDispatcherInfo.NextContinuation == null) { - if (isTplEnabled) + bool successfullySet = TrySetResult(m_result); + + contexts.Pop(); + + AsyncDispatcherInfo.t_current = asyncDispatcherInfo.Next; + + if (!successfullySet) { - TplEventSource.Log.TraceOperationEnd(this.Id, AsyncCausalityStatus.Completed); + ThrowHelper.ThrowInvalidOperationException(ExceptionResource.TaskT_TransitionToFinal_AlreadyCompleted); } - if (Task.s_asyncDebuggingEnabled) + + return; + } + + if (QueueContinuationFollowUpActionIfNecessary(asyncDispatcherInfo.NextContinuation)) + { + contexts.Pop(); + AsyncDispatcherInfo.t_current = asyncDispatcherInfo.Next; + return; + } + + if (RuntimeAsyncInstrumentationHelpers.InstrumentCheckPoint) + { + SetContinuationState(asyncDispatcherInfo.NextContinuation); + + contexts.Pop(); + AsyncDispatcherInfo.t_current = asyncDispatcherInfo.Next; + + InstrumentedDispatchContinuations(AsyncInstrumentation.ActiveFlags); + return; + } + } + } + + [StackTraceHidden] + private unsafe void InstrumentedDispatchContinuations(AsyncInstrumentation.Flags flags) + { + ExecutionAndSyncBlockStore contexts = default; + contexts.Push(); + + AsyncDispatcherInfo asyncDispatcherInfo; + asyncDispatcherInfo.Next = AsyncDispatcherInfo.t_current; + asyncDispatcherInfo.NextContinuation = MoveContinuationState(); + AsyncDispatcherInfo.t_current = &asyncDispatcherInfo; + + RuntimeAsyncInstrumentationHelpers.ResumeRuntimeAsyncContext(this, ref asyncDispatcherInfo, flags); + + while (true) + { + Debug.Assert(asyncDispatcherInfo.NextContinuation != null); + Continuation curContinuation = asyncDispatcherInfo.NextContinuation; + try + { + Continuation? nextContinuation = curContinuation.Next; + asyncDispatcherInfo.NextContinuation = nextContinuation; + + ref byte resultLoc = ref nextContinuation != null ? ref nextContinuation.GetResultStorageOrNull() : ref GetResultStorage(); + + RuntimeAsyncInstrumentationHelpers.ResumeRuntimeAsyncMethod(ref asyncDispatcherInfo, flags, curContinuation); + Continuation? newContinuation = curContinuation.ResumeInfo->Resume(curContinuation, ref resultLoc); + + if (newContinuation != null) { - Task.RemoveFromActiveTasks(this); - Task.RemoveRuntimeAsyncTaskTimestamp(this); + newContinuation.Next = nextContinuation; + RuntimeAsyncInstrumentationHelpers.SuspendRuntimeAsyncContext(flags, curContinuation, newContinuation); + InstrumentedHandleSuspended(flags, newContinuation); + + contexts.Pop(); + AsyncDispatcherInfo.t_current = asyncDispatcherInfo.Next; + return; } + + RuntimeAsyncInstrumentationHelpers.CompleteRuntimeAsyncMethod(flags, curContinuation); + } + catch (Exception ex) + { + uint unwindedFrames = 1; // Count current frame. + Continuation? handlerContinuation = UnwindToPossibleHandler(asyncDispatcherInfo.NextContinuation, ex, ref unwindedFrames); + if (handlerContinuation == null) + { + RuntimeAsyncInstrumentationHelpers.UnwindRuntimeAsyncMethodUnhandledException(ref asyncDispatcherInfo, flags, ex, curContinuation, unwindedFrames); + + // Tail of AsyncTaskMethodBuilderT.SetException + bool successfullySet = ex is OperationCanceledException oce ? + TrySetCanceled(oce.CancellationToken, oce) : + TrySetException(ex); + + contexts.Pop(); + + AsyncDispatcherInfo.t_current = asyncDispatcherInfo.Next; + + if (!successfullySet) + { + ThrowHelper.ThrowInvalidOperationException(ExceptionResource.TaskT_TransitionToFinal_AlreadyCompleted); + } + + return; + } + + RuntimeAsyncInstrumentationHelpers.UnwindRuntimeAsyncMethodHandledException(flags, curContinuation, unwindedFrames); + + handlerContinuation.SetException(ex); + asyncDispatcherInfo.NextContinuation = handlerContinuation; + } + + if (asyncDispatcherInfo.NextContinuation == null) + { + RuntimeAsyncInstrumentationHelpers.CompleteRuntimeAsyncContext(ref asyncDispatcherInfo, flags); + bool successfullySet = TrySetResult(m_result); contexts.Pop(); @@ -606,25 +676,25 @@ private unsafe void DispatchContinuations() ThrowHelper.ThrowInvalidOperationException(ExceptionResource.TaskT_TransitionToFinal_AlreadyCompleted); } - break; + return; } if (QueueContinuationFollowUpActionIfNecessary(asyncDispatcherInfo.NextContinuation)) { + RuntimeAsyncInstrumentationHelpers.SuspendRuntimeAsyncContext(ref asyncDispatcherInfo, flags, curContinuation); + contexts.Pop(); AsyncDispatcherInfo.t_current = asyncDispatcherInfo.Next; - break; + return; } - } - if (isTplEnabled) - { - TplEventSource.Log.TraceSynchronousWorkEnd(CausalitySynchronousWork.Execution); + + flags = AsyncInstrumentation.ActiveFlags; } } private ref byte GetResultStorage() => ref Unsafe.As(ref m_result); - private static unsafe Continuation? UnwindToPossibleHandler(Continuation? continuation, Exception ex) + private static unsafe Continuation? UnwindToPossibleHandler(Continuation? continuation, Exception ex, ref uint unwindedFrames) { while (true) { @@ -639,11 +709,9 @@ private unsafe void DispatchContinuations() } if (continuation == null || continuation.HasException()) return continuation; - if (Task.s_asyncDebuggingEnabled) - { - Task.RemoveRuntimeAsyncContinuationTimestamp(continuation); - } + continuation = continuation.Next; + unwindedFrames++; } } @@ -723,6 +791,37 @@ private bool QueueContinuationFollowUpActionIfNecessary(Continuation continuatio }; } + private static void InstrumentedFinalizeRuntimeAsyncTask(RuntimeAsyncTask task, AsyncInstrumentation.Flags flags) + { + if (AsyncInstrumentation.IsEnabled.CreateAsyncContext(flags)) + { + if (AsyncInstrumentation.IsEnabled.AsyncDebugger(flags)) + { + task.NotifyDebuggerOfRuntimeAsyncState(); + AsyncDebugger.CreateAsyncContext(task); + } + } + + task.InstrumentedHandleSuspended(flags); + return; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void FinalizeRuntimeAsyncTask(RuntimeAsyncTask task) + { + if (RuntimeAsyncInstrumentationHelpers.InstrumentCheckPoint) + { + AsyncInstrumentation.Flags flags = AsyncInstrumentation.SyncActiveFlags(); + if (flags != AsyncInstrumentation.Flags.Disabled) + { + InstrumentedFinalizeRuntimeAsyncTask(task, flags); + return; + } + } + + task.HandleSuspended(); + } + // Change return type to RuntimeAsyncTask -- no benefit since this is used for Task returning thunks only #pragma warning disable CA1859 // When a Task-returning thunk gets a continuation result @@ -730,32 +829,14 @@ private bool QueueContinuationFollowUpActionIfNecessary(Continuation continuatio private static Task FinalizeTaskReturningThunk() { RuntimeAsyncTask result = new(); - if (Task.s_asyncDebuggingEnabled) - { - result.NotifyDebuggerOfRuntimeAsyncState(); - Task.AddToActiveTasks(result); - } - if (TplEventSource.Log.IsEnabled()) - { - TplEventSource.Log.TraceOperationBegin(result.Id, "System.Runtime.CompilerServices.AsyncHelpers+RuntimeAsyncTask", 0); - } - result.HandleSuspended(); + FinalizeRuntimeAsyncTask(result!); return result; } private static Task FinalizeTaskReturningThunk() { RuntimeAsyncTask result = new(); - if (Task.s_asyncDebuggingEnabled) - { - result.NotifyDebuggerOfRuntimeAsyncState(); - Task.AddToActiveTasks(result); - } - if (TplEventSource.Log.IsEnabled()) - { - TplEventSource.Log.TraceOperationBegin(result.Id, "System.Runtime.CompilerServices.AsyncHelpers+RuntimeAsyncTask", 0); - } - result.HandleSuspended(); + FinalizeRuntimeAsyncTask(result!); return result; } @@ -915,5 +996,207 @@ internal static void CompletedTask(Task task) { TaskAwaiter.ValidateEnd(task); } + + // Instrumentation helpers called from InstrumentedDispatchContinuations. + // These methods should not throw - exceptions would break the dispatch loop. + internal static class RuntimeAsyncInstrumentationHelpers + { + public static bool InstrumentCheckPoint + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => AsyncInstrumentation.IsSupported && AsyncInstrumentation.ActiveFlags != AsyncInstrumentation.Flags.Disabled; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ResumeRuntimeAsyncContext(Task task, ref AsyncDispatcherInfo info, AsyncInstrumentation.Flags flags) + { + info.CurrentTask = task; + + if (AsyncInstrumentation.IsEnabled.ResumeAsyncContext(flags)) + { + if (AsyncInstrumentation.IsEnabled.AsyncDebugger(flags)) + { + AsyncDebugger.ResumeAsyncContext(task.Id); + } + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void SuspendRuntimeAsyncContext(ref AsyncDispatcherInfo info, AsyncInstrumentation.Flags flags, Continuation curContinuation) + { + if (AsyncInstrumentation.IsEnabled.SuspendAsyncContext(flags)) + { + if (AsyncInstrumentation.IsEnabled.AsyncDebugger(flags)) + { + AsyncDebugger.SuspendAsyncContext(ref info, curContinuation); + } + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void SuspendRuntimeAsyncContext(AsyncInstrumentation.Flags flags, Continuation curContinuation, Continuation newContinuation) + { + if (AsyncInstrumentation.IsEnabled.SuspendAsyncContext(flags)) + { + if (AsyncInstrumentation.IsEnabled.AsyncDebugger(flags)) + { + AsyncDebugger.SuspendAsyncContext(curContinuation, newContinuation); + } + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void CompleteRuntimeAsyncContext(ref AsyncDispatcherInfo info, AsyncInstrumentation.Flags flags) + { + if (AsyncInstrumentation.IsEnabled.CompleteAsyncContext(flags)) + { + if (AsyncInstrumentation.IsEnabled.AsyncDebugger(flags)) + { + AsyncDebugger.CompleteAsyncContext(info.CurrentTask); + } + } + } + + public static void UnwindRuntimeAsyncMethodUnhandledException(ref AsyncDispatcherInfo info, AsyncInstrumentation.Flags flags, Exception ex, Continuation curContinuation, uint _) + { + if (AsyncInstrumentation.IsEnabled.UnwindAsyncException(flags)) + { + if (AsyncInstrumentation.IsEnabled.AsyncDebugger(flags)) + { + AsyncDebugger.AsyncMethodUnhandledException(info.CurrentTask, ex, curContinuation); + } + } + } + + public static void UnwindRuntimeAsyncMethodHandledException(AsyncInstrumentation.Flags flags, Continuation curContinuation, uint unwindedFrames) + { + if (AsyncInstrumentation.IsEnabled.UnwindAsyncException(flags)) + { + if (AsyncInstrumentation.IsEnabled.AsyncDebugger(flags)) + { + AsyncDebugger.AsyncMethodHandledException(curContinuation, unwindedFrames); + } + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ResumeRuntimeAsyncMethod(ref AsyncDispatcherInfo info, AsyncInstrumentation.Flags flags, Continuation curContinuation) + { + if (AsyncInstrumentation.IsEnabled.ResumeAsyncMethod(flags)) + { + if (AsyncInstrumentation.IsEnabled.AsyncDebugger(flags)) + { + AsyncDebugger.ResumeAsyncMethod(ref info, curContinuation); + } + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void CompleteRuntimeAsyncMethod(AsyncInstrumentation.Flags flags, Continuation curContinuation) + { + if (AsyncInstrumentation.IsEnabled.CompleteAsyncMethod(flags)) + { + if (AsyncInstrumentation.IsEnabled.AsyncDebugger(flags)) + { + AsyncDebugger.CompleteAsyncMethod(curContinuation); + } + } + } + } + + internal static class AsyncDebugger + { + public static void CreateAsyncContext(Task task) + { + Task.AddToActiveTasks(task); + TplEventSource.Log.TraceOperationBegin(task.Id, "System.Runtime.CompilerServices.AsyncHelpers+RuntimeAsyncTask", 0); + } + + public static void ResumeAsyncContext(int id) + { + TplEventSource.Log.TraceSynchronousWorkBegin(id, CausalitySynchronousWork.Execution); + } + + public static void SuspendAsyncContext(ref AsyncDispatcherInfo info, Continuation curContinuation) + { + if (info.NextContinuation != null) + { + Task.TryAddRuntimeAsyncContinuationChainTimestamps(info.NextContinuation, curContinuation); + } + + TplEventSource.Log.TraceSynchronousWorkEnd(CausalitySynchronousWork.Execution); + } + + public static void SuspendAsyncContext(Continuation curContinuation, Continuation newContinuation) + { + Task.ReplaceOrAddRuntimeAsyncContinuationTimestamp(curContinuation, newContinuation); + TplEventSource.Log.TraceSynchronousWorkEnd(CausalitySynchronousWork.Execution); + } + + public static void CompleteAsyncContext(Task? task) + { + if (task != null) + { + Task.RemoveRuntimeAsyncTask(task); + TplEventSource.Log.TraceOperationEnd(task.Id, AsyncCausalityStatus.Completed); + TplEventSource.Log.TraceSynchronousWorkEnd(CausalitySynchronousWork.Execution); + } + } + + public static void AsyncMethodUnhandledException(Task? task, Exception ex, Continuation curContinuation) + { + if (task != null) + { + Task.RemoveRuntimeAsyncTask(task, curContinuation); + TplEventSource.Log.TraceOperationEnd(task.Id, ex is OperationCanceledException ? AsyncCausalityStatus.Canceled : AsyncCausalityStatus.Error); + TplEventSource.Log.TraceSynchronousWorkEnd(CausalitySynchronousWork.Execution); + } + } + + public static void AsyncMethodHandledException(Continuation curContinuation, uint unwindedFrames) + { + Task.RemoveRuntimeAsyncContinuationChainTimestamps(curContinuation, unwindedFrames); + } + + public static void ResumeAsyncMethod(ref AsyncDispatcherInfo info, Continuation curContinuation) + { + if (info.CurrentTask != null) + { + Task.UpdateRuntimeAsyncTaskTimestamp(info.CurrentTask, curContinuation); + } + } + + public static void CompleteAsyncMethod(Continuation curContinuation) + { + Task.RemoveRuntimeAsyncContinuationTimestamp(curContinuation); + } + + public static void HandleSuspended(Continuation? nextContinuation, Continuation? newContinuation) + { + if (nextContinuation != null) + { + if (newContinuation != null) + { + Task.TryAddRuntimeAsyncContinuationChainTimestamps(nextContinuation, newContinuation); + } + else + { + Task.TryAddRuntimeAsyncContinuationChainTimestamps(nextContinuation); + } + } + } + + public static void HandleSuspendedFailed(Task task, Continuation? nextContinuation) + { + if (nextContinuation != null) + { + Task.RemoveRuntimeAsyncTask(task, nextContinuation); + } + else + { + Task.RemoveRuntimeAsyncTask(task); + } + } + } } } diff --git a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems index 2db9af120d00cc..cc962e9fe1e4e0 100644 --- a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems +++ b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems @@ -841,6 +841,7 @@ + diff --git a/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncInstrumentation.cs b/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncInstrumentation.cs new file mode 100644 index 00000000000000..e04495632eb28b --- /dev/null +++ b/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncInstrumentation.cs @@ -0,0 +1,129 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Threading; +using System.Threading.Tasks; +using System.Diagnostics; +using System.Diagnostics.Tracing; + +namespace System.Runtime.CompilerServices +{ + internal static class AsyncInstrumentation + { + public static bool IsSupported => Debugger.IsSupported && EventSource.IsSupported; + + [Flags] + public enum Flags : uint + { + Disabled = 0x0, + + // Bit 1 - 24 reserved for async instrumentation points. + CreateAsyncContext = 0x1, + ResumeAsyncContext = 0x2, + SuspendAsyncContext = 0x4, + CompleteAsyncContext = 0x8, + UnwindAsyncException = 0x10, + ResumeAsyncMethod = 0x20, + CompleteAsyncMethod = 0x40, + + // Bit 25 - 31 reserved for instrumentation clients. + AsyncProfiler = 0x1000000, + AsyncDebugger = 0x2000000, + + // Bit 32 reserved for initialization state. + Uninitialized = 0x80000000 + } + + public const Flags DefaultFlags = + Flags.CreateAsyncContext | Flags.ResumeAsyncContext | Flags.SuspendAsyncContext | + Flags.CompleteAsyncContext | Flags.UnwindAsyncException | + Flags.ResumeAsyncMethod | Flags.CompleteAsyncMethod; + + public static class IsEnabled + { + public static bool CreateAsyncContext(Flags flags) => (Flags.CreateAsyncContext & flags) != 0; + public static bool ResumeAsyncContext(Flags flags) => (Flags.ResumeAsyncContext & flags) != 0; + public static bool SuspendAsyncContext(Flags flags) => (Flags.SuspendAsyncContext & flags) != 0; + public static bool CompleteAsyncContext(Flags flags) => (Flags.CompleteAsyncContext & flags) != 0; + public static bool UnwindAsyncException(Flags flags) => (Flags.UnwindAsyncException & flags) != 0; + public static bool ResumeAsyncMethod(Flags flags) => (Flags.ResumeAsyncMethod & flags) != 0; + public static bool CompleteAsyncMethod(Flags flags) => (Flags.CompleteAsyncMethod & flags) != 0; + public static bool AsyncProfiler(Flags flags) => (Flags.AsyncProfiler & flags) != 0; + public static bool AsyncDebugger(Flags flags) => (Flags.AsyncDebugger & flags) != 0 && Task.s_asyncDebuggingEnabled; + } + + public static Flags ActiveFlags => s_activeFlags; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Flags SyncActiveFlags() + { + Flags flags = s_activeFlags; + if (IsUninitialized(flags)) + { + return InitializeFlags(); + } + return flags; + } + + public static void UpdateAsyncProfilerFlags(Flags asyncProfilerFlags) + { + if (asyncProfilerFlags != Flags.Disabled) + { + asyncProfilerFlags |= Flags.AsyncProfiler; + } + + lock (s_lock) + { + s_asyncProfilerActiveFlags = asyncProfilerFlags; + if (IsInitialized(s_activeFlags)) + { + s_activeFlags = s_asyncProfilerActiveFlags | s_asyncDebuggerActiveFlags; + } + } + } + + public static void UpdateAsyncDebuggerFlags(Flags asyncDebuggerFlags) + { + if (asyncDebuggerFlags != Flags.Disabled) + { + asyncDebuggerFlags |= Flags.AsyncDebugger; + } + + lock (s_lock) + { + s_asyncDebuggerActiveFlags = asyncDebuggerFlags; + if (IsInitialized(s_activeFlags)) + { + s_activeFlags = s_asyncProfilerActiveFlags | s_asyncDebuggerActiveFlags; + } + } + } + + private static Flags InitializeFlags() + { + _ = TplEventSource.Log; // Touch TplEventSource to trigger static constructor which will initialize TPL flags if EventSource is supported. + + lock (s_lock) + { + if (IsUninitialized(s_activeFlags)) + { + s_activeFlags = s_asyncProfilerActiveFlags | s_asyncDebuggerActiveFlags; + } + + return s_activeFlags; + } + } + + private static bool IsInitialized(Flags flags) => !IsUninitialized(flags); + + private static bool IsUninitialized(Flags flags) => (flags & Flags.Uninitialized) != 0; + + private static Flags s_activeFlags = Flags.Uninitialized; + + private static Flags s_asyncProfilerActiveFlags; + + private static Flags s_asyncDebuggerActiveFlags; + + private static readonly Lock s_lock = new(); + } +} 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 520c0642c4e1fd..1a559e653c7049 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 @@ -218,37 +218,70 @@ internal static void RemoveFromActiveTasks(Task task) } #if !MONO - internal static void SetRuntimeAsyncContinuationTimestamp(Continuation continuation, long timestamp) + private static Dictionary GetOrCreateRuntimeAsyncContinuationTimestamps() { - Dictionary continuationTimestamps = - Volatile.Read(ref s_runtimeAsyncContinuationTimestamps) ?? + return Volatile.Read(ref s_runtimeAsyncContinuationTimestamps) ?? Interlocked.CompareExchange(ref s_runtimeAsyncContinuationTimestamps, new Dictionary(ReferenceEqualityComparer.Instance), null) ?? - s_runtimeAsyncContinuationTimestamps; + s_runtimeAsyncContinuationTimestamps!; + } + private static Dictionary GetOrCreateRuntimeAsyncTaskTimestamps() + { + return Volatile.Read(ref s_runtimeAsyncTaskTimestamps) ?? + Interlocked.CompareExchange(ref s_runtimeAsyncTaskTimestamps, new Dictionary(), null) ?? + s_runtimeAsyncTaskTimestamps!; + } + + internal static void ReplaceOrAddRuntimeAsyncContinuationTimestamp(Continuation curContinuation, Continuation newContinuation) + { + var continuationTimestamps = GetOrCreateRuntimeAsyncContinuationTimestamps(); lock (continuationTimestamps) { - continuationTimestamps.TryAdd(continuation, timestamp); + if (continuationTimestamps.TryGetValue(curContinuation, out long timestampVal)) + { + continuationTimestamps.Remove(curContinuation); + continuationTimestamps[newContinuation] = timestampVal; + } + else + { + continuationTimestamps.TryAdd(newContinuation, Stopwatch.GetTimestamp()); + } } } - internal static bool GetRuntimeAsyncContinuationTimestamp(Continuation continuation, out long timestamp) + internal static void TryAddRuntimeAsyncContinuationChainTimestamps(Continuation continuationChain) { - Dictionary? continuationTimestamps = s_runtimeAsyncContinuationTimestamps; - if (continuationTimestamps is null) + var continuationTimestamps = GetOrCreateRuntimeAsyncContinuationTimestamps(); + long timestamp = Stopwatch.GetTimestamp(); + lock (continuationTimestamps) { - timestamp = 0; - return false; + Continuation? nc = continuationChain; + while (nc != null) + { + continuationTimestamps.TryAdd(nc, timestamp); + nc = nc.Next; + } } + } + internal static void TryAddRuntimeAsyncContinuationChainTimestamps(Continuation continuationChain, Continuation timestampSource) + { + var continuationTimestamps = GetOrCreateRuntimeAsyncContinuationTimestamps(); lock (continuationTimestamps) { - return continuationTimestamps.TryGetValue(continuation, out timestamp); + long timestamp = continuationTimestamps.TryGetValue(timestampSource, out long timestampVal) ? timestampVal : Stopwatch.GetTimestamp(); + Continuation? nc = continuationChain; + while (nc != null) + { + continuationTimestamps.TryAdd(nc, timestamp); + nc = nc.Next; + } } } internal static void RemoveRuntimeAsyncContinuationTimestamp(Continuation continuation) { - Dictionary? continuationTimestamps = s_runtimeAsyncContinuationTimestamps; + var continuationTimestamps = s_runtimeAsyncContinuationTimestamps; if (continuationTimestamps is null) return; @@ -258,21 +291,51 @@ internal static void RemoveRuntimeAsyncContinuationTimestamp(Continuation contin } } - internal static void UpdateRuntimeAsyncTaskTimestamp(Task task, long inflightTimestamp) + internal static void RemoveRuntimeAsyncContinuationChainTimestamps(Continuation continuation, uint count) { - Dictionary runtimeAsyncTaskTimestamps = - Volatile.Read(ref s_runtimeAsyncTaskTimestamps) ?? - Interlocked.CompareExchange(ref s_runtimeAsyncTaskTimestamps, new Dictionary(), null) ?? - s_runtimeAsyncTaskTimestamps; + var continuationTimestamps = s_runtimeAsyncContinuationTimestamps; + if (continuationTimestamps is null) + return; + lock (continuationTimestamps) + { + Continuation? nc = continuation; + for (uint i = 0; i < count && nc != null; i++) + { + continuationTimestamps.Remove(nc); + nc = nc.Next; + } + } + } + + internal static void UpdateRuntimeAsyncTaskTimestamp(Task task, Continuation timestampSource) + { + long timestamp = 0; + var continuationTimestamps = s_runtimeAsyncContinuationTimestamps; + if (continuationTimestamps != null) + { + lock (continuationTimestamps) + { + continuationTimestamps.TryGetValue(timestampSource, out timestamp); + } + } + + if (timestamp == 0) + { + timestamp = Stopwatch.GetTimestamp(); + } + + var runtimeAsyncTaskTimestamps = GetOrCreateRuntimeAsyncTaskTimestamps(); lock (runtimeAsyncTaskTimestamps) { - runtimeAsyncTaskTimestamps[task.Id] = inflightTimestamp; + runtimeAsyncTaskTimestamps[task.Id] = timestamp; } } - internal static void RemoveRuntimeAsyncTaskTimestamp(Task task) + internal static void RemoveRuntimeAsyncTask(Task task) { + RemoveFromActiveTasks(task); + Dictionary? runtimeAsyncTaskTimestamps = s_runtimeAsyncTaskTimestamps; if (runtimeAsyncTaskTimestamps is null) return; @@ -282,6 +345,25 @@ internal static void RemoveRuntimeAsyncTaskTimestamp(Task task) runtimeAsyncTaskTimestamps.Remove(task.Id); } } + + internal static void RemoveRuntimeAsyncTask(Task task, Continuation continuationChain) + { + RemoveRuntimeAsyncTask(task); + + Dictionary? continuationTimestamps = s_runtimeAsyncContinuationTimestamps; + if (continuationTimestamps is null) + return; + + lock (continuationTimestamps) + { + Continuation? nc = continuationChain; + while (nc != null) + { + continuationTimestamps.Remove(nc); + nc = nc.Next; + } + } + } #endif // We moved a number of Task properties into this class. The idea is that in most cases, these properties never diff --git a/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/TplEventSource.cs b/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/TplEventSource.cs index 5418d665ab43b1..89e537c74e92eb 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/TplEventSource.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/TplEventSource.cs @@ -34,6 +34,26 @@ protected override void OnEventCommand(EventCommandEventArgs command) Debug = IsEnabled(EventLevel.Informational, Keywords.Debug); DebugActivityId = IsEnabled(EventLevel.Informational, Keywords.DebugActivityId); + + // Until debugger explicitly set the AsyncInstrumentation keyword, we enable async instrumentation when + // Tasks, AsyncCausalitySynchronousWork, AsyncCausalityOperation and TasksFlowActivityIds keywords are enabled. + bool asyncInstrumentationEnabled = IsEnabled(EventLevel.Informational, Keywords.AsyncInstrumentation); + if (!asyncInstrumentationEnabled) + { + asyncInstrumentationEnabled = IsEnabled(EventLevel.Informational, Keywords.Tasks); + asyncInstrumentationEnabled &= IsEnabled(EventLevel.Informational, Keywords.AsyncCausalityOperation); + asyncInstrumentationEnabled &= IsEnabled(EventLevel.Informational, Keywords.AsyncCausalitySynchronousWork); + asyncInstrumentationEnabled &= IsEnabled(EventLevel.Informational, Keywords.TasksFlowActivityIds); + } + + if (asyncInstrumentationEnabled) + { + AsyncInstrumentation.UpdateAsyncDebuggerFlags(AsyncInstrumentation.DefaultFlags); + } + else + { + AsyncInstrumentation.UpdateAsyncDebuggerFlags(AsyncInstrumentation.Flags.Disabled); + } } /// @@ -128,6 +148,11 @@ public enum TaskWaitBehavior : int /// Relatively Verbose logging meant for debugging the Task library itself. Will probably be removed in the future /// public const EventKeywords DebugActivityId = (EventKeywords)0x40000; + /// + /// Enable async instrumentation to track async operations across await/async method boundaries. + /// Mainly used by debugger to track task and continuation chain execution. + /// + public const EventKeywords AsyncInstrumentation = (EventKeywords)0x80000; } //----------------------------------------------------------------------------------- diff --git a/src/libraries/System.Runtime/tests/System.Threading.Tasks.Tests/System.Runtime.CompilerServices/RuntimeAsyncTests.cs b/src/libraries/System.Runtime/tests/System.Threading.Tasks.Tests/System.Runtime.CompilerServices/RuntimeAsyncTests.cs index 8557a26bc660e0..7e28cbbfd1e1d9 100644 --- a/src/libraries/System.Runtime/tests/System.Threading.Tasks.Tests/System.Runtime.CompilerServices/RuntimeAsyncTests.cs +++ b/src/libraries/System.Runtime/tests/System.Threading.Tasks.Tests/System.Runtime.CompilerServices/RuntimeAsyncTests.cs @@ -1,7 +1,9 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Collections.Concurrent; using System.Collections.Generic; +using System.Diagnostics.Tracing; using System.Reflection; using Microsoft.DotNet.RemoteExecutor; using Microsoft.DotNet.XUnitExtensions; @@ -13,34 +15,561 @@ public class RuntimeAsyncTests { private static bool IsRemoteExecutorAndRuntimeAsyncSupported => RemoteExecutor.IsSupported && PlatformDetection.IsRuntimeAsyncSupported; + private static readonly FieldInfo s_asyncDebuggingEnabledField = GetCorLibClassStaticField("System.Threading.Tasks.Task", "s_asyncDebuggingEnabled"); + private static readonly FieldInfo s_taskTimestampsField = GetCorLibClassStaticField("System.Threading.Tasks.Task", "s_runtimeAsyncTaskTimestamps"); + private static readonly FieldInfo s_continuationTimestampsField = GetCorLibClassStaticField("System.Threading.Tasks.Task", "s_runtimeAsyncContinuationTimestamps"); + private static readonly FieldInfo s_activeTasksField = GetCorLibClassStaticField("System.Threading.Tasks.Task", "s_currentActiveTasks"); + private static readonly FieldInfo s_activeFlagsField = GetCorLibClassStaticField("System.Runtime.CompilerServices.AsyncInstrumentation", "s_activeFlags"); + + private static readonly object s_debuggerLock = new object(); + private static TestEventListener? s_debuggerTplInstance; + + // AsyncDebugger(0x2000000) | all event flags(0x7F) + private const uint EnabledInstrumentationFlags = 0x200007F; + private const uint DisabledInstrumentationFlags = 0x0; + private const uint UninitializedInstrumentationFlags = 0x80000000; + + private static void AttachDebugger() + { + // Simulate a debugger attach to process, creating TPL event source session + setting s_asyncDebuggingEnabled. + lock (s_debuggerLock) + { + uint flags = Convert.ToUInt32(s_activeFlagsField.GetValue(null)); + Assert.True(flags == UninitializedInstrumentationFlags || flags == DisabledInstrumentationFlags, $"ActiveFlags equals {flags}, expected {UninitializedInstrumentationFlags} || {DisabledInstrumentationFlags}"); + + s_debuggerTplInstance = new TestEventListener("System.Threading.Tasks.TplEventSource", EventLevel.Verbose); + s_asyncDebuggingEnabledField.SetValue(null, true); + + // Initialize flags and collections. + Func().GetAwaiter().GetResult(); + + flags = Convert.ToUInt32(s_activeFlagsField.GetValue(null)); + Assert.True(flags == EnabledInstrumentationFlags, $"ActiveFlags equals {flags}, expected {EnabledInstrumentationFlags}"); + + var activeTasks = (Dictionary)s_activeTasksField.GetValue(null); + Assert.True(activeTasks != null, "Expected active tasks dictionary to be initialized"); + + var taskTimestamps = (Dictionary)s_taskTimestampsField.GetValue(null); + Assert.True(taskTimestamps != null, "Expected tasks timestamps dictionary to be initialized"); + + var continuationTimestamps = (Dictionary)s_continuationTimestampsField.GetValue(null); + Assert.True(continuationTimestamps != null, "Expected continuation timestamps dictionary to be initialized"); + } + } + + private static void DetachDebugger() + { + // Simulate a debugger detach from process. + lock (s_debuggerLock) + { + s_asyncDebuggingEnabledField.SetValue(null, false); + s_debuggerTplInstance?.Dispose(); + s_debuggerTplInstance = null; + + uint flags = Convert.ToUInt32(s_activeFlagsField.GetValue(null)); + Assert.True(flags == DisabledInstrumentationFlags, $"ActiveFlags equals {flags}, expected {DisabledInstrumentationFlags}"); + } + } + + private static FieldInfo GetCorLibClassStaticField(string className, string fieldName) + { + Type? classType = typeof(object).Assembly.GetType(className); + if (classType == null) + { + throw new InvalidOperationException($"Type '{className}' doesn't exist in System.Private.CoreLib."); + } + + FieldInfo? field = classType.GetField(fieldName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static); + if (field == null) + { + throw new InvalidOperationException($"Expected static field '{fieldName}' to exist on type '{className}'."); + } + + return field; + } + + private static int GetTaskTimestampCount() => + s_taskTimestampsField.GetValue(null) is Dictionary dict ? dict.Count : 0; + + private static int GetContinuationTimestampCount() => + s_continuationTimestampsField.GetValue(null) is Dictionary dict ? dict.Count : 0; + [System.Runtime.CompilerServices.RuntimeAsyncMethodGeneration(true)] static async Task Func() { - await Task.Delay(1); await Task.Yield(); } + [System.Runtime.CompilerServices.RuntimeAsyncMethodGeneration(true)] + static async Task FuncThatThrows() + { + await DeepThrow1(); + } + + [System.Runtime.CompilerServices.RuntimeAsyncMethodGeneration(true)] + static async Task DeepThrow1() + { + await DeepThrow2(); + } + + [System.Runtime.CompilerServices.RuntimeAsyncMethodGeneration(true)] + static async Task DeepThrow2() + { + await Task.Yield(); + throw new InvalidOperationException("test exception"); + } + + [System.Runtime.CompilerServices.RuntimeAsyncMethodGeneration(true)] + static async Task FuncWithResult() + { + await Task.Yield(); + return 42; + } + + [System.Runtime.CompilerServices.RuntimeAsyncMethodGeneration(true)] + static async Task OuterFuncThatCatches() + { + try + { + await MiddleFuncThatPropagates(); + } + catch (InvalidOperationException) + { + } + await Task.Yield(); + } + + [System.Runtime.CompilerServices.RuntimeAsyncMethodGeneration(true)] + static async Task MiddleFuncThatPropagates() + { + await InnerFuncThatThrows(); + } + + [System.Runtime.CompilerServices.RuntimeAsyncMethodGeneration(true)] + static async Task InnerFuncThatThrows() + { + await Task.Yield(); + throw new InvalidOperationException("inner exception"); + } + + [System.Runtime.CompilerServices.RuntimeAsyncMethodGeneration(true)] + static async Task FuncThatCancels(CancellationToken ct) + { + await Task.Yield(); + ct.ThrowIfCancellationRequested(); + } + + [System.Runtime.CompilerServices.RuntimeAsyncMethodGeneration(true)] + static async Task FuncThatWaitsTwice(TaskCompletionSource tcs1, TaskCompletionSource tcs2) + { + await tcs1.Task; + await tcs2.Task; + } + + [System.Runtime.CompilerServices.RuntimeAsyncMethodGeneration(true)] + static async Task FuncThatInspectsContinuationTimestamps(TaskCompletionSource tcs, Action callback) + { + await tcs.Task; + callback(); + } + + static void ValidateTimestampsCleared() + { + // some other tasks may be created by the runtime, so this is just using a reasonably small upper bound + Assert.InRange(GetTaskTimestampCount(), 0, 10); + Assert.InRange(GetContinuationTimestampCount(), 0, 10); + } + [ConditionalFact(typeof(RuntimeAsyncTests), nameof(IsRemoteExecutorAndRuntimeAsyncSupported))] [ActiveIssue("https://github.com/dotnet/runtime/issues/124072", typeof(PlatformDetection), nameof(PlatformDetection.IsInterpreter))] public void RuntimeAsync_TaskCompleted() { RemoteExecutor.Invoke(async () => { + AttachDebugger(); + + for (int i = 0; i < 1000; i++) + { + await Func(); + } + + ValidateTimestampsCleared(); - // NOTE: This depends on private implementation details generally only used by the debugger. - // If those ever change, this test will need to be updated as well. + DetachDebugger(); + + }).Dispose(); + } - typeof(Task).GetField("s_asyncDebuggingEnabled", BindingFlags.NonPublic | BindingFlags.Static).SetValue(null, true); + [ConditionalFact(typeof(RuntimeAsyncTests), nameof(IsRemoteExecutorAndRuntimeAsyncSupported))] + [ActiveIssue("https://github.com/dotnet/runtime/issues/124072", typeof(PlatformDetection), nameof(PlatformDetection.IsInterpreter))] + public void RuntimeAsync_ExceptionCleanup() + { + RemoteExecutor.Invoke(async () => + { + AttachDebugger(); for (int i = 0; i < 1000; i++) { - await Func(); + try + { + await FuncThatThrows(); + } + catch (InvalidOperationException) + { + } + } + + ValidateTimestampsCleared(); + + DetachDebugger(); + + }).Dispose(); + } + + [ConditionalFact(typeof(RuntimeAsyncTests), nameof(IsRemoteExecutorAndRuntimeAsyncSupported))] + [ActiveIssue("https://github.com/dotnet/runtime/issues/124072", typeof(PlatformDetection), nameof(PlatformDetection.IsInterpreter))] + public void RuntimeAsync_DebuggerDetach() + { + RemoteExecutor.Invoke(async () => + { + AttachDebugger(); + + var activeTasks = (Dictionary)s_activeTasksField.GetValue(null); + + // Use an in-flight task to deterministically verify tracking is active + var tcs = new TaskCompletionSource(); + Task inflight = FuncThatWaitsTwice(tcs, new TaskCompletionSource()); + + lock (activeTasks) + { + Assert.True(activeTasks.ContainsKey(inflight.Id), + "Expected in-flight task to be tracked while debugger is attached"); + } + + // Complete the first await so it resumes and sets a task timestamp + tcs.SetResult(); + var taskTimestamps = (Dictionary)s_taskTimestampsField.GetValue(null); + + bool seenTimestamp = SpinWait.SpinUntil(() => + { + lock (taskTimestamps) + { + return taskTimestamps.ContainsKey(inflight.Id); + } + }, timeout: TimeSpan.FromSeconds(5)); + + Assert.True(seenTimestamp, "Timed out waiting for task timestamp"); + + // Simulate debugger detach — the flag sync should detect the mismatch + // and disable the Debugger flags without crashing. + // Timestamps are not cleared (matches existing behavior). + DetachDebugger(); + + // Run one task to trigger the flag sync that detects the mismatch + await Func(); + + // Now start a new in-flight task after detach - it should NOT be tracked + var tcsPost = new TaskCompletionSource(); + Task postDetach = FuncThatWaitsTwice(tcsPost, new TaskCompletionSource()); + + lock (activeTasks) + { + Assert.False(activeTasks.ContainsKey(postDetach.Id), + "Expected in-flight task NOT to be tracked after debugger detach"); + } + + lock (taskTimestamps) + { + Assert.False(taskTimestamps.ContainsKey(postDetach.Id), + "Expected no task timestamp for post-detach in-flight task"); + } + + }).Dispose(); + } + + [ConditionalFact(typeof(RuntimeAsyncTests), nameof(IsRemoteExecutorAndRuntimeAsyncSupported))] + [ActiveIssue("https://github.com/dotnet/runtime/issues/124072", typeof(PlatformDetection), nameof(PlatformDetection.IsInterpreter))] + public void RuntimeAsync_ValueTypeResult() + { + RemoteExecutor.Invoke(async () => + { + AttachDebugger(); + + for (int i = 0; i < 1000; i++) + { + int result = await FuncWithResult(); + Assert.Equal(42, result); } - int taskCount = ((Dictionary)typeof(Task).GetField("s_runtimeAsyncTaskTimestamps", BindingFlags.NonPublic | BindingFlags.Static).GetValue(null)).Count; - int continuationCount = ((Dictionary)typeof(Task).GetField("s_runtimeAsyncContinuationTimestamps", BindingFlags.NonPublic | BindingFlags.Static).GetValue(null)).Count; - Assert.InRange(taskCount, 0, 10); // some other tasks may be created by the runtime, so this is just using a reasonably small upper bound - Assert.InRange(continuationCount, 0, 10); + ValidateTimestampsCleared(); + + DetachDebugger(); + + }).Dispose(); + } + + [ConditionalFact(typeof(RuntimeAsyncTests), nameof(IsRemoteExecutorAndRuntimeAsyncSupported))] + [ActiveIssue("https://github.com/dotnet/runtime/issues/124072", typeof(PlatformDetection), nameof(PlatformDetection.IsInterpreter))] + public void RuntimeAsync_HandledExceptionPartialUnwind() + { + RemoteExecutor.Invoke(async () => + { + AttachDebugger(); + + for (int i = 0; i < 1000; i++) + { + await OuterFuncThatCatches(); + } + + ValidateTimestampsCleared(); + + DetachDebugger(); + + }).Dispose(); + } + + [ConditionalFact(typeof(RuntimeAsyncTests), nameof(IsRemoteExecutorAndRuntimeAsyncSupported))] + [ActiveIssue("https://github.com/dotnet/runtime/issues/124072", typeof(PlatformDetection), nameof(PlatformDetection.IsInterpreter))] + public void RuntimeAsync_CancellationCleanup() + { + RemoteExecutor.Invoke(async () => + { + AttachDebugger(); + + using var cts = new CancellationTokenSource(); + cts.Cancel(); + + for (int i = 0; i < 1000; i++) + { + try + { + await FuncThatCancels(cts.Token); + } + catch (OperationCanceledException) + { + } + } + + ValidateTimestampsCleared(); + + DetachDebugger(); + + }).Dispose(); + } + + [ConditionalFact(typeof(RuntimeAsyncTests), nameof(IsRemoteExecutorAndRuntimeAsyncSupported))] + [ActiveIssue("https://github.com/dotnet/runtime/issues/124072", typeof(PlatformDetection), nameof(PlatformDetection.IsInterpreter))] + public void RuntimeAsync_TimestampsTrackedWhileInFlight() + { + RemoteExecutor.Invoke(async () => + { + AttachDebugger(); + + var tcs1 = new TaskCompletionSource(); + var tcs2 = new TaskCompletionSource(); + Task inflight = FuncThatWaitsTwice(tcs1, tcs2); + + // Task is suspended on tcs1 — should be in active tasks + var activeTasks = (Dictionary)s_activeTasksField.GetValue(null); + + lock (activeTasks) + { + Assert.True(activeTasks.ContainsKey(inflight.Id), "Expected suspended task to be in s_currentActiveTasks"); + } + + // Resume from first suspension — this triggers DispatchContinuations which sets timestamps + tcs1.SetResult(); + + // Poll until the dispatch loop has resumed and re-suspended on tcs2, + // which sets the task timestamp via ResumeRuntimeAsyncMethod. + var taskTimestamps = (Dictionary)s_taskTimestampsField.GetValue(null); + + bool seenTimestamp = SpinWait.SpinUntil(() => + { + lock (taskTimestamps) + { + return taskTimestamps.ContainsKey(inflight.Id); + } + }, timeout: TimeSpan.FromSeconds(5)); + + Assert.True(seenTimestamp, "Timed out waiting for task timestamp to appear after resume"); + + // Now the task has been through one resume cycle and is suspended again on tcs2. + lock (taskTimestamps) + { + Assert.True(taskTimestamps[inflight.Id] > 0, "Expected non-zero task timestamp"); + } + + // Complete the task + tcs2.SetResult(); + await inflight; + + // After completion the task should be removed from all collections + lock (activeTasks) + { + Assert.False(activeTasks.ContainsKey(inflight.Id), "Expected completed task to be removed from s_currentActiveTasks"); + } + + lock (taskTimestamps) + { + Assert.False(taskTimestamps.ContainsKey(inflight.Id), "Expected task timestamp to be removed after completion"); + } + + DetachDebugger(); + + }).Dispose(); + } + + [ConditionalFact(typeof(RuntimeAsyncTests), nameof(IsRemoteExecutorAndRuntimeAsyncSupported))] + [ActiveIssue("https://github.com/dotnet/runtime/issues/124072", typeof(PlatformDetection), nameof(PlatformDetection.IsInterpreter))] + public void RuntimeAsync_ContinuationTimestampObservedDuringResume() + { + RemoteExecutor.Invoke(async () => + { + AttachDebugger(); + + bool continuationTimestampObserved = false; + + var tcs = new TaskCompletionSource(); + Task inflight = FuncThatInspectsContinuationTimestamps(tcs, () => + { + // This callback runs inside the resumed async method body, after + // ResumeRuntimeAsyncMethod but before SuspendRuntimeAsyncContext/CompleteRuntimeAsyncMethod. + // The continuation timestamp for the current continuation should still be in the dictionary. + var continuationTimestamps = (Dictionary)s_continuationTimestampsField.GetValue(null); + lock (continuationTimestamps) + { + continuationTimestampObserved = continuationTimestamps.Count > 0; + } + }); + + tcs.SetResult(); + await inflight; + + Assert.True(continuationTimestampObserved, "Expected continuation timestamp to be present during resumed method execution"); + + DetachDebugger(); + + }).Dispose(); + } + + [ConditionalFact(typeof(RuntimeAsyncTests), nameof(IsRemoteExecutorAndRuntimeAsyncSupported))] + [ActiveIssue("https://github.com/dotnet/runtime/issues/124072", typeof(PlatformDetection), nameof(PlatformDetection.IsInterpreter))] + public void RuntimeAsync_InFlightInstrumentationUpgrade() + { + RemoteExecutor.Invoke(async () => + { + // Start a multi-await task WITHOUT instrumentation enabled. + var tcs1 = new TaskCompletionSource(); + var tcs2 = new TaskCompletionSource(); + Task inflight = FuncThatWaitsTwice(tcs1, tcs2); + + // Task is now suspended at first await with no instrumentation. + // Attach the debugger mid-flight — this enables instrumentation. + AttachDebugger(); + + var activeTasks = (Dictionary)s_activeTasksField.GetValue(null); + var taskTimestamps = (Dictionary)s_taskTimestampsField.GetValue(null); + + // The in-flight task was NOT tracked at creation (no instrumentation then). + lock (activeTasks) + { + Assert.False(activeTasks.ContainsKey(inflight.Id), + "Expected in-flight task NOT to be tracked (started before instrumentation)"); + } + + // Resume the first await — the dispatch loop's InstrumentCheckPoint should + // detect that instrumentation is now active and transition to the instrumented path. + tcs1.SetResult(); + + // Wait for the task to suspend at the second await. + bool seenTimestamp = SpinWait.SpinUntil(() => + { + lock (taskTimestamps) + { + return taskTimestamps.ContainsKey(inflight.Id); + } + }, timeout: TimeSpan.FromSeconds(5)); + + Assert.True(seenTimestamp, + "Expected task timestamp after mid-flight instrumentation upgrade (InstrumentCheckPoint transition)"); + + // Complete the second await and let the task finish. + tcs2.SetResult(); + await inflight; + + DetachDebugger(); + + }).Dispose(); + } + + [ConditionalFact(typeof(RuntimeAsyncTests), nameof(IsRemoteExecutorAndRuntimeAsyncSupported))] + [ActiveIssue("https://github.com/dotnet/runtime/issues/124072", typeof(PlatformDetection), nameof(PlatformDetection.IsInterpreter))] + public void RuntimeAsync_TplEvents() + { + RemoteExecutor.Invoke(() => + { + const int TraceOperationBeginId = 14; + const int TraceOperationEndId = 15; + const int TraceSynchronousWorkBeginId = 17; + const int TraceSynchronousWorkEndId = 18; + + AttachDebugger(); + + var events = new ConcurrentQueue(); + using (var listener = new TestEventListener("System.Threading.Tasks.TplEventSource", EventLevel.Verbose)) + { + listener.RunWithCallback(events.Enqueue, () => + { + for (int i = 0; i < 10; i++) + { + Func().GetAwaiter().GetResult(); + } + }); + } + + Assert.Contains(events, e => e.EventId == TraceOperationBeginId); + Assert.Contains(events, e => e.EventId == TraceOperationEndId); + Assert.Contains(events, e => e.EventId == TraceSynchronousWorkBeginId); + Assert.Contains(events, e => e.EventId == TraceSynchronousWorkEndId); + + DetachDebugger(); + + }).Dispose(); + } + + [ConditionalFact(typeof(RuntimeAsyncTests), nameof(IsRemoteExecutorAndRuntimeAsyncSupported))] + [ActiveIssue("https://github.com/dotnet/runtime/issues/124072", typeof(PlatformDetection), nameof(PlatformDetection.IsInterpreter))] + public void RuntimeAsync_NoTplEventsWithoutDebugger() + { + RemoteExecutor.Invoke(() => + { + const int TraceOperationBeginId = 14; + const string RuntimeAsyncTaskOperationName = "System.Runtime.CompilerServices.AsyncHelpers+RuntimeAsyncTask"; + + // Enable TplEventSource WITHOUT setting s_asyncDebuggingEnabled. + // The AsyncDebugger guard should prevent the V2 async instrumentation + // from emitting any TPL causality events. + var events = new ConcurrentQueue(); + using (var listener = new TestEventListener("System.Threading.Tasks.TplEventSource", EventLevel.Verbose)) + { + listener.RunWithCallback(events.Enqueue, () => + { + for (int i = 0; i < 10; i++) + { + Func().GetAwaiter().GetResult(); + } + }); + } + + // TraceOperationBegin with the RuntimeAsyncTask operation name is uniquely + // emitted by V2 async instrumentation. It must not appear without a debugger. + Assert.DoesNotContain(events, e => + e.EventId == TraceOperationBeginId && + e.Payload?.Count > 1 && + e.Payload[1] is string name && + name == RuntimeAsyncTaskOperationName); + }).Dispose(); } }