From: Lee Culver Date: Wed, 15 Mar 2023 00:49:46 +0000 (-0700) Subject: Add a way to filter !eeheap (#3750) X-Git-Tag: accepted/tizen/unified/riscv/20231226.055542~43^2~2^2~29 X-Git-Url: http://review.tizen.org/git/?a=commitdiff_plain;h=d9f613aab5376d24bf82821724e35773a1385425;p=platform%2Fcore%2Fdotnet%2Fdiagnostics.git Add a way to filter !eeheap (#3750) * Add a way to filter !eeheap - !eeheap now only operates on the currently selected runtime (again) - Maoni requested a feature where we can give filters to !eeheap, as its output can be too long and detailed on larger dumps - Added the same -segment, -heap, and [memory, range] arguments to !eeheap to filter the output down to the requested ranges, segments, or heaps. - Loader/Modules heaps will also be filtered by "[memory, range]" (not not -heap and -segment). - Changed "Total" to "Partial" whenever we filter the heap. e.g. 'Total bytes consumed by CLR' -> 'Partial bytes consumed by CLR'. * Add DML to !eeheap -gc * Fix format mistake --- diff --git a/src/Microsoft.Diagnostics.ExtensionCommands/EEHeapCommand.cs b/src/Microsoft.Diagnostics.ExtensionCommands/EEHeapCommand.cs index 4c9a695da..90afe5dc4 100644 --- a/src/Microsoft.Diagnostics.ExtensionCommands/EEHeapCommand.cs +++ b/src/Microsoft.Diagnostics.ExtensionCommands/EEHeapCommand.cs @@ -8,14 +8,22 @@ using System.Linq; using System.Text; using Microsoft.Diagnostics.DebugServices; using Microsoft.Diagnostics.Runtime; +using static Microsoft.Diagnostics.ExtensionCommands.TableOutput; namespace Microsoft.Diagnostics.ExtensionCommands { - [Command(Name = "eeheap", Help = "Displays information about native memory that CLR has allocated.")] + [Command(Name = CommandName, Help = "Displays information about native memory that CLR has allocated.")] public class EEHeapCommand : CommandBase { + private const string CommandName = "eeheap"; + + private HeapWithFilters HeapWithFilters { get; set; } + + // Don't use the word "Total" if we have filtered out entries + private string TotalString => HeapWithFilters.HasFilters ? "Partial" : "Total"; + [ServiceImport] - public IRuntimeService RuntimeService { get; set; } + public ClrRuntime Runtime { get; set; } [ServiceImport] public IMemoryService MemoryService { get; set; } @@ -26,32 +34,44 @@ namespace Microsoft.Diagnostics.ExtensionCommands [Option(Name = "-loader", Help = "Only display the Loader.")] public bool ShowLoader { get; set; } + [Option(Name = "-heap")] + public int GCHeap { get; set; } = -1; + + [Option(Name = "-segment")] + public string Segment { get; set; } + + [Argument(Help = "Optional memory ranges in the form of: [Start [End]]")] + public string[] MemoryRange { get; set; } + public override void Invoke() { - IRuntime[] runtimes = RuntimeService.EnumerateRuntimes().ToArray(); + HeapWithFilters = new(Runtime.Heap) + { + // The user may want to filter loader regions by address + ThrowIfNoMatchingGCRegions = false + }; - ulong totalBytes = 0; - StringBuilder stringBuilder = null; - foreach (IRuntime iRuntime in runtimes) + if (GCHeap >= 0) { - if (runtimes.Length > 1) - { - WriteDivider($"{iRuntime.RuntimeType} {iRuntime.RuntimeModule?.GetVersionData()}"); - } + HeapWithFilters.GCHeap = GCHeap; + } - ClrRuntime clrRuntime = iRuntime.Services.GetService(); - totalBytes += PrintOneRuntime(ref stringBuilder, clrRuntime); + if (!string.IsNullOrWhiteSpace(Segment)) + { + HeapWithFilters.FilterBySegmentHex(Segment); } - // Only print the total bytes if we walked everything. - if (runtimes.Length > 1 && !ShowGC && !ShowLoader) + if (MemoryRange is not null) { - WriteLine($"Total bytes consumed by all CLRs: {FormatMemorySize(totalBytes, "0")}"); + HeapWithFilters.FilterByStringMemoryRange(MemoryRange, CommandName); } + + PrintOneRuntime(Runtime); } - private ulong PrintOneRuntime(ref StringBuilder stringBuilder, ClrRuntime clrRuntime) + private ulong PrintOneRuntime(ClrRuntime clrRuntime) { + StringBuilder stringBuilder = null; TableOutput output = new(Console, (21, "x12"), (0, "x12")) { AlignLeft = true @@ -78,7 +98,7 @@ namespace Microsoft.Diagnostics.ExtensionCommands if (!ShowGC && !ShowLoader) { WriteLine(); - WriteLine($"Total bytes consumed by CLR: {FormatMemorySize(totalSize, "0")}"); + WriteLine($"{TotalString} bytes consumed by CLR: {FormatMemorySize(totalSize, "0")}"); WriteLine(); } @@ -129,13 +149,14 @@ namespace Microsoft.Diagnostics.ExtensionCommands } } - IOrderedEnumerable> heapsByKind = from heap in appDomain.EnumerateLoaderAllocatorHeaps() - where loaderAllocatorsSeen.Add(heap.Address) - group heap by heap.Kind into g - orderby GetSortOrder(g.Key) - select g; + IOrderedEnumerable> filteredHeapsByKind = from heap in appDomain.EnumerateLoaderAllocatorHeaps() + where IsIncludedInFilter(heap) + where loaderAllocatorsSeen.Add(heap.Address) + group heap by heap.Kind into g + orderby GetSortOrder(g.Key) + select g; - return PrintAppDomainHeapsByKind(output, heapsByKind); + return PrintAppDomainHeapsByKind(output, filteredHeapsByKind); } private static int GetSortOrder(NativeHeapKind key) @@ -161,14 +182,14 @@ namespace Microsoft.Diagnostics.ExtensionCommands }; } - private ulong PrintAppDomainHeapsByKind(TableOutput output, IOrderedEnumerable> heapsByKind) + private ulong PrintAppDomainHeapsByKind(TableOutput output, IOrderedEnumerable> filteredHeapsByKind) { // Just build and print the table. ulong totalSize = 0; ulong totalWasted = 0; StringBuilder text = new(512); - foreach (IGrouping item in heapsByKind) + foreach (IGrouping item in filteredHeapsByKind) { text.Clear(); NativeHeapKind kind = item.Key; @@ -204,7 +225,7 @@ namespace Microsoft.Diagnostics.ExtensionCommands { WriteSizeAndWasted(text, totalSize, totalWasted); text.Append('.'); - output.WriteRow("Total size:", text); + output.WriteRow($"{TotalString} size:", text); } else { @@ -218,13 +239,12 @@ namespace Microsoft.Diagnostics.ExtensionCommands private ulong PrintCodeHeaps(TableOutput output, ClrRuntime clrRuntime) { ulong totalSize = 0; - StringBuilder text = new(512); foreach (ClrJitManager jitManager in clrRuntime.EnumerateJitManagers()) { output.WriteRow("JIT Manager:", jitManager.Address); - IEnumerable heaps = jitManager.EnumerateNativeHeaps().OrderBy(r => r.Kind).ThenBy(r => r.Address); + IEnumerable heaps = jitManager.EnumerateNativeHeaps().Where(IsIncludedInFilter).OrderBy(r => r.Kind).ThenBy(r => r.Address); ulong jitMgrSize = 0, jitMgrWasted = 0; foreach (ClrNativeHeapInfo heap in heaps) @@ -246,7 +266,7 @@ namespace Microsoft.Diagnostics.ExtensionCommands WriteSizeAndWasted(text, jitMgrSize, jitMgrWasted); text.Append('.'); - output.WriteRow("Total size:", text); + output.WriteRow($"{TotalString} size:", text); WriteDivider(); totalSize += jitMgrSize; @@ -255,6 +275,29 @@ namespace Microsoft.Diagnostics.ExtensionCommands return totalSize; } + private bool IsIncludedInFilter(ClrNativeHeapInfo info) + { + // ClrNativeHeapInfo is only filtered by memory range (not heap or segment). + if (HeapWithFilters.MemoryRange is not MemoryRange filterRange) + { + // no filter, so include everything + return true; + } + + if (filterRange.Contains(info.Address)) + { + return true; + } + + if (info.Size is ulong size && size > 0) + { + // Check for the last valid address in the range + return filterRange.Contains(info.Address + size - 1); + } + + return false; + } + private (ulong Size, ulong Wasted) CalculateSizeAndWasted(StringBuilder sb, ClrNativeHeapInfo heap) { sb.Append(heap.Address.ToString("x12")); @@ -324,7 +367,7 @@ namespace Microsoft.Diagnostics.ExtensionCommands ulong moduleSize = 0, moduleWasted = 0; text.Clear(); - foreach (ClrNativeHeapInfo info in module.EnumerateThunkHeap()) + foreach (ClrNativeHeapInfo info in module.EnumerateThunkHeap().Where(IsIncludedInFilter)) { if (text.Length > 0) { @@ -348,7 +391,7 @@ namespace Microsoft.Diagnostics.ExtensionCommands text.Clear(); WriteSizeAndWasted(text, totalSize, totalWasted); - output.WriteRow("Total size:", text); + output.WriteRow($"{TotalString} size:", text); return totalSize; } @@ -408,18 +451,42 @@ namespace Microsoft.Diagnostics.ExtensionCommands Console.WriteLine($"Number of GC Heaps: {heap.SubHeaps.Length}"); WriteDivider(); - foreach (ClrSubHeap gc_heap in heap.SubHeaps) + foreach (ClrSubHeap gc_heap in HeapWithFilters.EnumerateFilteredSubHeaps()) { if (heap.IsServer) { - Console.WriteLine($"Heap {gc_heap.Index} ({gc_heap.Address:x16})"); + Console.Write("Heap "); + Console.WriteDmlExec(gc_heap.Index.ToString(), $"!dumpheap -heap {gc_heap.Index}"); + Console.WriteLine($" ({gc_heap.Address:x16})"); } if (!gc_heap.HasRegions) { for (int i = 0; i <= 2 && i < gc_heap.GenerationTable.Length; i++) { - Console.WriteLine($"generation {i} starts at {gc_heap.GenerationTable[i].AllocationStart:x}"); + ClrSegment seg = heap.GetSegmentByAddress(gc_heap.GenerationTable[i].AllocationStart); + MemoryRange range = default; + if (seg is not null) + { + range = i switch + { + 0 => seg.Generation0, + 1 => seg.Generation1, + 2 => seg.Generation2, + _ => default + }; + } + + if (range.Length > 0) + { + Console.Write($"generation {i} starts at "); + Console.WriteDmlExec(gc_heap.GenerationTable[i].AllocationStart.ToString("x"), $"!dumpheap {range.Start:x} {range.End:x}"); + Console.WriteLine(); + } + else + { + Console.WriteLine($"generation {i} starts at {gc_heap.GenerationTable[i].AllocationStart:x}"); + } } Console.Write("ephemeral segment allocation context: "); @@ -438,7 +505,7 @@ namespace Microsoft.Diagnostics.ExtensionCommands WriteSegmentHeader(gcOutput); bool[] needToPrintGen = new bool[] { gc_heap.HasRegions, gc_heap.HasRegions, gc_heap.HasRegions }; - IEnumerable ephemeralSegments = gc_heap.Segments.Where(seg => seg.Kind == GCSegmentKind.Ephemeral || (seg.Kind >= GCSegmentKind.Generation0 && seg.Kind <= GCSegmentKind.Generation2)); + IEnumerable ephemeralSegments = HeapWithFilters.EnumerateFilteredSegments(gc_heap).Where(seg => seg.Kind == GCSegmentKind.Ephemeral || (seg.Kind >= GCSegmentKind.Generation0 && seg.Kind <= GCSegmentKind.Generation2)); IEnumerable segments = ephemeralSegments.OrderBy(seg => seg.Kind).ThenBy(seg => seg.Start); foreach (ClrSegment segment in segments) { @@ -453,7 +520,7 @@ namespace Microsoft.Diagnostics.ExtensionCommands } // print frozen object heap - segments = gc_heap.Segments.Where(seg => seg.Kind == GCSegmentKind.Frozen).OrderBy(seg => seg.Start); + segments = HeapWithFilters.EnumerateFilteredSegments(gc_heap).Where(seg => seg.Kind == GCSegmentKind.Frozen).OrderBy(seg => seg.Start); if (segments.Any()) { Console.WriteLine("Frozen object heap"); @@ -475,7 +542,7 @@ namespace Microsoft.Diagnostics.ExtensionCommands Console.WriteLine($"Large object heap starts at {gc_heap.GenerationTable[3].AllocationStart:x}"); } - segments = gc_heap.Segments.Where(seg => seg.Kind == GCSegmentKind.Large).OrderBy(seg => seg.Start); + segments = HeapWithFilters.EnumerateFilteredSegments(gc_heap).Where(seg => seg.Kind == GCSegmentKind.Large).OrderBy(seg => seg.Start); WriteSegmentHeader(gcOutput); foreach (ClrSegment segment in segments) @@ -484,7 +551,7 @@ namespace Microsoft.Diagnostics.ExtensionCommands } // print pinned object heap - segments = gc_heap.Segments.Where(seg => seg.Kind == GCSegmentKind.Pinned).OrderBy(seg => seg.Start); + segments = HeapWithFilters.EnumerateFilteredSegments(gc_heap).Where(seg => seg.Kind == GCSegmentKind.Pinned).OrderBy(seg => seg.Start); if (segments.Any()) { if (gc_heap.HasRegions || gc_heap.GenerationTable.Length <= 3) @@ -504,17 +571,26 @@ namespace Microsoft.Diagnostics.ExtensionCommands } } - Console.WriteLine($"Total Allocated Size: Size: {FormatMemorySize((ulong)gc_heap.Segments.Sum(r => (long)r.ObjectRange.Length))} bytes."); - Console.WriteLine($"Total Committed Size: Size: {FormatMemorySize((ulong)gc_heap.Segments.Sum(r => (long)r.CommittedMemory.Length))} bytes."); + if (HeapWithFilters.HasFilters) + { + Console.WriteLine($"{TotalString} Allocated Size: Size: {FormatMemorySize((ulong)HeapWithFilters.EnumerateFilteredSegments(gc_heap).Sum(r => (long)r.ObjectRange.Length))} bytes."); + Console.WriteLine($"{TotalString} Committed Size: Size: {FormatMemorySize((ulong)HeapWithFilters.EnumerateFilteredSegments(gc_heap).Sum(r => (long)r.CommittedMemory.Length))} bytes."); + } Console.WriteLine("------------------------------"); } - ulong totalAllocated = (ulong)heap.SubHeaps.SelectMany(gc_heap => gc_heap.Segments).Sum(r => (long)r.ObjectRange.Length); - ulong totalCommitted = (ulong)heap.SubHeaps.SelectMany(gc_heap => gc_heap.Segments).Sum(r => (long)r.CommittedMemory.Length); + string prefix = ""; + if (HeapWithFilters.HasFilters) + { + prefix = "Partial "; + } + + ulong totalAllocated = (ulong)HeapWithFilters.EnumerateFilteredSegments().Sum(r => (long)r.ObjectRange.Length); + ulong totalCommitted = (ulong)HeapWithFilters.EnumerateFilteredSegments().Sum(r => (long)r.CommittedMemory.Length); - Console.WriteLine($"GC Allocated Heap Size: Size: {FormatMemorySize(totalAllocated)} bytes."); - Console.WriteLine($"GC Committed Heap Size: Size: {FormatMemorySize(totalCommitted)} bytes."); + Console.WriteLine($"{prefix}GC Allocated Heap Size: Size: {FormatMemorySize(totalAllocated)} bytes."); + Console.WriteLine($"{prefix}GC Committed Heap Size: Size: {FormatMemorySize(totalCommitted)} bytes."); return totalCommitted; } @@ -526,7 +602,7 @@ namespace Microsoft.Diagnostics.ExtensionCommands private static void WriteSegment(TableOutput gcOutput, ClrSegment segment) { - gcOutput.WriteRow(segment.Address, + gcOutput.WriteRow(new DmlDumpHeapSegment(segment), segment.ObjectRange.Start, segment.ObjectRange.End, segment.CommittedMemory.End, FormatMemorySize(segment.ObjectRange.Length), FormatMemorySize(segment.CommittedMemory.Length)); } @@ -542,30 +618,5 @@ namespace Microsoft.Diagnostics.ExtensionCommands } private void WriteDivider(char c = '-', int width = 40) => WriteLine(new string(c, width)); - - private void WriteDivider(string header, int width = 120) - { - int lhs = (width - header.Length - 2) / 2; - if (lhs < 0) - { - WriteLine(header); - return; - } - - int rhs = lhs; - if ((header.Length % 2) == 1) - { - rhs++; - } - - StringBuilder sb = new(width + 1); - sb.Append('-', lhs); - sb.Append(' '); - sb.Append(header); - sb.Append(' '); - sb.Append('-', rhs); - - WriteLine(sb.ToString()); - } } } diff --git a/src/Microsoft.Diagnostics.ExtensionCommands/HeapWithFilters.cs b/src/Microsoft.Diagnostics.ExtensionCommands/HeapWithFilters.cs index fb9cc06e5..7c5604e6b 100644 --- a/src/Microsoft.Diagnostics.ExtensionCommands/HeapWithFilters.cs +++ b/src/Microsoft.Diagnostics.ExtensionCommands/HeapWithFilters.cs @@ -48,6 +48,11 @@ namespace Microsoft.Diagnostics.ExtensionCommands } } + /// + /// Whether or not to throw if there are now matching segments or subheaps. + /// + public bool ThrowIfNoMatchingGCRegions { get; set; } = true; + /// /// The minimum size of an object to enumerate. /// @@ -63,10 +68,16 @@ namespace Microsoft.Diagnostics.ExtensionCommands /// public Func, IOrderedEnumerable> SortSegments { get; set; } + /// + /// The order in which to enumerate subheaps. This only applies to subheap enumeration. + /// + public Func, IOrderedEnumerable> SortSubHeaps { get; set; } + public HeapWithFilters(ClrHeap heap) { _heap = heap; SortSegments = (seg) => seg.OrderBy(s => s.SubHeap.Index).ThenBy(s => s.Address); + SortSubHeaps = (heap) => heap.OrderBy(heap => heap.Index); } public void FilterBySegmentHex(string segmentStr) @@ -76,7 +87,7 @@ namespace Microsoft.Diagnostics.ExtensionCommands throw new ArgumentException($"Invalid segment address: {segmentStr}"); } - if (!_heap.Segments.Any(seg => seg.Address == segment || seg.CommittedMemory.Contains(segment))) + if (ThrowIfNoMatchingGCRegions && !_heap.Segments.Any(seg => seg.Address == segment || seg.CommittedMemory.Contains(segment))) { throw new ArgumentException($"No segments match address: {segment:x}"); } @@ -84,6 +95,27 @@ namespace Microsoft.Diagnostics.ExtensionCommands Segment = segment; } + public void FilterByStringMemoryRange(string[] memoryRange, string commandName) + { + if (memoryRange.Length > 0) + { + if (memoryRange.Length > 2) + { + string badArgument = memoryRange.FirstOrDefault(f => f.StartsWith("-") || f.StartsWith("/")); + if (badArgument != null) + { + throw new ArgumentException($"Unknown argument: {badArgument}"); + } + + throw new ArgumentException($"Too many arguments to !{commandName}"); + } + + string start = memoryRange[0]; + string end = memoryRange.Length > 1 ? memoryRange[1] : null; + FilterByHexMemoryRange(start, end); + } + } + public void FilterByHexMemoryRange(string startStr, string endStr) { if (!ulong.TryParse(startStr, NumberStyles.HexNumber, null, out ulong start)) @@ -122,15 +154,43 @@ namespace Microsoft.Diagnostics.ExtensionCommands MemoryRange = new(start, end); } - if (!_heap.Segments.Any(seg => seg.CommittedMemory.Overlaps(MemoryRange.Value))) + if (ThrowIfNoMatchingGCRegions && !_heap.Segments.Any(seg => seg.CommittedMemory.Overlaps(MemoryRange.Value))) { throw new ArgumentException($"No segments or objects in range {MemoryRange.Value}"); } } - public IEnumerable EnumerateFilteredSegments() + public IEnumerable EnumerateFilteredSubHeaps() + { + IEnumerable subheaps = _heap.SubHeaps; + if (GCHeap is int gcheap) + { + subheaps = subheaps.Where(heap => heap.Index == gcheap); + } + + if (Segment is ulong segment) + { + subheaps = subheaps.Where(heap => heap.Segments.Any(seg => seg.Address == segment || seg.CommittedMemory.Contains(segment))); + } + + if (MemoryRange is MemoryRange range) + { + subheaps = subheaps.Where(heap => heap.Segments.Any(seg => seg.CommittedMemory.Overlaps(range))); + } + + if (SortSubHeaps is not null) + { + subheaps = SortSubHeaps(subheaps); + } + + return subheaps; + } + + public IEnumerable EnumerateFilteredSegments() => EnumerateFilteredSegments(null); + + public IEnumerable EnumerateFilteredSegments(ClrSubHeap subheap) { - IEnumerable segments = _heap.Segments; + IEnumerable segments = subheap != null ? subheap.Segments : _heap.Segments; if (GCHeap is int gcheap) { segments = segments.Where(seg => seg.SubHeap.Index == gcheap); diff --git a/src/Microsoft.Diagnostics.ExtensionCommands/TableOutput.cs b/src/Microsoft.Diagnostics.ExtensionCommands/TableOutput.cs index 6c3d3662d..fb03d164e 100644 --- a/src/Microsoft.Diagnostics.ExtensionCommands/TableOutput.cs +++ b/src/Microsoft.Diagnostics.ExtensionCommands/TableOutput.cs @@ -277,5 +277,13 @@ namespace Microsoft.Diagnostics.ExtensionCommands { } } + + public sealed class DmlDumpHeapSegment : DmlExec + { + public DmlDumpHeapSegment(ClrSegment seg) + : base(seg?.Address ?? 0, seg != null ? $"!dumpheap -segment {seg.Address:x}" : "") + { + } + } } } diff --git a/src/Microsoft.Diagnostics.ExtensionCommands/VerifyHeapCommand.cs b/src/Microsoft.Diagnostics.ExtensionCommands/VerifyHeapCommand.cs index 95335a6e4..f204d6912 100644 --- a/src/Microsoft.Diagnostics.ExtensionCommands/VerifyHeapCommand.cs +++ b/src/Microsoft.Diagnostics.ExtensionCommands/VerifyHeapCommand.cs @@ -10,9 +10,11 @@ using static Microsoft.Diagnostics.ExtensionCommands.TableOutput; namespace Microsoft.Diagnostics.ExtensionCommands { - [Command(Name = "verifyheap", Help = "Searches the managed heap for memory corruption..")] + [Command(Name = CommandName, Help = "Searches the managed heap for memory corruption..")] public class VerifyHeapCommand : CommandBase { + private const string CommandName = "verifyheap"; + private int _totalObjects; [ServiceImport] @@ -43,22 +45,9 @@ namespace Microsoft.Diagnostics.ExtensionCommands filteredHeap.FilterBySegmentHex(Segment); } - if (MemoryRange is not null && MemoryRange.Length > 0) + if (MemoryRange is not null) { - if (MemoryRange.Length > 2) - { - string badArgument = MemoryRange.FirstOrDefault(f => f.StartsWith("-") || f.StartsWith("/")); - if (badArgument != null) - { - throw new ArgumentException($"Unknown argument: {badArgument}"); - } - - throw new ArgumentException("Too many arguments to !verifyheap"); - } - - string start = MemoryRange[0]; - string end = MemoryRange.Length > 1 ? MemoryRange[1] : null; - filteredHeap.FilterByHexMemoryRange(start, end); + filteredHeap.FilterByStringMemoryRange(MemoryRange, CommandName); } VerifyHeap(filteredHeap.EnumerateFilteredObjects(Console.CancellationToken), verifySyncTable: filteredHeap.HasFilters);