diff --git a/src/Microsoft.Diagnostics.ExtensionCommands/DumpHeapCommand.cs b/src/Microsoft.Diagnostics.ExtensionCommands/DumpHeapCommand.cs index c62435f1d3..2f8c47256b 100644 --- a/src/Microsoft.Diagnostics.ExtensionCommands/DumpHeapCommand.cs +++ b/src/Microsoft.Diagnostics.ExtensionCommands/DumpHeapCommand.cs @@ -72,6 +72,14 @@ public override void Invoke() ParseArguments(); IEnumerable 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 @@ public override void Invoke() } DumpHeap.PrintHeap(objectsToPrint, displayKind, StatOnly, printFragmentation); + + if (liveObjectWarning is bool original) + { + LiveObjects.PrintWarning = original; + } } private void ParseArguments() diff --git a/src/Microsoft.Diagnostics.ExtensionCommands/GCRootCommand.cs b/src/Microsoft.Diagnostics.ExtensionCommands/GCRootCommand.cs index 210d0f73ea..7260a22eef 100644 --- a/src/Microsoft.Diagnostics.ExtensionCommands/GCRootCommand.cs +++ b/src/Microsoft.Diagnostics.ExtensionCommands/GCRootCommand.cs @@ -26,6 +26,9 @@ public class GCRootCommand : CommandBase [ServiceImport] public RootCacheService RootCache { get; set; } + [ServiceImport] + public StaticVariableService StaticVariables { get; set; } + [ServiceImport] public ManagedFileLineService FileLineService { get; set; } @@ -143,7 +146,7 @@ private int PrintOlderGenerationRoots(GCRoot gcroot, int gen) } Console.WriteLine($" {objAddress:x}"); - PrintPath(Console, RootCache, Runtime.Heap, path); + PrintPath(Console, RootCache, StaticVariables, Runtime.Heap, path); Console.WriteLine(); count++; @@ -216,11 +219,11 @@ private int PrintNonStackRoots(GCRoot gcroot) 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 @@ public static void PrintPath(IConsoleService console, RootCacheService rootCache 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 @@ private static string NameForHandle(ClrHandleKind handleKind) ClrHandleKind.SizedRef => "sized ref handle", ClrHandleKind.WeakWinRT => "weak WinRT handle", _ => handleKind.ToString() - }; ; + }; } private string GetFrameOutput(ClrStackFrame currFrame) diff --git a/src/Microsoft.Diagnostics.ExtensionCommands/PathToCommand.cs b/src/Microsoft.Diagnostics.ExtensionCommands/PathToCommand.cs index e4c2254fa6..e63940d3be 100644 --- a/src/Microsoft.Diagnostics.ExtensionCommands/PathToCommand.cs +++ b/src/Microsoft.Diagnostics.ExtensionCommands/PathToCommand.cs @@ -58,7 +58,7 @@ public override void Invoke() 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 index 0000000000..a9e03d4ecc --- /dev/null +++ b/src/Microsoft.Diagnostics.ExtensionCommands/StaticVariableService.cs @@ -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 _fields; + private IEnumerator<(ulong Address, ClrStaticField Static)> _enumerator; + + [ServiceImport] + public ClrRuntime Runtime { get; set; } + + /// + /// Returns the static field at the given 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[]. + /// The field corresponding to the given address. Non-null if return + /// is true. + /// True if the address corresponded to a static variable, false otherwise. + 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); + } + } + } + } + } + } + } +}