Add static variable names to !gcroot (#3872)
authorLee Culver <leculver@microsoft.com>
Wed, 10 May 2023 16:39:40 +0000 (09:39 -0700)
committerGitHub <noreply@github.com>
Wed, 10 May 2023 16:39:40 +0000 (09:39 -0700)
Static variables can be opaque to the user when looking at !gcroot, since they show up as a pinned handle to an object array.  It's also difficult to distinguish things _other_ than static variables which follow the same pattern.  This change adds the static variable name to the output of !gcroot to help distinguish these differences.

- Remove "this will take a while" warning output from !dumpheap -short.
- Added StaticVariableService to resolve the address/objects of static variables.  This can be a relatively expensive operation, so it's important to cache the result.
- Updated GCRootCommand with the heuristic.

src/Microsoft.Diagnostics.ExtensionCommands/DumpHeapCommand.cs
src/Microsoft.Diagnostics.ExtensionCommands/GCRootCommand.cs
src/Microsoft.Diagnostics.ExtensionCommands/PathToCommand.cs
src/Microsoft.Diagnostics.ExtensionCommands/StaticVariableService.cs [new file with mode: 0644]

index c62435f1d35be9704fb840706a8787e04cb28a5f..2f8c47256b3ee8f566265e1735260161d73c3901 100644 (file)
@@ -72,6 +72,14 @@ namespace Microsoft.Diagnostics.ExtensionCommands
             ParseArguments();
 
             IEnumerable<ClrObject> objectsToPrint = FilteredHeap.EnumerateFilteredObjects(Console.CancellationToken);
+
+            bool? liveObjectWarning = null;
+            if ((Live || Dead) && Short)
+            {
+                liveObjectWarning = LiveObjects.PrintWarning;
+                LiveObjects.PrintWarning = false;
+            }
+
             if (Live)
             {
                 objectsToPrint = objectsToPrint.Where(LiveObjects.IsLive);
@@ -148,6 +156,11 @@ namespace Microsoft.Diagnostics.ExtensionCommands
             }
 
             DumpHeap.PrintHeap(objectsToPrint, displayKind, StatOnly, printFragmentation);
+
+            if (liveObjectWarning is bool original)
+            {
+                LiveObjects.PrintWarning = original;
+            }
         }
 
         private void ParseArguments()
index 210d0f73eada153b2956d9e13fa2e094a57de65c..7260a22eef8bde818f17836e3d2e79c75929aa74 100644 (file)
@@ -26,6 +26,9 @@ namespace Microsoft.Diagnostics.ExtensionCommands
         [ServiceImport]
         public RootCacheService RootCache { get; set; }
 
+        [ServiceImport]
+        public StaticVariableService StaticVariables { get; set; }
+
         [ServiceImport]
         public ManagedFileLineService FileLineService { get; set; }
 
@@ -143,7 +146,7 @@ namespace Microsoft.Diagnostics.ExtensionCommands
                                 }
 
                                 Console.WriteLine($"    {objAddress:x}");
-                                PrintPath(Console, RootCache, Runtime.Heap, path);
+                                PrintPath(Console, RootCache, StaticVariables, Runtime.Heap, path);
                                 Console.WriteLine();
 
                                 count++;
@@ -216,11 +219,11 @@ namespace Microsoft.Diagnostics.ExtensionCommands
         private void PrintPath(ClrRoot root, GCRoot.ChainLink link)
         {
             PrintRoot(root);
-            PrintPath(Console, RootCache, Runtime.Heap, link);
+            PrintPath(Console, RootCache, StaticVariables, Runtime.Heap, link);
             Console.WriteLine();
         }
 
-        public static void PrintPath(IConsoleService console, RootCacheService rootCache, ClrHeap heap, GCRoot.ChainLink link)
+        public static void PrintPath(IConsoleService console, RootCacheService rootCache, StaticVariableService statics, ClrHeap heap, GCRoot.ChainLink link)
         {
             Table objectOutput = new(console, Text.WithWidth(2), DumpObj, TypeName, Text)
             {
@@ -229,13 +232,60 @@ namespace Microsoft.Diagnostics.ExtensionCommands
 
             objectOutput.SetAlignment(Align.Left);
 
+            bool first = true;
+            bool isPossibleStatic = true;
+
+            ClrObject firstObj = default;
+
             ulong prevObj = 0;
             while (link != null)
             {
-                bool isDependentHandleLink = rootCache.IsDependentHandleLink(prevObj, link.Object);
                 ClrObject obj = heap.GetObject(link.Object);
 
-                objectOutput.WriteRow("->", obj, obj.Type, (isDependentHandleLink ? " (dependent handle)" : ""));
+                // Check whether this link is a dependent handle
+                string extraText = "";
+                bool isDependentHandleLink = rootCache.IsDependentHandleLink(prevObj, link.Object);
+                if (isDependentHandleLink)
+                {
+                    extraText = "(dependent handle)";
+                }
+
+                // Print static variable info.  In all versions of the runtime, static variables are stored in
+                // a pinned object array.  We check if the first link in the chain is an object[], and if so we
+                // check if the second object's address is the location of a static variable.  We could further
+                // narrow this by checking the root type, but that needlessly complicates this code...we can't
+                // get false positives or negatives here (as nothing points to static variable object[] other
+                // than the root).
+                if (first)
+                {
+                    firstObj = obj;
+                    isPossibleStatic = firstObj.IsValid && firstObj.IsArray && firstObj.Type.Name == "System.Object[]";
+                    first = false;
+                }
+                else if (isPossibleStatic)
+                {
+                    if (statics is not null && !isDependentHandleLink)
+                    {
+                        foreach (ClrReference reference in firstObj.EnumerateReferencesWithFields(carefully: false, considerDependantHandles: false))
+                        {
+                            if (reference.Object == obj)
+                            {
+                                ulong address = firstObj + (uint)reference.Offset;
+
+                                if (statics.TryGetStaticByAddress(address, out ClrStaticField field))
+                                {
+                                    extraText = $"(static variable: {field.Type?.Name ?? "Unknown"}.{field.Name})";
+                                    break;
+                                }
+                            }
+                        }
+                    }
+
+                    // only the first object[] in the chain is possible to be the static array
+                    isPossibleStatic = false;
+                }
+
+                objectOutput.WriteRow("->", obj, obj.Type, extraText);
 
                 prevObj = link.Object;
                 link = link.Next;
@@ -322,7 +372,7 @@ namespace Microsoft.Diagnostics.ExtensionCommands
                 ClrHandleKind.SizedRef => "sized ref handle",
                 ClrHandleKind.WeakWinRT => "weak WinRT handle",
                 _ => handleKind.ToString()
-            }; ;
+            };
         }
 
         private string GetFrameOutput(ClrStackFrame currFrame)
