diff --git a/docs/design/datacontracts/GC.md b/docs/design/datacontracts/GC.md index b8d59f80475369..734f8dd7d7b84c 100644 --- a/docs/design/datacontracts/GC.md +++ b/docs/design/datacontracts/GC.md @@ -116,6 +116,33 @@ public readonly struct GCOomData HandleType[] GetHandleTypes(uint[] types); // Gets the global allocation context pointer and limit void GetGlobalAllocationContext(out TargetPointer allocPtr, out TargetPointer allocLimit); + + // Gets handle table memory regions (segments) + IReadOnlyList GetHandleTableMemoryRegions(); + // Gets GC bookkeeping memory regions (card table info linked list) + IReadOnlyList GetGCBookkeepingMemoryRegions(); + // Gets GC free regions (free region lists and freeable segments) + IReadOnlyList GetGCFreeRegions(); +``` + +```csharp +public enum FreeRegionKind +{ + FreeUnknownRegion = 0, + FreeGlobalHugeRegion = 1, + FreeGlobalRegion = 2, + FreeRegion = 3, + FreeSohSegment = 4, + FreeUohSegment = 5, +} + +public readonly struct GCMemoryRegionData +{ + public TargetPointer Start { get; init; } + public ulong Size { get; init; } + public ulong ExtraData { get; init; } + public int Heap { get; init; } +} ``` ## Version 1 @@ -175,6 +202,13 @@ Data descriptors used: | `TableSegment` | RgAllocation | GC | Circular block-list links per block | | `TableSegment` | RgValue | GC | Start of handle value storage | | `TableSegment` | RgUserData | GC | Auxiliary per-block metadata (e.g. secondary handle blocks) | +| `CardTableInfo` | Recount | GC | Reference count for the card table | +| `CardTableInfo` | Size | GC | Total size of the bookkeeping allocation | +| `CardTableInfo` | NextCardTable | GC | Pointer to the next card table in the linked list | +| `RegionFreeList` | HeadFreeRegion | GC | Head of the free region segment list | +| `GCHeap` | FreeableSohSegment | GC | Head of the freeable SOH segment linked list (server builds, background GC) | +| `GCHeap` | FreeableUohSegment | GC | Head of the freeable UOH segment linked list (server builds, background GC) | +| `GCHeap` | FreeRegions | GC | Start of the per-heap free region list array (server builds, region GC) | | `GCAllocContext` | AllocBytes | VM | Number of bytes allocated on SOH by this context | | `GCAllocContext` | AllocBytesLoh | VM | Number of bytes allocated not on SOH by this context | | `EEAllocContext` | GCAllocationContext | VM | The `GCAllocContext` struct within an `EEAllocContext` | @@ -229,6 +263,15 @@ Global variables used: | `FeatureJavaMarshal` | byte | VM | Non-zero when Java marshal support is enabled | | `GlobalAllocContext` | TargetPointer | VM | Pointer to the global `EEAllocContext` | | `TotalCpuCount` | uint | GC | Number of available processors | +| `HandleSegmentSize` | uint | GC | Size of each handle table segment allocation | +| `CardTableInfoSize` | uint | GC | Size of the `dac_card_table_info` structure | +| `CountFreeRegionKinds` | uint | GC | Number of free region kinds (basic, large, huge) | +| `GlobalFreeHugeRegions` | TargetPointer | GC | Pointer to the global free huge region list | +| `GlobalRegionsToDecommit` | TargetPointer | GC | Pointer to the global regions-to-decommit array | +| `BookkeepingStart` | TargetPointer | GC | Pointer to the bookkeeping start address | +| `GCHeapFreeableSohSegment` | TargetPointer | GC | Pointer to the freeable SOH segment head (workstation builds) | +| `GCHeapFreeableUohSegment` | TargetPointer | GC | Pointer to the freeable UOH segment head (workstation builds) | +| `GCHeapFreeRegions` | TargetPointer | GC | Pointer to the free regions array (workstation builds) | Contracts used: | Contract Name | @@ -740,3 +783,145 @@ void IGC.GetGlobalAllocationContext(out TargetPointer allocPtr, out TargetPointe allocLimit = target.ReadPointer(globalAllocContextAddress + /* EEAllocContext::GCAllocationContext offset */ + /* GCAllocContext::Limit offset */); } ``` + +GetHandleTableMemoryRegions +```csharp +IReadOnlyList IGC.GetHandleTableMemoryRegions() +{ + List regions = new(); + uint handleSegmentSize = target.ReadGlobal("HandleSegmentSize"); + // For server GC, use TotalCpuCount (processor count) for the number of + // table slots per bucket, not NumHeaps (GC heap count). + uint tableCount = isServerGC + ? target.Read(target.ReadGlobalPointer("TotalCpuCount")) + : 1; + + int maxRegions = 8192; + TargetPointer handleTableMap = target.ReadGlobalPointer("HandleTableMap"); + while (handleTableMap != null && maxRegions >= 0) + { + HandleTableMap map = Read(handleTableMap); + foreach (TargetPointer bucketPtr in map.BucketsPtr) + { + if (bucketPtr == null) continue; + HandleTableBucket bucket = Read(bucketPtr); + for (uint j = 0; j < tableCount; j++) + { + TargetPointer htPtr = ReadPointer(bucket.Table + j * PointerSize); + if (htPtr == null) continue; + HandleTable ht = Read(htPtr); + if (ht.SegmentList == null) continue; + TargetPointer seg = ht.SegmentList; + TargetPointer first = seg; + do + { + TableSegment ts = Read(seg); + regions.Add(new GCMemoryRegionData { Start = seg, Size = handleSegmentSize, Heap = (int)j }); + seg = ts.NextSegment; + maxRegions--; + } while (seg != null && seg != first && maxRegions >= 0); + } + } + handleTableMap = map.Next; + maxRegions--; + } + return regions; +} +``` + +GetGCBookkeepingMemoryRegions +```csharp +IReadOnlyList IGC.GetGCBookkeepingMemoryRegions() +{ + List regions = new(); + if (!TryReadGlobalPointer("BookkeepingStart", out TargetPointer? bkGlobal)) + return regions; + TargetPointer bookkeepingStart = ReadPointer(bkGlobal); + if (bookkeepingStart == null) return regions; + + uint cardTableInfoSize = ReadGlobal("CardTableInfoSize"); + CardTableInfo cti = Read(bookkeepingStart); + if (cti.Recount != 0 && cti.Size != 0) + regions.Add(new GCMemoryRegionData { Start = bookkeepingStart, Size = cti.Size }); + + TargetPointer next = cti.NextCardTable; + TargetPointer firstNext = next; + int maxRegions = 32; + while (next != null && next > cardTableInfoSize && maxRegions > 0) + { + TargetPointer ctAddr = next - cardTableInfoSize; + CardTableInfo ct = Read(ctAddr); + if (ct.Recount != 0 && ct.Size != 0) + regions.Add(new GCMemoryRegionData { Start = ctAddr, Size = ct.Size }); + next = ct.NextCardTable; + if (next == firstNext) break; + maxRegions--; + } + return regions; +} +``` + +GetGCFreeRegions +```csharp +IReadOnlyList IGC.GetGCFreeRegions() +{ + List regions = new(); + int countFreeRegionKinds = min(ReadGlobal("CountFreeRegionKinds"), 16); + uint regionFreeListSize = GetTypeInfo(RegionFreeList).Size; + + // Global free huge regions + if (TryReadGlobalPointer("GlobalFreeHugeRegions", out TargetPointer? globalHuge)) + AddFreeList(globalHuge, FreeGlobalHugeRegion, regions); + + // Global regions to decommit + if (TryReadGlobalPointer("GlobalRegionsToDecommit", out TargetPointer? globalDecommit)) + for (int i = 0; i < countFreeRegionKinds; i++) + AddFreeList(globalDecommit + i * regionFreeListSize, FreeGlobalRegion, regions); + + if (isServerGC) + { + // For each server heap: enumerate per-heap free regions + freeable segments + for each heap in server heaps: + for (int j = 0; j < countFreeRegionKinds; j++) + AddFreeList(heap.FreeRegions + j * regionFreeListSize, FreeRegion, regions, heapIndex); + AddSegmentList(heap.FreeableSohSegment, FreeSohSegment, regions, heapIndex); + AddSegmentList(heap.FreeableUohSegment, FreeUohSegment, regions, heapIndex); + } + else + { + // Workstation: use globals for free regions and freeable segments + if (TryReadGlobalPointer("GCHeapFreeRegions", out TargetPointer? freeRegions)) + for (int i = 0; i < countFreeRegionKinds; i++) + AddFreeList(freeRegions + i * regionFreeListSize, FreeRegion, regions); + if (TryReadGlobalPointer("GCHeapFreeableSohSegment", out TargetPointer? soh)) + AddSegmentList(ReadPointer(soh), FreeSohSegment, regions); + if (TryReadGlobalPointer("GCHeapFreeableUohSegment", out TargetPointer? uoh)) + AddSegmentList(ReadPointer(uoh), FreeUohSegment, regions); + } + return regions; +} + +void AddFreeList(TargetPointer freeListAddr, FreeRegionKind kind, List regions, int heap = 0) +{ + RegionFreeList fl = Read(freeListAddr); + if (fl.HeadFreeRegion != null) + AddSegmentList(fl.HeadFreeRegion, kind, regions, heap); +} + +void AddSegmentList(TargetPointer start, FreeRegionKind kind, List regions, int heap = 0) +{ + int iterationMax = 2048; + TargetPointer curr = start; + while (curr != null && iterationMax-- > 0) + { + HeapSegment seg = Read(curr); + if (seg.Mem != null) + { + ulong size = (seg.Mem < seg.Committed) ? seg.Committed - seg.Mem : 0; + regions.Add(new GCMemoryRegionData { Start = seg.Mem, Size = size, ExtraData = kind, Heap = heap }); + } + curr = seg.Next; + if (curr == start) break; + } +} +``` diff --git a/src/coreclr/gc/datadescriptor/datadescriptor.h b/src/coreclr/gc/datadescriptor/datadescriptor.h index e337cc0ca650e3..d9589fedb0e056 100644 --- a/src/coreclr/gc/datadescriptor/datadescriptor.h +++ b/src/coreclr/gc/datadescriptor/datadescriptor.h @@ -76,6 +76,19 @@ struct cdac_data GC_HEAP_FIELD(CompactReasons, compact_reasons_per_heap) GC_HEAP_FIELD(ExpandMechanisms, expand_mechanisms_per_heap) GC_HEAP_FIELD(InterestingMechanismBits, interesting_mechanism_bits_per_heap) + /* For use in GCFreeRegions APIs */ +#ifdef BACKGROUND_GC + GC_HEAP_FIELD(FreeableSohSegment, freeable_soh_segment) + GC_HEAP_FIELD(FreeableUohSegment, freeable_uoh_segment) +#endif // BACKGROUND_GC +#ifdef USE_REGIONS + GC_HEAP_FIELD(FreeRegions, free_regions) + + static constexpr decltype(&GC_NAMESPACE::gc_heap::global_free_huge_regions) GlobalFreeHugeRegions = &GC_NAMESPACE::gc_heap::global_free_huge_regions; + static constexpr decltype(&GC_NAMESPACE::gc_heap::global_regions_to_decommit) GlobalRegionsToDecommit = &GC_NAMESPACE::gc_heap::global_regions_to_decommit; +#endif // USE_REGIONS + + static constexpr decltype(&GC_NAMESPACE::gc_heap::bookkeeping_start) BookkeepingStart = &GC_NAMESPACE::gc_heap::bookkeeping_start; }; template<> diff --git a/src/coreclr/gc/datadescriptor/datadescriptor.inc b/src/coreclr/gc/datadescriptor/datadescriptor.inc index 3b9b487aa069d4..2678b166bb36f0 100644 --- a/src/coreclr/gc/datadescriptor/datadescriptor.inc +++ b/src/coreclr/gc/datadescriptor/datadescriptor.inc @@ -34,6 +34,13 @@ CDAC_TYPE_FIELD(GCHeap, /*pointer*/, InterestingData, cdac_data::CompactReasons) CDAC_TYPE_FIELD(GCHeap, /*pointer*/, ExpandMechanisms, cdac_data::ExpandMechanisms) CDAC_TYPE_FIELD(GCHeap, /*pointer*/, InterestingMechanismBits, cdac_data::InterestingMechanismBits) +#ifdef BACKGROUND_GC +CDAC_TYPE_FIELD(GCHeap, /*pointer*/, FreeableSohSegment, cdac_data::FreeableSohSegment) +CDAC_TYPE_FIELD(GCHeap, /*pointer*/, FreeableUohSegment, cdac_data::FreeableUohSegment) +#endif // BACKGROUND_GC +#ifdef USE_REGIONS +CDAC_TYPE_FIELD(GCHeap, /*pointer*/, FreeRegions, cdac_data::FreeRegions) +#endif // USE_REGIONS CDAC_TYPE_END(GCHeap) #endif // SERVER_GC @@ -104,6 +111,18 @@ CDAC_TYPE_FIELD(TableSegment, /*uint8_t[]*/, RgValue, offsetof(TableSegment, rgV CDAC_TYPE_FIELD(TableSegment, /*uint8_t[]*/, RgUserData, offsetof(TableSegment, rgUserData)) CDAC_TYPE_END(TableSegment) +CDAC_TYPE_BEGIN(CardTableInfo) +CDAC_TYPE_SIZE(sizeof(dac_card_table_info)) +CDAC_TYPE_FIELD(CardTableInfo, /*uint32*/, Recount, offsetof(dac_card_table_info, recount)) +CDAC_TYPE_FIELD(CardTableInfo, /*nuint*/, Size, offsetof(dac_card_table_info, size)) +CDAC_TYPE_FIELD(CardTableInfo, /*pointer*/, NextCardTable, offsetof(dac_card_table_info, next_card_table)) +CDAC_TYPE_END(CardTableInfo) + +CDAC_TYPE_BEGIN(RegionFreeList) +CDAC_TYPE_SIZE(sizeof(dac_region_free_list)) +CDAC_TYPE_FIELD(RegionFreeList, /*pointer*/, HeadFreeRegion, offsetof(dac_region_free_list, head_free_region)) +CDAC_TYPE_END(RegionFreeList) + CDAC_TYPES_END() CDAC_GLOBALS_BEGIN() @@ -125,6 +144,11 @@ CDAC_GLOBAL(HandleBlocksPerSegment, /*uint32*/, HANDLE_BLOCKS_PER_SEGMENT) CDAC_GLOBAL(HandleMaxInternalTypes, /*uint32*/, HANDLE_MAX_INTERNAL_TYPES) CDAC_GLOBAL(HandlesPerBlock, /*uint32*/, HANDLE_HANDLES_PER_BLOCK) CDAC_GLOBAL(BlockInvalid, /*uint8*/, BLOCK_INVALID) +CDAC_GLOBAL(HandleSegmentSize, /*uint32*/, HANDLE_SEGMENT_SIZE) +CDAC_GLOBAL(CardTableInfoSize, /*uint32*/, (uint32_t)sizeof(dac_card_table_info)) +#ifdef USE_REGIONS +CDAC_GLOBAL(CountFreeRegionKinds, /*uint32*/, (uint32_t)FREE_REGION_KINDS) +#endif // USE_REGIONS #ifndef SERVER_GC @@ -149,9 +173,21 @@ CDAC_GLOBAL_POINTER(GCHeapInterestingData, cdac_data::Int CDAC_GLOBAL_POINTER(GCHeapCompactReasons, cdac_data::CompactReasons) CDAC_GLOBAL_POINTER(GCHeapExpandMechanisms, cdac_data::ExpandMechanisms) CDAC_GLOBAL_POINTER(GCHeapInterestingMechanismBits, cdac_data::InterestingMechanismBits) +#ifdef BACKGROUND_GC +CDAC_GLOBAL_POINTER(GCHeapFreeableSohSegment, cdac_data::FreeableSohSegment) +CDAC_GLOBAL_POINTER(GCHeapFreeableUohSegment, cdac_data::FreeableUohSegment) +#endif // BACKGROUND_GC +#ifdef USE_REGIONS +CDAC_GLOBAL_POINTER(GCHeapFreeRegions, cdac_data::FreeRegions) +#endif // USE_REGIONS #endif // !SERVER_GC CDAC_GLOBAL_POINTER(HandleTableMap, &g_HandleTableMap) +CDAC_GLOBAL_POINTER(BookkeepingStart, cdac_data::BookkeepingStart) +#ifdef USE_REGIONS +CDAC_GLOBAL_POINTER(GlobalFreeHugeRegions, cdac_data::GlobalFreeHugeRegions) +CDAC_GLOBAL_POINTER(GlobalRegionsToDecommit, cdac_data::GlobalRegionsToDecommit) +#endif // USE_REGIONS #ifdef SERVER_GC #define GC_TYPE server diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IGC.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IGC.cs index 761219b245b431..cc4b75fe915ec5 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IGC.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IGC.cs @@ -98,6 +98,24 @@ public readonly struct GCOomData public bool LohP { get; init; } } +public enum FreeRegionKind +{ + FreeUnknownRegion = 0, + FreeGlobalHugeRegion = 1, + FreeGlobalRegion = 2, + FreeRegion = 3, + FreeSohSegment = 4, + FreeUohSegment = 5, +} + +public readonly struct GCMemoryRegionData +{ + public TargetPointer Start { get; init; } + public ulong Size { get; init; } + public ulong ExtraData { get; init; } + public int Heap { get; init; } +} + public interface IGC : IContract { static string IContract.Name { get; } = nameof(GC); @@ -128,6 +146,10 @@ public interface IGC : IContract HandleType[] GetHandleTypes(uint[] types) => throw new NotImplementedException(); void GetGlobalAllocationContext(out TargetPointer allocPtr, out TargetPointer allocLimit) => throw new NotImplementedException(); + + IReadOnlyList GetHandleTableMemoryRegions() => throw new NotImplementedException(); + IReadOnlyList GetGCBookkeepingMemoryRegions() => throw new NotImplementedException(); + IReadOnlyList GetGCFreeRegions() => throw new NotImplementedException(); } public readonly struct GC : IGC diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/DataType.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/DataType.cs index 8507623b077c59..c863a679a17865 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/DataType.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/DataType.cs @@ -157,4 +157,6 @@ public enum DataType HandleTableBucket, HandleTable, TableSegment, + CardTableInfo, + RegionFreeList, } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Constants.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Constants.cs index 9e5dc03cc874f7..b79fef13e6d46e 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Constants.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Constants.cs @@ -139,6 +139,15 @@ public static class Globals public const string HandlesPerBlock = nameof(HandlesPerBlock); public const string BlockInvalid = nameof(BlockInvalid); public const string TotalCpuCount = nameof(TotalCpuCount); + public const string HandleSegmentSize = nameof(HandleSegmentSize); + public const string CardTableInfoSize = nameof(CardTableInfoSize); + public const string CountFreeRegionKinds = nameof(CountFreeRegionKinds); + public const string GlobalFreeHugeRegions = nameof(GlobalFreeHugeRegions); + public const string GlobalRegionsToDecommit = nameof(GlobalRegionsToDecommit); + public const string BookkeepingStart = nameof(BookkeepingStart); + public const string GCHeapFreeableSohSegment = nameof(GCHeapFreeableSohSegment); + public const string GCHeapFreeableUohSegment = nameof(GCHeapFreeableUohSegment); + public const string GCHeapFreeRegions = nameof(GCHeapFreeRegions); } public static class FieldNames { diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GC/GCHeapWKS.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GC/GCHeapWKS.cs index 412c6c71a620cb..99e9ee879cdd4a 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GC/GCHeapWKS.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GC/GCHeapWKS.cs @@ -32,6 +32,13 @@ public GCHeapWKS(Target target) CompactReasons = target.ReadGlobalPointer(Constants.Globals.GCHeapCompactReasons); ExpandMechanisms = target.ReadGlobalPointer(Constants.Globals.GCHeapExpandMechanisms); InterestingMechanismBits = target.ReadGlobalPointer(Constants.Globals.GCHeapInterestingMechanismBits); + + if (target.TryReadGlobalPointer(Constants.Globals.GCHeapFreeableSohSegment, out TargetPointer? freeableSohSegPtr)) + FreeableSohSegment = target.ReadPointer(freeableSohSegPtr.Value); + if (target.TryReadGlobalPointer(Constants.Globals.GCHeapFreeableUohSegment, out TargetPointer? freeableUohSegPtr)) + FreeableUohSegment = target.ReadPointer(freeableUohSegPtr.Value); + if (target.TryReadGlobalPointer(Constants.Globals.GCHeapFreeRegions, out TargetPointer? freeRegionsPtr)) + FreeRegions = freeRegionsPtr.Value; } public TargetPointer MarkArray { get; } @@ -57,4 +64,8 @@ public GCHeapWKS(Target target) public TargetPointer CompactReasons { get; } public TargetPointer ExpandMechanisms { get; } public TargetPointer InterestingMechanismBits { get; } + + public TargetPointer? FreeableSohSegment { get; } + public TargetPointer? FreeableUohSegment { get; } + public TargetPointer? FreeRegions { get; } } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GC/GC_1.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GC/GC_1.cs index 935a224cebf7ce..eca0da22703bc0 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GC/GC_1.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GC/GC_1.cs @@ -494,4 +494,229 @@ private HandleData CreateHandleData(TargetPointer handleAddress, byte uBlock, ui return handleData; } + + IReadOnlyList IGC.GetHandleTableMemoryRegions() + { + List regions = new(); + uint handleSegmentSize = _target.ReadGlobal(Constants.Globals.HandleSegmentSize); + uint tableCount = GetGCType() switch + { + GCType.Workstation => 1, + GCType.Server => _target.Read(_target.ReadGlobalPointer(Constants.Globals.TotalCpuCount)), + _ => 0 + }; + + int maxRegions = 8192; + TargetPointer handleTableMap = _target.ReadGlobalPointer(Constants.Globals.HandleTableMap); + while (handleTableMap != TargetPointer.Null && maxRegions >= 0) + { + Data.HandleTableMap map = _target.ProcessedData.GetOrAdd(handleTableMap); + foreach (TargetPointer bucketPtr in map.BucketsPtr) + { + if (bucketPtr == TargetPointer.Null) + continue; + + Data.HandleTableBucket bucket = _target.ProcessedData.GetOrAdd(bucketPtr); + for (uint j = 0; j < tableCount; j++) + { + TargetPointer handleTablePtr = _target.ReadPointer(bucket.Table + (ulong)(j * _target.PointerSize)); + if (handleTablePtr == TargetPointer.Null) + continue; + + Data.HandleTable handleTable = _target.ProcessedData.GetOrAdd(handleTablePtr); + if (handleTable.SegmentList == TargetPointer.Null) + continue; + + TargetPointer segmentPtr = handleTable.SegmentList; + TargetPointer firstSegment = segmentPtr; + do + { + Data.TableSegment segment = _target.ProcessedData.GetOrAdd(segmentPtr); + regions.Add(new GCMemoryRegionData + { + Start = segmentPtr, + Size = handleSegmentSize, + Heap = (int)j, + }); + segmentPtr = segment.NextSegment; + maxRegions--; + } while (segmentPtr != TargetPointer.Null && segmentPtr != firstSegment && maxRegions >= 0); + } + } + handleTableMap = map.Next; + maxRegions--; + } + + return regions; + } + + IReadOnlyList IGC.GetGCBookkeepingMemoryRegions() + { + List regions = new(); + + if (!_target.TryReadGlobalPointer(Constants.Globals.BookkeepingStart, out TargetPointer? bookkeepingStartGlobal)) + return regions; + + TargetPointer bookkeepingStart = _target.ReadPointer(bookkeepingStartGlobal.Value); + if (bookkeepingStart == TargetPointer.Null) + return regions; + + uint cardTableInfoSize = _target.ReadGlobal(Constants.Globals.CardTableInfoSize); + Data.CardTableInfo cardTableInfo = _target.ProcessedData.GetOrAdd(bookkeepingStart); + + if (cardTableInfo.Recount != 0 && cardTableInfo.Size.Value != 0) + { + regions.Add(new GCMemoryRegionData + { + Start = bookkeepingStart, + Size = cardTableInfo.Size.Value, + }); + } + + TargetPointer next = cardTableInfo.NextCardTable; + TargetPointer firstNext = next; + int maxRegions = 32; + + while (next != TargetPointer.Null && next > cardTableInfoSize && maxRegions > 0) + { + TargetPointer ctAddr = next - cardTableInfoSize; + Data.CardTableInfo ct = _target.ProcessedData.GetOrAdd(ctAddr); + + if (ct.Recount != 0 && ct.Size.Value != 0) + { + regions.Add(new GCMemoryRegionData + { + Start = ctAddr, + Size = ct.Size.Value, + }); + } + + next = ct.NextCardTable; + if (next == firstNext) + break; + + maxRegions--; + } + + return regions; + } + + IReadOnlyList IGC.GetGCFreeRegions() + { + List regions = new(); + + int countFreeRegionKinds = (int)_target.ReadGlobal(Constants.Globals.CountFreeRegionKinds); + countFreeRegionKinds = Math.Min(countFreeRegionKinds, 16); + uint regionFreeListSize = _target.GetTypeInfo(DataType.RegionFreeList).Size + ?? throw new InvalidOperationException("RegionFreeList type has no size"); + + // Global free huge regions + if (_target.TryReadGlobalPointer(Constants.Globals.GlobalFreeHugeRegions, out TargetPointer? globalFreeHugePtr)) + { + AddFreeList(globalFreeHugePtr.Value, FreeRegionKind.FreeGlobalHugeRegion, regions); + } + + // Global regions to decommit + if (_target.TryReadGlobalPointer(Constants.Globals.GlobalRegionsToDecommit, out TargetPointer? globalDecommitPtr)) + { + for (int i = 0; i < countFreeRegionKinds; i++) + { + TargetPointer listAddr = globalDecommitPtr.Value + (ulong)(i * regionFreeListSize); + AddFreeList(listAddr, FreeRegionKind.FreeGlobalRegion, regions); + } + } + + if (GetGCType() == GCType.Server) + { + uint heapCount = ((IGC)this).GetGCHeapCount(); + TargetPointer heapTable = _target.ReadPointer(_target.ReadGlobalPointer(Constants.Globals.Heaps)); + for (uint i = 0; i < heapCount; i++) + { + TargetPointer heapAddress = _target.ReadPointer(heapTable + (i * (uint)_target.PointerSize)); + if (heapAddress == TargetPointer.Null) + continue; + + Data.GCHeapSVR heap = _target.ProcessedData.GetOrAdd(heapAddress); + + if (heap.FreeRegions is TargetPointer freeRegionsBase) + { + for (int j = 0; j < countFreeRegionKinds; j++) + { + TargetPointer listAddr = freeRegionsBase + (ulong)(j * regionFreeListSize); + AddFreeList(listAddr, FreeRegionKind.FreeRegion, regions, (int)i); + } + } + + if (heap.FreeableSohSegment is TargetPointer freeableSoh && freeableSoh != TargetPointer.Null) + AddSegmentList(freeableSoh, FreeRegionKind.FreeSohSegment, regions, (int)i); + + if (heap.FreeableUohSegment is TargetPointer freeableUoh && freeableUoh != TargetPointer.Null) + AddSegmentList(freeableUoh, FreeRegionKind.FreeUohSegment, regions, (int)i); + } + } + else + { + // Workstation GC: free regions from globals + if (_target.TryReadGlobalPointer(Constants.Globals.GCHeapFreeRegions, out TargetPointer? freeRegionsPtr)) + { + for (int i = 0; i < countFreeRegionKinds; i++) + { + TargetPointer listAddr = freeRegionsPtr.Value + (ulong)(i * regionFreeListSize); + AddFreeList(listAddr, FreeRegionKind.FreeRegion, regions); + } + } + + if (_target.TryReadGlobalPointer(Constants.Globals.GCHeapFreeableSohSegment, out TargetPointer? freeableSohPtr)) + { + TargetPointer segPtr = _target.ReadPointer(freeableSohPtr.Value); + if (segPtr != TargetPointer.Null) + AddSegmentList(segPtr, FreeRegionKind.FreeSohSegment, regions); + } + + if (_target.TryReadGlobalPointer(Constants.Globals.GCHeapFreeableUohSegment, out TargetPointer? freeableUohPtr)) + { + TargetPointer segPtr = _target.ReadPointer(freeableUohPtr.Value); + if (segPtr != TargetPointer.Null) + AddSegmentList(segPtr, FreeRegionKind.FreeUohSegment, regions); + } + } + + return regions; + } + + private void AddFreeList(TargetPointer freeListAddr, FreeRegionKind kind, List regions, int heap = 0) + { + Data.RegionFreeList freeList = _target.ProcessedData.GetOrAdd(freeListAddr); + if (freeList.HeadFreeRegion != TargetPointer.Null) + AddSegmentList(freeList.HeadFreeRegion, kind, regions, heap); + } + + private void AddSegmentList(TargetPointer start, FreeRegionKind kind, List regions, int heap = 0) + { + int iterationMax = 2048; + TargetPointer curr = start; + while (curr != TargetPointer.Null) + { + Data.HeapSegment segment = _target.ProcessedData.GetOrAdd(curr); + if (segment.Mem != TargetPointer.Null) + { + ulong size = 0; + if (segment.Mem < segment.Committed) + size = segment.Committed - segment.Mem; + regions.Add(new GCMemoryRegionData + { + Start = segment.Mem, + Size = size, + ExtraData = (ulong)kind, + Heap = heap, + }); + } + + curr = segment.Next; + if (curr == start) + break; + if (--iterationMax <= 0) + break; + } + } } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GC/IGCHeap.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GC/IGCHeap.cs index cab7d0ee4d9f1c..f7e6dc1cc9ac77 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GC/IGCHeap.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GC/IGCHeap.cs @@ -28,4 +28,8 @@ internal interface IGCHeap TargetPointer CompactReasons { get; } TargetPointer ExpandMechanisms { get; } TargetPointer InterestingMechanismBits { get; } + + TargetPointer? FreeableSohSegment { get; } + TargetPointer? FreeableUohSegment { get; } + TargetPointer? FreeRegions { get; } } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/CardTableInfo.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/CardTableInfo.cs new file mode 100644 index 00000000000000..360cd94915c2cb --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/CardTableInfo.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Diagnostics.DataContractReader.Data; + +internal sealed class CardTableInfo : IData +{ + static CardTableInfo IData.Create(Target target, TargetPointer address) + => new CardTableInfo(target, address); + + public CardTableInfo(Target target, TargetPointer address) + { + Target.TypeInfo type = target.GetTypeInfo(DataType.CardTableInfo); + Recount = target.Read(address + (ulong)type.Fields[nameof(Recount)].Offset); + Size = target.ReadNUInt(address + (ulong)type.Fields[nameof(Size)].Offset); + NextCardTable = target.ReadPointer(address + (ulong)type.Fields[nameof(NextCardTable)].Offset); + } + + public uint Recount { get; init; } + public TargetNUInt Size { get; init; } + public TargetPointer NextCardTable { get; init; } +} diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/GC/GCHeapSVR.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/GC/GCHeapSVR.cs index c3f7520bb4da8c..b6f4743149844e 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/GC/GCHeapSVR.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/GC/GCHeapSVR.cs @@ -40,6 +40,13 @@ public GCHeapSVR(Target target, TargetPointer address) CompactReasons = address + (ulong)type.Fields[nameof(CompactReasons)].Offset; ExpandMechanisms = address + (ulong)type.Fields[nameof(ExpandMechanisms)].Offset; InterestingMechanismBits = address + (ulong)type.Fields[nameof(InterestingMechanismBits)].Offset; + + if (type.Fields.ContainsKey(nameof(FreeableSohSegment))) + FreeableSohSegment = target.ReadPointer(address + (ulong)type.Fields[nameof(FreeableSohSegment)].Offset); + if (type.Fields.ContainsKey(nameof(FreeableUohSegment))) + FreeableUohSegment = target.ReadPointer(address + (ulong)type.Fields[nameof(FreeableUohSegment)].Offset); + if (type.Fields.ContainsKey(nameof(FreeRegions))) + FreeRegions = address + (ulong)type.Fields[nameof(FreeRegions)].Offset; } public TargetPointer MarkArray { get; } @@ -65,4 +72,8 @@ public GCHeapSVR(Target target, TargetPointer address) public TargetPointer CompactReasons { get; } public TargetPointer ExpandMechanisms { get; } public TargetPointer InterestingMechanismBits { get; } + + public TargetPointer? FreeableSohSegment { get; } + public TargetPointer? FreeableUohSegment { get; } + public TargetPointer? FreeRegions { get; } } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/RegionFreeList.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/RegionFreeList.cs new file mode 100644 index 00000000000000..d31afbc0bf459d --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/RegionFreeList.cs @@ -0,0 +1,18 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Diagnostics.DataContractReader.Data; + +internal sealed class RegionFreeList : IData +{ + static RegionFreeList IData.Create(Target target, TargetPointer address) + => new RegionFreeList(target, address); + + public RegionFreeList(Target target, TargetPointer address) + { + Target.TypeInfo type = target.GetTypeInfo(DataType.RegionFreeList); + HeadFreeRegion = target.ReadPointer(address + (ulong)type.Fields[nameof(HeadFreeRegion)].Offset); + } + + public TargetPointer HeadFreeRegion { get; init; } +} diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/ISOSDacInterface.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/ISOSDacInterface.cs index c775b62dddd4af..304b74da3b83c0 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/ISOSDacInterface.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/ISOSDacInterface.cs @@ -931,6 +931,22 @@ public unsafe partial interface ISOSDacInterface12 int GetGlobalAllocationContext(ClrDataAddress* allocPtr, ClrDataAddress* allocLimit); } +public struct SOSMemoryRegion +{ + public ClrDataAddress Start; + public ClrDataAddress Size; + public ClrDataAddress ExtraData; + public int Heap; +} + +[GeneratedComInterface] +[Guid("E4B860EC-337A-40C0-A591-F09A9680690F")] +public unsafe partial interface ISOSMemoryEnum : ISOSEnum +{ + [PreserveSig] + int Next(uint count, [In, Out, MarshalUsing(CountElementName = nameof(count))] SOSMemoryRegion[] memRegion, uint* pNeeded); +} + [GeneratedComInterface] [Guid("3176a8ed-597b-4f54-a71f-83695c6a8c5e")] public unsafe partial interface ISOSDacInterface13 @@ -944,11 +960,11 @@ public unsafe partial interface ISOSDacInterface13 [PreserveSig] int GetLoaderAllocatorHeaps(ClrDataAddress loaderAllocator, int count, ClrDataAddress* pLoaderHeaps, /*LoaderHeapKind*/ int* pKinds, int* pNeeded); [PreserveSig] - int GetHandleTableMemoryRegions(/*ISOSMemoryEnum*/ void** ppEnum); + int GetHandleTableMemoryRegions(out ISOSMemoryEnum? ppEnum); [PreserveSig] - int GetGCBookkeepingMemoryRegions(/*ISOSMemoryEnum*/ void** ppEnum); + int GetGCBookkeepingMemoryRegions(out ISOSMemoryEnum? ppEnum); [PreserveSig] - int GetGCFreeRegions(/*ISOSMemoryEnum*/ void** ppEnum); + int GetGCFreeRegions(out ISOSMemoryEnum? ppEnum); [PreserveSig] int LockedFlush(); } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.cs index 3fda278dc5e08e..69d038bf914eac 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.cs @@ -1493,6 +1493,63 @@ int ISOSEnum.GetCount(uint* pCount) } } + [GeneratedComClass] + internal sealed unsafe partial class SOSMemoryEnum : ISOSMemoryEnum + { + private readonly SOSMemoryRegion[] _regions; + private uint _index; + + public SOSMemoryEnum(Target target, IReadOnlyList regions) + { + _regions = new SOSMemoryRegion[regions.Count]; + for (int i = 0; i < regions.Count; i++) + { + _regions[i] = new SOSMemoryRegion + { + Start = regions[i].Start.ToClrDataAddress(target), + Size = (ClrDataAddress)regions[i].Size, + ExtraData = (ClrDataAddress)regions[i].ExtraData, + Heap = regions[i].Heap, + }; + } + } + + int ISOSMemoryEnum.Next(uint count, SOSMemoryRegion[] memRegion, uint* pNeeded) + { + if (pNeeded is null) + return HResults.E_POINTER; + if (memRegion is null) + return HResults.E_POINTER; + + uint written = 0; + while (written < count && _index < _regions.Length) + memRegion[written++] = _regions[(int)_index++]; + + *pNeeded = written; + return _index < _regions.Length ? HResults.S_FALSE : HResults.S_OK; + } + + int ISOSEnum.Skip(uint count) + { + _index += count; + return HResults.S_OK; + } + + int ISOSEnum.Reset() + { + _index = 0; + return HResults.S_OK; + } + + int ISOSEnum.GetCount(uint* pCount) + { + if (pCount is null) + return HResults.E_POINTER; + *pCount = (uint)_regions.Length; + return HResults.S_OK; + } + } + int ISOSDacInterface.GetHandleEnum(out ISOSHandleEnum? ppHandleEnum) { int hr = HResults.S_OK; @@ -4817,12 +4874,75 @@ int ISOSDacInterface13.GetLoaderAllocatorHeapNames(int count, char** ppNames, in => _legacyImpl13 is not null ? _legacyImpl13.GetLoaderAllocatorHeapNames(count, ppNames, pNeeded) : HResults.E_NOTIMPL; int ISOSDacInterface13.GetLoaderAllocatorHeaps(ClrDataAddress loaderAllocator, int count, ClrDataAddress* pLoaderHeaps, /*LoaderHeapKind*/ int* pKinds, int* pNeeded) => _legacyImpl13 is not null ? _legacyImpl13.GetLoaderAllocatorHeaps(loaderAllocator, count, pLoaderHeaps, pKinds, pNeeded) : HResults.E_NOTIMPL; - int ISOSDacInterface13.GetHandleTableMemoryRegions(/*ISOSMemoryEnum*/ void** ppEnum) - => _legacyImpl13 is not null ? _legacyImpl13.GetHandleTableMemoryRegions(ppEnum) : HResults.E_NOTIMPL; - int ISOSDacInterface13.GetGCBookkeepingMemoryRegions(/*ISOSMemoryEnum*/ void** ppEnum) - => _legacyImpl13 is not null ? _legacyImpl13.GetGCBookkeepingMemoryRegions(ppEnum) : HResults.E_NOTIMPL; - int ISOSDacInterface13.GetGCFreeRegions(/*ISOSMemoryEnum*/ void** ppEnum) - => _legacyImpl13 is not null ? _legacyImpl13.GetGCFreeRegions(ppEnum) : HResults.E_NOTIMPL; + int ISOSDacInterface13.GetHandleTableMemoryRegions(out ISOSMemoryEnum? ppEnum) + { + ppEnum = null; + int hr = HResults.S_OK; + try + { + IReadOnlyList regions = _target.Contracts.GC.GetHandleTableMemoryRegions(); + ppEnum = new SOSMemoryEnum(_target, regions); + } + catch (System.Exception e) + { + hr = e.HResult; + } + +#if DEBUG + if (_legacyImpl13 is not null) + { + int hrLocal = _legacyImpl13.GetHandleTableMemoryRegions(out _); + Debug.Assert(hrLocal == hr, $"cDAC: {hr:x}, DAC: {hrLocal:x}"); + } +#endif + return hr; + } + int ISOSDacInterface13.GetGCBookkeepingMemoryRegions(out ISOSMemoryEnum? ppEnum) + { + ppEnum = null; + int hr = HResults.S_OK; + try + { + IReadOnlyList regions = _target.Contracts.GC.GetGCBookkeepingMemoryRegions(); + ppEnum = new SOSMemoryEnum(_target, regions); + } + catch (System.Exception e) + { + hr = e.HResult; + } + +#if DEBUG + if (_legacyImpl13 is not null) + { + int hrLocal = _legacyImpl13.GetGCBookkeepingMemoryRegions(out _); + Debug.Assert(hrLocal == hr, $"cDAC: {hr:x}, DAC: {hrLocal:x}"); + } +#endif + return hr; + } + int ISOSDacInterface13.GetGCFreeRegions(out ISOSMemoryEnum? ppEnum) + { + ppEnum = null; + int hr = HResults.S_OK; + try + { + IReadOnlyList regions = _target.Contracts.GC.GetGCFreeRegions(); + ppEnum = new SOSMemoryEnum(_target, regions); + } + catch (System.Exception e) + { + hr = e.HResult; + } + +#if DEBUG + if (_legacyImpl13 is not null) + { + int hrLocal = _legacyImpl13.GetGCFreeRegions(out _); + Debug.Assert(hrLocal == hr, $"cDAC: {hr:x}, DAC: {hrLocal:x}"); + } +#endif + return hr; + } int ISOSDacInterface13.LockedFlush() => _legacyImpl13 is not null ? _legacyImpl13.LockedFlush() : HResults.E_NOTIMPL; #endregion ISOSDacInterface13 diff --git a/src/native/managed/cdac/tests/DumpTests/ServerGCDumpTests.cs b/src/native/managed/cdac/tests/DumpTests/ServerGCDumpTests.cs index 2a00258b14af7f..9b7f69b8d61520 100644 --- a/src/native/managed/cdac/tests/DumpTests/ServerGCDumpTests.cs +++ b/src/native/managed/cdac/tests/DumpTests/ServerGCDumpTests.cs @@ -117,6 +117,59 @@ public void ServerGC_EachHeapHasGenerationData(TestConfiguration config) } } + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnVersion("net10.0", "GC contract is not available in .NET 10 dumps")] + public void ServerGC_GetHandleTableMemoryRegions(TestConfiguration config) + { + InitializeDumpTest(config); + IGC gcContract = Target.Contracts.GC; + + IReadOnlyList regions = gcContract.GetHandleTableMemoryRegions(); + Assert.NotNull(regions); + Assert.True(regions.Count > 0, "Expected at least one handle table memory region"); + Assert.All(regions, region => + { + Assert.NotEqual(TargetPointer.Null, region.Start); + Assert.True(region.Size > 0, $"Expected non-zero size for region starting at 0x{region.Start:X}"); + }); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnVersion("net10.0", "GC contract is not available in .NET 10 dumps")] + public void ServerGC_GetGCBookkeepingMemoryRegions(TestConfiguration config) + { + InitializeDumpTest(config); + IGC gcContract = Target.Contracts.GC; + + IReadOnlyList regions = gcContract.GetGCBookkeepingMemoryRegions(); + Assert.NotNull(regions); + Assert.True(regions.Count > 0, "Expected at least one bookkeeping memory region"); + Assert.All(regions, region => + { + Assert.NotEqual(TargetPointer.Null, region.Start); + Assert.True(region.Size > 0, $"Expected non-zero size for region starting at 0x{region.Start:X}"); + }); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnVersion("net10.0", "GC contract is not available in .NET 10 dumps")] + public void ServerGC_GetGCFreeRegions(TestConfiguration config) + { + InitializeDumpTest(config); + IGC gcContract = Target.Contracts.GC; + + IReadOnlyList regions = gcContract.GetGCFreeRegions(); + Assert.NotNull(regions); + Assert.All(regions, region => + { + Assert.NotEqual(TargetPointer.Null, region.Start); + Assert.True(region.Size > 0, $"Expected non-zero size for region starting at 0x{region.Start:X}"); + }); + } + [ConditionalTheory] [MemberData(nameof(TestConfigurations))] [SkipOnVersion("net10.0", "GC contract is not available in .NET 10 dumps")] diff --git a/src/native/managed/cdac/tests/DumpTests/WorkstationGCDumpTests.cs b/src/native/managed/cdac/tests/DumpTests/WorkstationGCDumpTests.cs index 8045d9dd0e97d0..98a1c71d7d1dfd 100644 --- a/src/native/managed/cdac/tests/DumpTests/WorkstationGCDumpTests.cs +++ b/src/native/managed/cdac/tests/DumpTests/WorkstationGCDumpTests.cs @@ -1,6 +1,7 @@ // 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.DataContractReader.Contracts; using Xunit; @@ -139,4 +140,57 @@ public void WorkstationGC_GlobalAllocationContextIsReadable(TestConfiguration co $"Expected allocPtr (0x{pointer:X}) <= allocLimit (0x{limit:X})"); } } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnVersion("net10.0", "GC contract is not available in .NET 10 dumps")] + public void WorkstationGC_GetHandleTableMemoryRegions(TestConfiguration config) + { + InitializeDumpTest(config); + IGC gcContract = Target.Contracts.GC; + + IReadOnlyList regions = gcContract.GetHandleTableMemoryRegions(); + Assert.NotNull(regions); + Assert.True(regions.Count > 0, "Expected at least one handle table memory region"); + Assert.All(regions, region => + { + Assert.NotEqual(TargetPointer.Null, region.Start); + Assert.True(region.Size > 0, $"Expected non-zero size for region starting at 0x{region.Start:X}"); + }); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnVersion("net10.0", "GC contract is not available in .NET 10 dumps")] + public void WorkstationGC_GetGCBookkeepingMemoryRegions(TestConfiguration config) + { + InitializeDumpTest(config); + IGC gcContract = Target.Contracts.GC; + + IReadOnlyList regions = gcContract.GetGCBookkeepingMemoryRegions(); + Assert.NotNull(regions); + Assert.True(regions.Count > 0, "Expected at least one bookkeeping memory region"); + Assert.All(regions, region => + { + Assert.NotEqual(TargetPointer.Null, region.Start); + Assert.True(region.Size > 0, $"Expected non-zero size for region starting at 0x{region.Start:X}"); + }); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnVersion("net10.0", "GC contract is not available in .NET 10 dumps")] + public void WorkstationGC_GetGCFreeRegions(TestConfiguration config) + { + InitializeDumpTest(config); + IGC gcContract = Target.Contracts.GC; + + IReadOnlyList regions = gcContract.GetGCFreeRegions(); + Assert.NotNull(regions); + Assert.All(regions, region => + { + Assert.NotEqual(TargetPointer.Null, region.Start); + Assert.True(region.Size > 0, $"Expected non-zero size for region starting at 0x{region.Start:X}"); + }); + } } diff --git a/src/native/managed/cdac/tests/GCMemoryRegionTests.cs b/src/native/managed/cdac/tests/GCMemoryRegionTests.cs new file mode 100644 index 00000000000000..31fc522a450de8 --- /dev/null +++ b/src/native/managed/cdac/tests/GCMemoryRegionTests.cs @@ -0,0 +1,474 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using Microsoft.Diagnostics.DataContractReader.Contracts; +using Moq; +using Xunit; + +namespace Microsoft.Diagnostics.DataContractReader.Tests; + +public class GCMemoryRegionTests +{ + private const uint HandleSegmentSize = 0x10000; + private const uint InitialHandleTableArraySize = 2; + private const uint HandleBlocksPerSegment = 64; + private const uint HandleMaxInternalTypes = 12; + private const uint HandlesPerBlock = 256; + private const byte BlockInvalid = 0xFF; + private const uint CountFreeRegionKinds = 3; + + private static IGC CreateGCContract(MockTarget.Architecture arch, + Dictionary types, + (string Name, ulong Value)[] globals, + (string Name, string Value)[] globalStrings, + TestPlaceholderTarget.ReadFromTargetDelegate readFromTarget) + { + var target = new TestPlaceholderTarget(arch, readFromTarget, types, globals, globalStrings); + var gcContract = ((IContractFactory)new GCFactory()).CreateContract(target, 1); + target.SetContracts(Mock.Of( + c => c.GC == gcContract)); + return gcContract; + } + + private static (string Name, ulong Value)[] BuildGlobals( + Dictionary pointerGlobals, + Dictionary valueGlobals) + { + var result = new List<(string Name, ulong Value)>(); + + result.Add((Constants.Globals.HandlesPerBlock, HandlesPerBlock)); + result.Add((Constants.Globals.BlockInvalid, BlockInvalid)); + result.Add((Constants.Globals.DebugDestroyedHandleValue, 0)); + result.Add((Constants.Globals.HandleMaxInternalTypes, HandleMaxInternalTypes)); + result.Add((Constants.Globals.HandleSegmentSize, HandleSegmentSize)); + result.Add((Constants.Globals.InitialHandleTableArraySize, InitialHandleTableArraySize)); + result.Add((Constants.Globals.HandleBlocksPerSegment, HandleBlocksPerSegment)); + result.Add((Constants.Globals.CountFreeRegionKinds, CountFreeRegionKinds)); + + foreach (var (name, value) in valueGlobals) + result.Add((name, value)); + foreach (var (name, value) in pointerGlobals) + result.Add((name, value)); + + return result.ToArray(); + } + + private static Dictionary MakeFields(params (string Name, int Offset, DataType Type)[] fields) + { + var result = new Dictionary(); + foreach (var (name, offset, type) in fields) + result[name] = new Target.FieldInfo { Offset = offset, Type = type }; + return result; + } + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void GetHandleTableMemoryRegions_SingleSegment(MockTarget.Architecture arch) + { + TargetTestHelpers helpers = new(arch); + MockMemorySpace.Builder builder = new(helpers); + MockMemorySpace.BumpAllocator allocator = builder.CreateAllocator(0x0010_0000, 0x0100_0000); + int ptrSize = helpers.PointerSize; + + var types = new Dictionary + { + [DataType.HandleTableMap] = new() { Fields = MakeFields(("BucketsPtr", 0, DataType.pointer), ("Next", ptrSize, DataType.pointer)) }, + [DataType.HandleTableBucket] = new() { Fields = MakeFields(("Table", 0, DataType.pointer)) }, + [DataType.HandleTable] = new() { Fields = MakeFields(("SegmentList", 0, DataType.pointer)) }, + [DataType.TableSegment] = new() { Fields = MakeFields( + ("NextSegment", 0, DataType.pointer), + ("RgAllocation", ptrSize, DataType.uint8), + ("RgTail", ptrSize + (int)HandleBlocksPerSegment, DataType.uint8), + ("RgValue", ptrSize + (int)HandleBlocksPerSegment + (int)HandleMaxInternalTypes, DataType.pointer), + ("RgUserData", ptrSize + (int)HandleBlocksPerSegment + (int)HandleMaxInternalTypes + ptrSize, DataType.uint8)) }, + }; + + var segFragment = allocator.Allocate(HandleSegmentSize, "Segment"); + builder.AddHeapFragment(segFragment); + Span segSpan = builder.BorrowAddressRange(segFragment.Address, segFragment.Data.Length); + helpers.WritePointer(segSpan.Slice(0, ptrSize), TargetPointer.Null); + + var htFragment = allocator.Allocate((ulong)ptrSize, "HandleTable"); + builder.AddHeapFragment(htFragment); + Span htSpan = builder.BorrowAddressRange(htFragment.Address, htFragment.Data.Length); + helpers.WritePointer(htSpan.Slice(0, ptrSize), segFragment.Address); + + var bucketTableFragment = allocator.Allocate((ulong)ptrSize, "BucketTable"); + builder.AddHeapFragment(bucketTableFragment); + Span btSpan = builder.BorrowAddressRange(bucketTableFragment.Address, bucketTableFragment.Data.Length); + helpers.WritePointer(btSpan.Slice(0, ptrSize), htFragment.Address); + + var bucketFragment = allocator.Allocate((ulong)ptrSize, "Bucket"); + builder.AddHeapFragment(bucketFragment); + Span bSpan = builder.BorrowAddressRange(bucketFragment.Address, bucketFragment.Data.Length); + helpers.WritePointer(bSpan.Slice(0, ptrSize), bucketTableFragment.Address); + + var bucketsArrayFragment = allocator.Allocate((ulong)(InitialHandleTableArraySize * ptrSize), "BucketsArray"); + builder.AddHeapFragment(bucketsArrayFragment); + Span baSpan = builder.BorrowAddressRange(bucketsArrayFragment.Address, bucketsArrayFragment.Data.Length); + helpers.WritePointer(baSpan.Slice(0, ptrSize), bucketFragment.Address); + helpers.WritePointer(baSpan.Slice(ptrSize, ptrSize), TargetPointer.Null); + + var mapFragment = allocator.Allocate((ulong)(2 * ptrSize), "Map"); + builder.AddHeapFragment(mapFragment); + Span mapSpan = builder.BorrowAddressRange(mapFragment.Address, mapFragment.Data.Length); + helpers.WritePointer(mapSpan.Slice(0, ptrSize), bucketsArrayFragment.Address); + helpers.WritePointer(mapSpan.Slice(ptrSize, ptrSize), TargetPointer.Null); + + var globals = BuildGlobals( + new() { [Constants.Globals.HandleTableMap] = mapFragment.Address }, + new()); + + var readContext = builder.GetMemoryContext(); + IGC gc = CreateGCContract(arch, types, globals, + [(Constants.Globals.GCIdentifiers, "workstation,regions,background")], + readContext.ReadFromTarget); + + IReadOnlyList regions = gc.GetHandleTableMemoryRegions(); + + Assert.Single(regions); + Assert.Equal(new TargetPointer(segFragment.Address), regions[0].Start); + Assert.Equal((ulong)HandleSegmentSize, regions[0].Size); + } + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void GetHandleTableMemoryRegions_AllNullBuckets_ReturnsEmpty(MockTarget.Architecture arch) + { + TargetTestHelpers helpers = new(arch); + MockMemorySpace.Builder builder = new(helpers); + MockMemorySpace.BumpAllocator allocator = builder.CreateAllocator(0x0010_0000, 0x0100_0000); + int ptrSize = helpers.PointerSize; + + var types = new Dictionary + { + [DataType.HandleTableMap] = new() { Fields = MakeFields(("BucketsPtr", 0, DataType.pointer), ("Next", ptrSize, DataType.pointer)) }, + [DataType.HandleTableBucket] = new() { Fields = MakeFields(("Table", 0, DataType.pointer)) }, + [DataType.HandleTable] = new() { Fields = MakeFields(("SegmentList", 0, DataType.pointer)) }, + [DataType.TableSegment] = new() { Fields = MakeFields(("NextSegment", 0, DataType.pointer)) }, + }; + + var bucketsArrayFragment = allocator.Allocate((ulong)(InitialHandleTableArraySize * ptrSize), "BucketsArray"); + builder.AddHeapFragment(bucketsArrayFragment); + Span baSpan = builder.BorrowAddressRange(bucketsArrayFragment.Address, bucketsArrayFragment.Data.Length); + helpers.WritePointer(baSpan.Slice(0, ptrSize), TargetPointer.Null); + helpers.WritePointer(baSpan.Slice(ptrSize, ptrSize), TargetPointer.Null); + + var mapFragment = allocator.Allocate((ulong)(2 * ptrSize), "Map"); + builder.AddHeapFragment(mapFragment); + Span mapSpan = builder.BorrowAddressRange(mapFragment.Address, mapFragment.Data.Length); + helpers.WritePointer(mapSpan.Slice(0, ptrSize), bucketsArrayFragment.Address); + helpers.WritePointer(mapSpan.Slice(ptrSize, ptrSize), TargetPointer.Null); + + var globals = BuildGlobals( + new() { [Constants.Globals.HandleTableMap] = mapFragment.Address }, + new()); + + var readContext = builder.GetMemoryContext(); + IGC gc = CreateGCContract(arch, types, globals, + [(Constants.Globals.GCIdentifiers, "workstation,regions,background")], + readContext.ReadFromTarget); + + Assert.Empty(gc.GetHandleTableMemoryRegions()); + } + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void GetHandleTableMemoryRegions_MultipleLinkedSegments(MockTarget.Architecture arch) + { + TargetTestHelpers helpers = new(arch); + MockMemorySpace.Builder builder = new(helpers); + MockMemorySpace.BumpAllocator allocator = builder.CreateAllocator(0x0010_0000, 0x0100_0000); + int ptrSize = helpers.PointerSize; + + var types = new Dictionary + { + [DataType.HandleTableMap] = new() { Fields = MakeFields(("BucketsPtr", 0, DataType.pointer), ("Next", ptrSize, DataType.pointer)) }, + [DataType.HandleTableBucket] = new() { Fields = MakeFields(("Table", 0, DataType.pointer)) }, + [DataType.HandleTable] = new() { Fields = MakeFields(("SegmentList", 0, DataType.pointer)) }, + [DataType.TableSegment] = new() { Fields = MakeFields( + ("NextSegment", 0, DataType.pointer), + ("RgAllocation", ptrSize, DataType.uint8), + ("RgTail", ptrSize + (int)HandleBlocksPerSegment, DataType.uint8), + ("RgValue", ptrSize + (int)HandleBlocksPerSegment + (int)HandleMaxInternalTypes, DataType.pointer), + ("RgUserData", ptrSize + (int)HandleBlocksPerSegment + (int)HandleMaxInternalTypes + ptrSize, DataType.uint8)) }, + }; + + var seg2Fragment = allocator.Allocate(HandleSegmentSize, "Seg2"); + builder.AddHeapFragment(seg2Fragment); + Span seg2Span = builder.BorrowAddressRange(seg2Fragment.Address, seg2Fragment.Data.Length); + helpers.WritePointer(seg2Span.Slice(0, ptrSize), TargetPointer.Null); + + var seg1Fragment = allocator.Allocate(HandleSegmentSize, "Seg1"); + builder.AddHeapFragment(seg1Fragment); + Span seg1Span = builder.BorrowAddressRange(seg1Fragment.Address, seg1Fragment.Data.Length); + helpers.WritePointer(seg1Span.Slice(0, ptrSize), seg2Fragment.Address); + + var htFragment = allocator.Allocate((ulong)ptrSize, "HT"); + builder.AddHeapFragment(htFragment); + Span htSpan = builder.BorrowAddressRange(htFragment.Address, htFragment.Data.Length); + helpers.WritePointer(htSpan.Slice(0, ptrSize), seg1Fragment.Address); + + var btFragment = allocator.Allocate((ulong)ptrSize, "BT"); + builder.AddHeapFragment(btFragment); + Span btSpan = builder.BorrowAddressRange(btFragment.Address, btFragment.Data.Length); + helpers.WritePointer(btSpan.Slice(0, ptrSize), htFragment.Address); + + var bucketFragment = allocator.Allocate((ulong)ptrSize, "Bucket"); + builder.AddHeapFragment(bucketFragment); + Span bucketSpan = builder.BorrowAddressRange(bucketFragment.Address, bucketFragment.Data.Length); + helpers.WritePointer(bucketSpan.Slice(0, ptrSize), btFragment.Address); + + var baFragment = allocator.Allocate((ulong)(InitialHandleTableArraySize * ptrSize), "BA"); + builder.AddHeapFragment(baFragment); + Span baSpan = builder.BorrowAddressRange(baFragment.Address, baFragment.Data.Length); + helpers.WritePointer(baSpan.Slice(0, ptrSize), bucketFragment.Address); + helpers.WritePointer(baSpan.Slice(ptrSize, ptrSize), TargetPointer.Null); + + var mapFragment = allocator.Allocate((ulong)(2 * ptrSize), "Map"); + builder.AddHeapFragment(mapFragment); + Span mapSpan = builder.BorrowAddressRange(mapFragment.Address, mapFragment.Data.Length); + helpers.WritePointer(mapSpan.Slice(0, ptrSize), baFragment.Address); + helpers.WritePointer(mapSpan.Slice(ptrSize, ptrSize), TargetPointer.Null); + + var globals = BuildGlobals( + new() { [Constants.Globals.HandleTableMap] = mapFragment.Address }, + new()); + + var readContext = builder.GetMemoryContext(); + IGC gc = CreateGCContract(arch, types, globals, + [(Constants.Globals.GCIdentifiers, "workstation,regions,background")], + readContext.ReadFromTarget); + + IReadOnlyList regions = gc.GetHandleTableMemoryRegions(); + + Assert.Equal(2, regions.Count); + Assert.Equal(new TargetPointer(seg1Fragment.Address), regions[0].Start); + Assert.Equal(new TargetPointer(seg2Fragment.Address), regions[1].Start); + } + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void GetGCBookkeepingMemoryRegions_SingleEntry(MockTarget.Architecture arch) + { + TargetTestHelpers helpers = new(arch); + MockMemorySpace.Builder builder = new(helpers); + MockMemorySpace.BumpAllocator allocator = builder.CreateAllocator(0x0010_0000, 0x0100_0000); + int ptrSize = helpers.PointerSize; + + uint cardTableTypeSize = (uint)(4 + ptrSize + ptrSize); + var types = new Dictionary + { + [DataType.CardTableInfo] = new() { Fields = MakeFields( + ("Recount", 0, DataType.uint32), + ("Size", 4, DataType.nuint), + ("NextCardTable", 4 + ptrSize, DataType.pointer)), Size = cardTableTypeSize }, + }; + + var ctiFragment = allocator.Allocate(cardTableTypeSize, "CTI"); + builder.AddHeapFragment(ctiFragment); + Span ctiSpan = builder.BorrowAddressRange(ctiFragment.Address, ctiFragment.Data.Length); + helpers.Write(ctiSpan.Slice(0, sizeof(uint)), (uint)1); + if (ptrSize == 8) + helpers.Write(ctiSpan.Slice(4, sizeof(ulong)), (ulong)0x1000); + else + helpers.Write(ctiSpan.Slice(4, sizeof(uint)), (uint)0x1000); + helpers.WritePointer(ctiSpan.Slice(4 + ptrSize, ptrSize), TargetPointer.Null); + + var bkFragment = allocator.Allocate((ulong)ptrSize, "BK"); + builder.AddHeapFragment(bkFragment); + Span bkSpan = builder.BorrowAddressRange(bkFragment.Address, bkFragment.Data.Length); + helpers.WritePointer(bkSpan.Slice(0, ptrSize), ctiFragment.Address); + + var globals = BuildGlobals( + new() { [Constants.Globals.BookkeepingStart] = bkFragment.Address }, + new() { [Constants.Globals.CardTableInfoSize] = cardTableTypeSize }); + + var readContext = builder.GetMemoryContext(); + IGC gc = CreateGCContract(arch, types, globals, + [(Constants.Globals.GCIdentifiers, "workstation,regions,background")], + readContext.ReadFromTarget); + + IReadOnlyList regions = gc.GetGCBookkeepingMemoryRegions(); + + Assert.Single(regions); + Assert.Equal(new TargetPointer(ctiFragment.Address), regions[0].Start); + Assert.Equal((ulong)0x1000, regions[0].Size); + } + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void GetGCBookkeepingMemoryRegions_MissingGlobal_ReturnsEmpty(MockTarget.Architecture arch) + { + TargetTestHelpers helpers = new(arch); + MockMemorySpace.Builder builder = new(helpers); + + var types = new Dictionary(); + var globals = BuildGlobals(new(), new()); + + var readContext = builder.GetMemoryContext(); + IGC gc = CreateGCContract(arch, types, globals, + [(Constants.Globals.GCIdentifiers, "workstation,regions,background")], + readContext.ReadFromTarget); + + Assert.Empty(gc.GetGCBookkeepingMemoryRegions()); + } + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void GetGCFreeRegions_SingleFreeRegion(MockTarget.Architecture arch) + { + TargetTestHelpers helpers = new(arch); + MockMemorySpace.Builder builder = new(helpers); + MockMemorySpace.BumpAllocator allocator = builder.CreateAllocator(0x0010_0000, 0x0100_0000); + int ptrSize = helpers.PointerSize; + + uint heapSegmentSize = (uint)(8 * ptrSize); + uint regionFreeListSize = (uint)(7 * ptrSize); + + var types = new Dictionary + { + [DataType.HeapSegment] = new() { Fields = MakeFields( + ("Allocated", 0, DataType.pointer), + ("Committed", ptrSize, DataType.pointer), + ("Reserved", 2 * ptrSize, DataType.pointer), + ("Used", 3 * ptrSize, DataType.pointer), + ("Mem", 4 * ptrSize, DataType.pointer), + ("Flags", 5 * ptrSize, DataType.nuint), + ("Next", 6 * ptrSize, DataType.pointer), + ("BackgroundAllocated", 7 * ptrSize, DataType.pointer)), Size = heapSegmentSize }, + [DataType.RegionFreeList] = new() { Fields = MakeFields( + ("HeadFreeRegion", 5 * ptrSize, DataType.pointer)), Size = regionFreeListSize }, + }; + + TargetPointer memAddr = new(0x1000_0000); + TargetPointer committedAddr = new(0x1000_2000); + + var segFragment = allocator.Allocate(heapSegmentSize, "Seg"); + builder.AddHeapFragment(segFragment); + Span segSpan = builder.BorrowAddressRange(segFragment.Address, segFragment.Data.Length); + helpers.WritePointer(segSpan.Slice(ptrSize, ptrSize), committedAddr); + helpers.WritePointer(segSpan.Slice(4 * ptrSize, ptrSize), memAddr); + helpers.WritePointer(segSpan.Slice(6 * ptrSize, ptrSize), TargetPointer.Null); + + var flFragment = allocator.Allocate(regionFreeListSize, "FL"); + builder.AddHeapFragment(flFragment); + Span flSpan = builder.BorrowAddressRange(flFragment.Address, flFragment.Data.Length); + helpers.WritePointer(flSpan.Slice(5 * ptrSize, ptrSize), segFragment.Address); + + var globals = BuildGlobals( + new() { [Constants.Globals.GlobalFreeHugeRegions] = flFragment.Address }, + new()); + + var readContext = builder.GetMemoryContext(); + IGC gc = CreateGCContract(arch, types, globals, + [(Constants.Globals.GCIdentifiers, "workstation,regions,background")], + readContext.ReadFromTarget); + + IReadOnlyList regions = gc.GetGCFreeRegions(); + + Assert.Single(regions); + Assert.Equal(memAddr, regions[0].Start); + Assert.Equal((ulong)(committedAddr - memAddr), regions[0].Size); + Assert.Equal((ulong)FreeRegionKind.FreeGlobalHugeRegion, regions[0].ExtraData); + } + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void GetGCFreeRegions_NoFreeRegions_ReturnsEmpty(MockTarget.Architecture arch) + { + TargetTestHelpers helpers = new(arch); + MockMemorySpace.Builder builder = new(helpers); + MockMemorySpace.BumpAllocator allocator = builder.CreateAllocator(0x0010_0000, 0x0100_0000); + int ptrSize = helpers.PointerSize; + + uint regionFreeListSize = (uint)(7 * ptrSize); + var types = new Dictionary + { + [DataType.RegionFreeList] = new() { Fields = MakeFields( + ("HeadFreeRegion", 5 * ptrSize, DataType.pointer)), Size = regionFreeListSize }, + }; + + var flFragment = allocator.Allocate(regionFreeListSize, "FL"); + builder.AddHeapFragment(flFragment); + Span flSpan = builder.BorrowAddressRange(flFragment.Address, flFragment.Data.Length); + helpers.WritePointer(flSpan.Slice(5 * ptrSize, ptrSize), TargetPointer.Null); + + var globals = BuildGlobals( + new() { [Constants.Globals.GlobalFreeHugeRegions] = flFragment.Address }, + new()); + + var readContext = builder.GetMemoryContext(); + IGC gc = CreateGCContract(arch, types, globals, + [(Constants.Globals.GCIdentifiers, "workstation,regions,background")], + readContext.ReadFromTarget); + + Assert.Empty(gc.GetGCFreeRegions()); + } + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void GetGCFreeRegions_MultipleFreeRegionsWithLinkedSegments(MockTarget.Architecture arch) + { + TargetTestHelpers helpers = new(arch); + MockMemorySpace.Builder builder = new(helpers); + MockMemorySpace.BumpAllocator allocator = builder.CreateAllocator(0x0010_0000, 0x0100_0000); + int ptrSize = helpers.PointerSize; + + uint heapSegmentSize = (uint)(8 * ptrSize); + uint regionFreeListSize = (uint)(7 * ptrSize); + + var types = new Dictionary + { + [DataType.HeapSegment] = new() { Fields = MakeFields( + ("Allocated", 0, DataType.pointer), + ("Committed", ptrSize, DataType.pointer), + ("Reserved", 2 * ptrSize, DataType.pointer), + ("Used", 3 * ptrSize, DataType.pointer), + ("Mem", 4 * ptrSize, DataType.pointer), + ("Flags", 5 * ptrSize, DataType.nuint), + ("Next", 6 * ptrSize, DataType.pointer), + ("BackgroundAllocated", 7 * ptrSize, DataType.pointer)), Size = heapSegmentSize }, + [DataType.RegionFreeList] = new() { Fields = MakeFields( + ("HeadFreeRegion", 5 * ptrSize, DataType.pointer)), Size = regionFreeListSize }, + }; + + var seg2Fragment = allocator.Allocate(heapSegmentSize, "Seg2"); + builder.AddHeapFragment(seg2Fragment); + Span seg2Span = builder.BorrowAddressRange(seg2Fragment.Address, seg2Fragment.Data.Length); + helpers.WritePointer(seg2Span.Slice(ptrSize, ptrSize), new TargetPointer(0x2000_4000)); + helpers.WritePointer(seg2Span.Slice(4 * ptrSize, ptrSize), new TargetPointer(0x2000_0000)); + helpers.WritePointer(seg2Span.Slice(6 * ptrSize, ptrSize), TargetPointer.Null); + + var seg1Fragment = allocator.Allocate(heapSegmentSize, "Seg1"); + builder.AddHeapFragment(seg1Fragment); + Span seg1Span = builder.BorrowAddressRange(seg1Fragment.Address, seg1Fragment.Data.Length); + helpers.WritePointer(seg1Span.Slice(ptrSize, ptrSize), new TargetPointer(0x1000_2000)); + helpers.WritePointer(seg1Span.Slice(4 * ptrSize, ptrSize), new TargetPointer(0x1000_0000)); + helpers.WritePointer(seg1Span.Slice(6 * ptrSize, ptrSize), seg2Fragment.Address); + + var flFragment = allocator.Allocate(regionFreeListSize, "FL"); + builder.AddHeapFragment(flFragment); + Span flSpan = builder.BorrowAddressRange(flFragment.Address, flFragment.Data.Length); + helpers.WritePointer(flSpan.Slice(5 * ptrSize, ptrSize), seg1Fragment.Address); + + var globals = BuildGlobals( + new() { [Constants.Globals.GlobalFreeHugeRegions] = flFragment.Address }, + new()); + + var readContext = builder.GetMemoryContext(); + IGC gc = CreateGCContract(arch, types, globals, + [(Constants.Globals.GCIdentifiers, "workstation,regions,background")], + readContext.ReadFromTarget); + + IReadOnlyList regions = gc.GetGCFreeRegions(); + + Assert.Equal(2, regions.Count); + Assert.Equal(new TargetPointer(0x1000_0000), regions[0].Start); + Assert.Equal((ulong)0x2000, regions[0].Size); + Assert.Equal(new TargetPointer(0x2000_0000), regions[1].Start); + Assert.Equal((ulong)0x4000, regions[1].Size); + } +}