Add !dumpheap fragmentation statistics (#3799)
authorLee Culver <leculver@microsoft.com>
Tue, 4 Apr 2023 16:06:28 +0000 (09:06 -0700)
committerGitHub <noreply@github.com>
Tue, 4 Apr 2023 16:06:28 +0000 (16:06 +0000)
In my previous !dumpheap change I overlooked the fragmentation output.  This adds fragmentation output in the exact same was as the previous C++ code.

The new code now validates the Free region is actually followed by the next object, and that those objects do not live on the Pinned, Frozen, or Large object heaps.

src/Microsoft.Diagnostics.ExtensionCommands/DumpHeapCommand.cs
src/Microsoft.Diagnostics.ExtensionCommands/DumpHeapService.cs
src/Microsoft.Diagnostics.ExtensionCommands/NotReachableInRangeCommand.cs
src/Microsoft.Diagnostics.ExtensionCommands/ObjSizeCommand.cs
src/SOS/Strike/strike.cpp

index 4ba19ae1cf0faca25aff647e6aa14af311705617..c62435f1d35be9704fb840706a8787e04cb28a5f 100644 (file)
@@ -128,6 +128,7 @@ namespace Microsoft.Diagnostics.ExtensionCommands
                 });
             }
 
+            bool printFragmentation = false;
             DumpHeapService.DisplayKind displayKind = DumpHeapService.DisplayKind.Normal;
             if (ThinLock)
             {
@@ -141,8 +142,12 @@ namespace Microsoft.Diagnostics.ExtensionCommands
             {
                 displayKind = DumpHeapService.DisplayKind.Short;
             }
+            else
+            {
+                printFragmentation = true;
+            }
 
-            DumpHeap.PrintHeap(objectsToPrint, displayKind, StatOnly);
+            DumpHeap.PrintHeap(objectsToPrint, displayKind, StatOnly, printFragmentation);
         }
 
         private void ParseArguments()
index 8de38629670aec14eff730116d5bf1bb20cf3e43..f9687f8f33504c8d3e249d2e7f03978159a0e0d6 100644 (file)
@@ -18,6 +18,7 @@ namespace Microsoft.Diagnostics.ExtensionCommands
     [ServiceExport(Scope = ServiceScope.Runtime)]
     public class DumpHeapService
     {
+        private const ulong FragmentationBlockMinSize = 512 * 1024;
         private const char StringReplacementCharacter = '.';
 
         [ServiceImport]
@@ -34,8 +35,12 @@ namespace Microsoft.Diagnostics.ExtensionCommands
             Strings
         }
 
-        public void PrintHeap(IEnumerable<ClrObject> objects, DisplayKind displayKind, bool statsOnly)
+        public void PrintHeap(IEnumerable<ClrObject> objects, DisplayKind displayKind, bool statsOnly, bool printFragmentation)
         {
+            List<(ClrObject Free, ClrObject Next)> fragmentation = null;
+            Dictionary<(string String, ulong Size), uint> stringTable = null;
+            Dictionary<ulong, (int Count, ulong Size, string TypeName)> stats = new();
+
             TableOutput thinLockOutput = null;
             TableOutput objectTable = new(Console, (12, "x12"), (12, "x12"), (12, ""), (0, ""));
             if (!statsOnly && (displayKind is DisplayKind.Normal or DisplayKind.Strings))
@@ -43,9 +48,7 @@ namespace Microsoft.Diagnostics.ExtensionCommands
                 objectTable.WriteRow("Address", "MT", "Size");
             }
 
-            Dictionary<ulong, (int Count, ulong Size, string TypeName)> stats = new();
-            Dictionary<(string String, ulong Size), uint> stringTable = null;
-
+            ClrObject lastFreeObject = default;
             foreach (ClrObject obj in objects)
             {
                 if (displayKind == DisplayKind.ThinLock)
@@ -77,6 +80,35 @@ namespace Microsoft.Diagnostics.ExtensionCommands
                     objectTable.WriteRow(new DmlDumpObj(obj), new DmlDumpHeap(obj.Type?.MethodTable ?? 0), size, obj.IsFree ? "Free" : "");
                 }
 