index e4c2254fa68735aff0276fc0aea1fb5e4e964f9c..e63940d3beab218e9118df0dfa286bedde10a5bb 100644 (file)
@@ -58,7 +58,7 @@ namespace Microsoft.Diagnostics.ExtensionCommands
             GCRoot.ChainLink path = gcroot.FindPathFrom(sourceObj);
             if (path is not null)
             {
-                GCRootCommand.PrintPath(Console, RootCache, heap, path);
+                GCRootCommand.PrintPath(Console, RootCache, null, heap, path);
             }
             else
             {
diff --git a/src/Microsoft.Diagnostics.ExtensionCommands/StaticVariableService.cs b/src/Microsoft.Diagnostics.ExtensionCommands/StaticVariableService.cs
new file mode 100644 (file)
index 0000000..a9e03d4
--- /dev/null
@@ -0,0 +1,99 @@
+// 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.Generic;
+using Microsoft.Diagnostics.DebugServices;
+using Microsoft.Diagnostics.Runtime;
+
+namespace Microsoft.Diagnostics.ExtensionCommands
+{
+    [ServiceExport(Scope = ServiceScope.Runtime)]
+    public class StaticVariableService
+    {
+        private Dictionary<ulong, ClrStaticField> _fields;
+        private IEnumerator<(ulong Address, ClrStaticField Static)> _enumerator;
+
+        [ServiceImport]
+        public ClrRuntime Runtime { get; set; }
+
+        /// <summary>
+        /// Returns the static field at the given address.
+        /// </summary>
+        /// <param name="address">The address of the static field.  Note that this is not a pointer to
+        /// an object, but rather a pointer to where the CLR runtime tracks the static variable's
+        /// location.  In all versions of the runtime, address will live in the middle of a pinned
+        /// object[].</param>
+        /// <param name="field">The field corresponding to the given address.  Non-null if return
+        /// is true.</param>
+        /// <returns>True if the address corresponded to a static variable, false otherwise.</returns>
+        public bool TryGetStaticByAddress(ulong address, out ClrStaticField field)
+        {
+            if (_fields is null)
+            {
+                _fields = new();
+                _enumerator = EnumerateStatics().GetEnumerator();
+            }
+
+            if (_fields.TryGetValue(address, out field))
+            {
+                return true;
+            }
+
+            // pay for play lookup
+            if (_enumerator is not null)
+            {
+                do
+                {
+                    _fields[_enumerator.Current.Address] = _enumerator.Current.Static;
+                    if (_enumerator.Current.Address == address)
+                    {
+                        field = _enumerator.Current.Static;
+                        return true;
+                    }
+                } while (_enumerator.MoveNext());
+
+                _enumerator = null;
+            }
+
+            return false;
+        }
+
+        public IEnumerable<(ulong Address, ClrStaticField Static)> EnumerateStatics()
+        {
+            ClrAppDomain shared = Runtime.SharedDomain;
+
+            foreach (ClrModule module in Runtime.EnumerateModules())
+            {
+                foreach ((ulong mt, _) in module.EnumerateTypeDefToMethodTableMap())
+                {
+                    ClrType type = Runtime.GetTypeByMethodTable(mt);
+                    if (type is null)
+                    {
+                        continue;
+                    }
+
+                    foreach (ClrStaticField stat in type.StaticFields)
+                    {
+                        foreach (ClrAppDomain domain in Runtime.AppDomains)
+                        {
+                            ulong address = stat.GetAddress(domain);
+                            if (address != 0)
+                            {
+                                yield return (address, stat);
+                            }
+                        }
+
+                        if (shared is not null)
+                        {
+                            ulong address = stat.GetAddress(shared);
+                            if (address != 0)
+                            {
+                                yield return (address, stat);
+                            }
+                        }
+                    }
+                }
+            }
+        }
+    }
+}