// Copyright (c) Microsoft. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. namespace Microsoft.VisualStudio.Threading { using System; using System.Collections; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Threading.Tasks; /// /// Internal helper/extension methods for this assembly's own use. /// internal static class InternalUtilities { /// /// The substring that should be inserted before each async return stack frame. /// /// /// When printing synchronous callstacks, .NET begins each frame with " at ". /// When printing async return stack, we use this to indicate continuations. /// private const string AsyncReturnStackPrefix = " -> "; /// /// Removes an element from the middle of a queue without disrupting the other elements. /// /// The element to remove. /// The queue to modify. /// The value to remove. /// /// If a value appears multiple times in the queue, only its first entry is removed. /// internal static bool RemoveMidQueue(this Queue queue, T valueToRemove) where T : class { Requires.NotNull(queue, nameof(queue)); Requires.NotNull(valueToRemove, nameof(valueToRemove)); int originalCount = queue.Count; int dequeueCounter = 0; bool found = false; while (dequeueCounter < originalCount) { dequeueCounter++; T dequeued = queue.Dequeue(); if (!found && dequeued == valueToRemove) { // only find 1 match found = true; } else { queue.Enqueue(dequeued); } } return found; } /// /// Walk the continuation objects inside "async state machines" to generate the return callstack. /// FOR DIAGNOSTIC PURPOSES ONLY. /// /// The delegate that represents the head of an async continuation chain. internal static IEnumerable GetAsyncReturnStackFrames(this Delegate continuationDelegate) { IAsyncStateMachine? stateMachine = FindAsyncStateMachine(continuationDelegate); if (stateMachine is null) { // Did not find the async state machine, so returns the method name as top frame and stop walking. yield return GetDelegateLabel(continuationDelegate); yield break; } do { var state = GetStateMachineFieldValueOnSuffix(stateMachine, "__state"); yield return string.Format( CultureInfo.CurrentCulture, "{2}{0} (state: {1}, address: 0x{3:X8})", stateMachine.GetType().FullName, state, AsyncReturnStackPrefix, (int)GetAddress(stateMachine)); // the int cast allows hex formatting Delegate[]? continuationDelegates = FindContinuationDelegates(stateMachine).ToArray(); if (continuationDelegates.Length == 0) { break; } // Consider: It's possible but uncommon scenario to have multiple "async methods" being awaiting for one "async method". // Here we just choose the first awaiting "async method" as that should be good enough for postmortem. // In future we might want to revisit this to cover the other awaiting "async methods". stateMachine = continuationDelegates.Select((d) => FindAsyncStateMachine(d)) .FirstOrDefault((s) => s is object); if (stateMachine is null) { yield return GetDelegateLabel(continuationDelegates.First()); } } while (stateMachine is object); } /// /// A helper method to get the label of the given delegate. /// private static string GetDelegateLabel(Delegate invokeDelegate) { Requires.NotNull(invokeDelegate, nameof(invokeDelegate)); MethodInfo? method = invokeDelegate.GetMethodInfo(); if (invokeDelegate.Target is object) { string instanceType = string.Empty; if (!(method?.DeclaringType?.Equals(invokeDelegate.Target.GetType()) ?? false)) { instanceType = " (" + invokeDelegate.Target.GetType().FullName + ")"; } return string.Format( CultureInfo.CurrentCulture, "{3}{0}.{1}{2} (target address: 0x{4:X" + (IntPtr.Size * 2) + "})", method?.DeclaringType?.FullName, method?.Name, instanceType, AsyncReturnStackPrefix, GetAddress(invokeDelegate.Target).ToInt64()); // the cast allows hex formatting } return string.Format( CultureInfo.CurrentCulture, "{2}{0}.{1}", method?.DeclaringType?.FullName, method?.Name, AsyncReturnStackPrefix); } /// /// Gets the memory address of a given object. /// /// The object to get the address for. /// The memory address. /// /// This method works when GCHandle will refuse because the type of object is a non-blittable type. /// However, this method provides no guarantees that the address will remain valid for the caller, /// so it is only useful for diagnostics and when we don't expect addresses to be changing much any more. /// private static unsafe IntPtr GetAddress(object value) => new IntPtr(Unsafe.AsPointer(ref value)); /// /// A helper method to find the async state machine from the given delegate. /// private static IAsyncStateMachine? FindAsyncStateMachine(Delegate invokeDelegate) { Requires.NotNull(invokeDelegate, nameof(invokeDelegate)); if (invokeDelegate.Target is object) { // Some delegates are wrapped with a ContinuationWrapper object. We have to unwrap that in those cases. // In testing, this m_continuation field jump is only required when the debugger is attached -- weird. // I suspect however that it's a natural behavior of the async state machine (when there are >1 continuations perhaps). // So we check for the case in all cases. if (GetFieldValue(invokeDelegate.Target, "m_continuation") is Action continuation) { invokeDelegate = continuation; if (invokeDelegate.Target is null) { return null; } } var stateMachine = GetFieldValue(invokeDelegate.Target, "m_stateMachine") as IAsyncStateMachine; return stateMachine; } return null; } /// /// This is the core to find the continuation delegate(s) inside the given async state machine. /// The chain of objects is like this: async state machine -> async method builder -> task -> continuation object -> action. /// /// /// There are 3 types of "async method builder": AsyncVoidMethodBuilder, AsyncTaskMethodBuilder, AsyncTaskMethodBuilder<T>. /// We don't cover AsyncVoidMethodBuilder as it is used rarely and it can't be awaited either; /// AsyncTaskMethodBuilder is a wrapper on top of AsyncTaskMethodBuilder<VoidTaskResult>. /// private static IEnumerable FindContinuationDelegates(IAsyncStateMachine stateMachine) { Requires.NotNull(stateMachine, nameof(stateMachine)); var builder = GetStateMachineFieldValueOnSuffix(stateMachine, "__builder"); if (builder is null) { yield break; } var task = GetFieldValue(builder, "m_task"); if (task is null) { // Probably this builder is an instance of "AsyncTaskMethodBuilder", so we need to get its inner "AsyncTaskMethodBuilder" builder = GetFieldValue(builder, "m_builder"); if (builder is object) { task = GetFieldValue(builder, "m_task"); } } if (task is null) { yield break; } // "task" might be an instance of the type deriving from "Task", but "m_continuationObject" is a private field in "Task", // so we need to use "typeof(Task)" to access "m_continuationObject". FieldInfo? continuationField = typeof(Task).GetTypeInfo().GetDeclaredField("m_continuationObject"); if (continuationField is null) { yield break; } var continuationObject = continuationField.GetValue(task); if (continuationObject is null) { yield break; } if (continuationObject is IEnumerable items) { foreach (var item in items) { Delegate? action = item as Delegate ?? GetFieldValue(item!, "m_action") as Delegate; if (action is object) { yield return action; } } } else { Delegate? action = continuationObject as Delegate ?? GetFieldValue(continuationObject, "m_action") as Delegate; if (action is object) { yield return action; } } } /// /// A helper method to get field's value given the object and the field name. /// private static object? GetFieldValue(object obj, string fieldName) { Requires.NotNull(obj, nameof(obj)); Requires.NotNullOrEmpty(fieldName, nameof(fieldName)); FieldInfo? field = obj.GetType().GetTypeInfo().GetDeclaredField(fieldName); if (field is object) { return field.GetValue(obj); } return null; } /// /// The field names of "async state machine" are not fixed; the workaround is to find the field based on the suffix. /// private static object? GetStateMachineFieldValueOnSuffix(IAsyncStateMachine stateMachine, string suffix) { Requires.NotNull(stateMachine, nameof(stateMachine)); Requires.NotNullOrEmpty(suffix, nameof(suffix)); IEnumerable? fields = stateMachine.GetType().GetTypeInfo().DeclaredFields; FieldInfo? field = fields.FirstOrDefault((f) => f.Name.EndsWith(suffix, StringComparison.Ordinal)); if (field is object) { return field.GetValue(stateMachine); } return null; } } }