+                if (printFragmentation)
+                {
+                    if (lastFreeObject.IsFree && obj.IsValid && !obj.IsFree)
+                    {
+                        // Check to see if the previous object lands directly before this one.  We don't want
+                        // to print fragmentation after changing segments, or after an allocation context.
+                        if (lastFreeObject.Address + lastFreeObject.Size == obj.Address)
+                        {
+                            // Also, don't report fragmentation for Large/Pinned/Frozen segments.  This check
+                            // is a little slow, so we do this last.
+                            ClrSegment seg = obj.Type.Heap.GetSegmentByAddress(obj);
+                            if (seg is not null && seg.Kind is not GCSegmentKind.Large or GCSegmentKind.Pinned or GCSegmentKind.Frozen)
+                            {
+                                fragmentation ??= new();
+                                fragmentation.Add((lastFreeObject, obj));
+                            }
+                        }
+                    }
+
+                    if (obj.IsFree && size >= FragmentationBlockMinSize)
+                    {
+                        lastFreeObject = obj;
+                    }
+                    else
+                    {
+                        lastFreeObject = default;
+                    }
+                }
+
                 if (displayKind == DisplayKind.Strings)
                 {
                     // We only read a maximum of 1024 characters for each string.  This may lead to some collisions if strings are unique
@@ -198,6 +230,28 @@ namespace Microsoft.Diagnostics.ExtensionCommands
                     Console.WriteLine($"Total {stats.Values.Sum(r => r.Count):n0} objects, {stats.Values.Sum(r => (long)r.Size):n0} bytes");
                 }
             }
+
+            // Print fragmentation if we calculated it
+            PrintFragmentation(fragmentation);
+        }
+
+        private void PrintFragmentation(List<(ClrObject Free, ClrObject Next)> fragmentation)
+        {
+            if (fragmentation is null || fragmentation.Count == 0)
+            {
+                return;
+            }
+
+            TableOutput output = new(Console, (16, "x12"), (12, "n0"), (16, "x12"));
+
+            Console.WriteLine();
+            Console.WriteLine("Fragmented blocks larger than 0.5 MB:");
+            output.WriteRow("Address", "Size", "Followed By");
+
+            foreach ((ClrObject free, ClrObject next) in fragmentation)
+            {
+                output.WriteRow(free.Address, free.Size, new DmlDumpObj(next.Address), next.Type?.Name ?? "<unknown_type>");
+            }
         }
 
         private string Sanitize(string str, int maxLen)
index 55ed214c4a58186063d5c697f9e39b78a7688ed8..59e399e303152af4251a301155a950c64532d979 100644 (file)
@@ -62,7 +62,7 @@ namespace Microsoft.Diagnostics.ExtensionCommands
             ulong curr = start;
 
             IEnumerable<ClrObject> liveObjs = EnumerateLiveObjectsInRange(end, curr);
-            DumpHeap.PrintHeap(liveObjs, Short ? DumpHeapService.DisplayKind.Short : DumpHeapService.DisplayKind.Normal, statsOnly: false);
+            DumpHeap.PrintHeap(liveObjs, Short ? DumpHeapService.DisplayKind.Short : DumpHeapService.DisplayKind.Normal, statsOnly: false, printFragmentation: false);
         }
 
         private IEnumerable<ClrObject> EnumerateLiveObjectsInRange(ulong end, ulong curr)
index 71b08ed5364e82eed7ec04d3bdd56f822555b559..0beb0e98f01f8b421920bab8ebd4190e77260e01 100644 (file)
@@ -49,7 +49,7 @@ namespace Microsoft.Diagnostics.ExtensionCommands
             Console.WriteLine();
 
             DumpHeapService.DisplayKind displayKind = Strings ? DumpHeapService.DisplayKind.Strings : DumpHeapService.DisplayKind.Normal;
-            DumpHeap.PrintHeap(GetTransitiveClosure(obj), displayKind, Stat);
+            DumpHeap.PrintHeap(GetTransitiveClosure(obj), displayKind, Stat, printFragmentation: false);
         }
 
         private static IEnumerable<ClrObject> GetTransitiveClosure(ClrObject obj)
index f7715c32fac41741b31357e328095d4c4703be01..e1470e45b080a9307e5f4f358793e91adc08db61 100644 (file)
@@ -3833,7 +3833,6 @@ DECLARE_API(DumpRuntimeTypes)
     return Status;
 }
 
-#define MIN_FRAGMENTATIONBLOCK_BYTES (1024*512)
 namespace sos
 {
     class FragmentationBlock