Augment GetStateMachineData to recur (dotnet/corefx#38376)
authorStephen Toub <stoub@microsoft.com>
Sun, 9 Jun 2019 09:40:35 +0000 (05:40 -0400)
committerGitHub <noreply@github.com>
Sun, 9 Jun 2019 09:40:35 +0000 (05:40 -0400)
The GetStateMachineData test helper I previously added only output the top-level state machine's fields.  This change causes it to recur into awaiters, so we can get more details on exactly what's being awaited and what's causing a hang.

Commit migrated from https://github.com/dotnet/corefx/commit/48c3adf8ad0256689375060437c2e6262d5e805a

src/libraries/Common/tests/System/Threading/Tasks/GetStateMachineData.cs

index 00ad9a8..6b72251 100644 (file)
 // The .NET Foundation licenses this file to you under the MIT license.
 // See the LICENSE file in the project root for more information.
 
+using System.Collections.Generic;
+using System.Linq;
 using System.Reflection;
 using System.Runtime.CompilerServices;
 using System.Text;
 
-/// <summary>
-/// Fragile trick for getting a description of the current state of a .NET Core async method state machine.
-/// To use, first await FetchAsync to get back an object:
-///     object box = await GetStateMachineData.FetchAsync();
-/// and then when the description is desired, use:
-///     string description = GetStateMachineData.Describe(box);
-/// </summary>
 namespace System.Threading.Tasks
 {
+    /// <summary>
+    /// Fragile trick for getting a description of the current state of a .NET Core async method state machine.
+    /// To use, first await FetchAsync to get back an object:
+    ///     object box = await GetStateMachineData.FetchAsync();
+    /// and then when the description is desired, use:
+    ///     string description = GetStateMachineData.Describe(box);
+    /// For example:
+    ///     using (new Timer(s => Console.WriteLine(GetStateMachineData.Describe(s)), await GetStateMachineData.FetchAsync(), 60_000, 60_000))
+    /// </summary>
     internal sealed class GetStateMachineData : ICriticalNotifyCompletion
     {
         private object _box;
 
-        private GetStateMachineData() { }
+        /// <summary>Returns an awaitable whose awaited result will be the boxed state machine for the async method.</summary>
         public static GetStateMachineData FetchAsync() => new GetStateMachineData();
 
-        public GetStateMachineData GetAwaiter() => this;
-        public bool IsCompleted => false;
-        public void OnCompleted(Action continuation) => UnsafeOnCompleted(continuation);
-        public void UnsafeOnCompleted(Action continuation)
-        {
-            _box = continuation.Target;
-            Task.Run(continuation);
-        }
-        public object GetResult() => _box;
-
+        /// <summary>Creates a string representation of a boxed state machine object.</summary>
         public static string Describe(object box)
         {
-            if (box is null)
-            {
-                return "(Couldn't get state machine box.)";
-            }
+            var sb = new StringBuilder();
+            var seen = new HashSet<object>();
+            Describe(box, sb, seen, 0);
+            return sb.ToString();
 
-            FieldInfo stateMachineField = box.GetType().GetField("StateMachine", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
-            if (stateMachineField is null)
+            static void Describe(object box, StringBuilder sb, HashSet<object> seen, int indentLevel)
             {
-                return $"(Couldn't get state machine field from {box}.";
-            }
+                string indent = string.Concat(Enumerable.Repeat("    ", indentLevel));
 
-            IAsyncStateMachine stateMachine = stateMachineField.GetValue(box) as IAsyncStateMachine;
-            if (stateMachine is null)
-            {
-                return $"(Null state machine from {box}.)";
-            }
+                // If we were handed a null object (which should only happen from recursion),
+                // state that the object was null.
+                if (box is null)
+                {
+                    sb.Append(indent).AppendLine($"(Object was null.)");
+                    return;
+                }
 
-            Type stateMachineType = stateMachine.GetType();
-            FieldInfo[] fields = stateMachineType.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
+                // If as we're walking a graph we happen upon a cycle, break it.
+                if (!seen.Add(box))
+                {
+                    sb.Append(indent).AppendLine($"(Object already seen in graph walk.)");
+                    return;
+                }
 
-            var sb = new StringBuilder();
-            sb.AppendLine(stateMachineType.FullName);
-            foreach (FieldInfo fi in fields)
-            {
-                sb.AppendLine($"    {fi.Name}: {ToString(fi.GetValue(stateMachine))}");
-            }
-            return sb.ToString();
-        }
+                // Try to get the StateMachine field from the AsyncStateMachineBox<>.  If we can't,
+                // just print out the details we can and bail.
+                FieldInfo stateMachineField = box.GetType().GetField("StateMachine", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
+                if (stateMachineField is null)
+                {
+                    sb.Append(indent).AppendLine($"(Couldn't get state machine field from {box}.)");
+                    sb.Append(indent).AppendLine(ToString(box));
+                    return;
+                }
 
-        private static string ToString(object value)
-        {
-            if (value is null)
-            {
-                return "(null)";
-            }
+                // Get the state machine from the StateMachine field.
+                IAsyncStateMachine stateMachine = stateMachineField.GetValue(box) as IAsyncStateMachine;
+                if (stateMachine is null)
+                {
+                    sb.Append(indent).AppendLine($"(Null state machine from {box}.)");
+                    return;
+                }
 
-            if (value is Task t)
-            {
-                return $"Status: {t.Status}, Exception: {t.Exception?.InnerException}";
+                // Print out the name of the state machine.
+                Type stateMachineType = stateMachine.GetType();
+                sb.Append(indent).AppendLine(stateMachineType.FullName);
+
+                // Get all of the fields on the state machine, and print them all out.
+                FieldInfo[] fields = stateMachineType.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
+                foreach (FieldInfo fi in fields)
+                {
+                    // Print out the field name and its value.
+                    object fiValue = fi.GetValue(stateMachine);
+                    sb.Append(indent).AppendLine($"  {fi.Name}: {ToString(fiValue)}");
+
+                    // If the field is an awaiter, recursively examine any tasks it directly references.
+                    if (fiValue is ICriticalNotifyCompletion)
+                    {
+                        foreach (FieldInfo possibleTask in fiValue.GetType().GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic))
+                        {
+                            if (possibleTask.GetValue(fiValue) is Task awaitedTask)
+                            {
+                                Describe(awaitedTask, sb, seen, indentLevel + 1);
+                            }
+                        }
+                    }
+                }
             }
 
-            return value.ToString();
+            static string ToString(object value) =>
+                value is null ? "(null)" :
+                value is Task t ? $"Status: {t.Status}, Exception: {t.Exception?.InnerException}" :
+                value.ToString();
+        }
+
+        private GetStateMachineData() { }
+        public GetStateMachineData GetAwaiter() => this;
+        public bool IsCompleted => false;
+        public void OnCompleted(Action continuation) => UnsafeOnCompleted(continuation);
+        public void UnsafeOnCompleted(Action continuation)
+        {
+            _box = continuation.Target;
+            Task.Run(continuation);
         }
+        public object GetResult() => _box;
     }
 }