Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
34f599c
Initial plan
Copilot Feb 26, 2026
50cb033
Add ISOSMemoryEnum/SOSMemoryRegion interfaces and GC data descriptors…
Copilot Feb 26, 2026
04b43d8
Add managed Data classes, DataType entries, Constants.Globals, IGCHea…
Copilot Feb 26, 2026
4f6b717
Implement GC_1 memory region methods, SOSMemoryEnum class, and SOSDac…
Copilot Feb 26, 2026
4bbdf38
Add tests for GC memory region methods in IGC contract
Copilot Feb 26, 2026
2c5d3f5
Changes before error encountered
Copilot Feb 26, 2026
50041df
Add edge-case tests: empty buckets, multiple segments, multiple bookk…
Copilot Feb 27, 2026
6320ebd
Use TotalCpuCount instead of GetGCHeapCount for handle table slot lim…
Copilot Feb 27, 2026
a47d22d
Add extern declaration for g_totalCpuCount in datadescriptor.h to fix…
Copilot Feb 27, 2026
0e2f8c9
Revert all previous changes to start fresh from base
Copilot Feb 27, 2026
7641ef4
Merge remote-tracking branch 'origin/main' into copilot/implement-mem…
Copilot Feb 27, 2026
8c95b43
Re-implement cDAC Memory Region APIs on top of latest main with PR #1…
Copilot Feb 27, 2026
7943eab
Fix GCMemoryRegionTests to use MockMemorySpace correctly
Copilot Feb 27, 2026
d4af9be
Update GC.md documentation with new API types, data descriptors, glob…
Copilot Feb 27, 2026
6e3291f
Fix native build: add conditional compilation guards for GC fields an…
Copilot Feb 27, 2026
b3ddcf1
Fix build break: use FREE_REGION_KINDS macro instead of namespaced co…
Copilot Feb 28, 2026
99dd629
Address review feedback: use ToClrDataAddress helper, null-check memR…
Copilot Mar 2, 2026
f87a031
Merge branch 'main' into copilot/implement-memory-region-apis
max-charlamb Mar 2, 2026
f9672e8
Add cDAC dump tests for GetHandleTableMemoryRegions, GetGCBookkeeping…
Copilot Mar 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
185 changes: 185 additions & 0 deletions docs/design/datacontracts/GC.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<GCMemoryRegionData> GetHandleTableMemoryRegions();
// Gets GC bookkeeping memory regions (card table info linked list)
IReadOnlyList<GCMemoryRegionData> GetGCBookkeepingMemoryRegions();
// Gets GC free regions (free region lists and freeable segments)
IReadOnlyList<GCMemoryRegionData> 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
Expand Down Expand Up @@ -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` |
Expand Down Expand Up @@ -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 |
Expand Down Expand Up @@ -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<GCMemoryRegionData> IGC.GetHandleTableMemoryRegions()
{
List<GCMemoryRegionData> regions = new();
uint handleSegmentSize = target.ReadGlobal<uint>("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<uint>(target.ReadGlobalPointer("TotalCpuCount"))
: 1;

int maxRegions = 8192;
TargetPointer handleTableMap = target.ReadGlobalPointer("HandleTableMap");
while (handleTableMap != null && maxRegions >= 0)
{
HandleTableMap map = Read<HandleTableMap>(handleTableMap);
foreach (TargetPointer bucketPtr in map.BucketsPtr)
{
if (bucketPtr == null) continue;
HandleTableBucket bucket = Read<HandleTableBucket>(bucketPtr);
for (uint j = 0; j < tableCount; j++)
{
TargetPointer htPtr = ReadPointer(bucket.Table + j * PointerSize);
if (htPtr == null) continue;
HandleTable ht = Read<HandleTable>(htPtr);
if (ht.SegmentList == null) continue;
TargetPointer seg = ht.SegmentList;
TargetPointer first = seg;
do
{
TableSegment ts = Read<TableSegment>(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<GCMemoryRegionData> IGC.GetGCBookkeepingMemoryRegions()
{
List<GCMemoryRegionData> regions = new();
if (!TryReadGlobalPointer("BookkeepingStart", out TargetPointer? bkGlobal))
return regions;
TargetPointer bookkeepingStart = ReadPointer(bkGlobal);
if (bookkeepingStart == null) return regions;

uint cardTableInfoSize = ReadGlobal<uint>("CardTableInfoSize");
CardTableInfo cti = Read<CardTableInfo>(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<CardTableInfo>(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<GCMemoryRegionData> IGC.GetGCFreeRegions()
{
List<GCMemoryRegionData> regions = new();
int countFreeRegionKinds = min(ReadGlobal<uint>("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<GCMemoryRegionData> regions, int heap = 0)
{
RegionFreeList fl = Read<RegionFreeList>(freeListAddr);
if (fl.HeadFreeRegion != null)
AddSegmentList(fl.HeadFreeRegion, kind, regions, heap);
}

void AddSegmentList(TargetPointer start, FreeRegionKind kind, List<GCMemoryRegionData> regions, int heap = 0)
{
int iterationMax = 2048;
TargetPointer curr = start;
while (curr != null && iterationMax-- > 0)
{
HeapSegment seg = Read<HeapSegment>(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;
}
}
```
13 changes: 13 additions & 0 deletions src/coreclr/gc/datadescriptor/datadescriptor.h
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,19 @@ struct cdac_data<GC_NAMESPACE::gc_heap>
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<>
Expand Down
36 changes: 36 additions & 0 deletions src/coreclr/gc/datadescriptor/datadescriptor.inc
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,13 @@ CDAC_TYPE_FIELD(GCHeap, /*pointer*/, InterestingData, cdac_data<GC_NAMESPACE::gc
CDAC_TYPE_FIELD(GCHeap, /*pointer*/, CompactReasons, cdac_data<GC_NAMESPACE::gc_heap>::CompactReasons)
CDAC_TYPE_FIELD(GCHeap, /*pointer*/, ExpandMechanisms, cdac_data<GC_NAMESPACE::gc_heap>::ExpandMechanisms)
CDAC_TYPE_FIELD(GCHeap, /*pointer*/, InterestingMechanismBits, cdac_data<GC_NAMESPACE::gc_heap>::InterestingMechanismBits)
#ifdef BACKGROUND_GC
CDAC_TYPE_FIELD(GCHeap, /*pointer*/, FreeableSohSegment, cdac_data<GC_NAMESPACE::gc_heap>::FreeableSohSegment)
CDAC_TYPE_FIELD(GCHeap, /*pointer*/, FreeableUohSegment, cdac_data<GC_NAMESPACE::gc_heap>::FreeableUohSegment)
#endif // BACKGROUND_GC
#ifdef USE_REGIONS
CDAC_TYPE_FIELD(GCHeap, /*pointer*/, FreeRegions, cdac_data<GC_NAMESPACE::gc_heap>::FreeRegions)
#endif // USE_REGIONS
CDAC_TYPE_END(GCHeap)
#endif // SERVER_GC

Expand Down Expand Up @@ -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()
Expand All @@ -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
Expand All @@ -149,9 +173,21 @@ CDAC_GLOBAL_POINTER(GCHeapInterestingData, cdac_data<GC_NAMESPACE::gc_heap>::Int
CDAC_GLOBAL_POINTER(GCHeapCompactReasons, cdac_data<GC_NAMESPACE::gc_heap>::CompactReasons)
CDAC_GLOBAL_POINTER(GCHeapExpandMechanisms, cdac_data<GC_NAMESPACE::gc_heap>::ExpandMechanisms)
CDAC_GLOBAL_POINTER(GCHeapInterestingMechanismBits, cdac_data<GC_NAMESPACE::gc_heap>::InterestingMechanismBits)
#ifdef BACKGROUND_GC
CDAC_GLOBAL_POINTER(GCHeapFreeableSohSegment, cdac_data<GC_NAMESPACE::gc_heap>::FreeableSohSegment)
CDAC_GLOBAL_POINTER(GCHeapFreeableUohSegment, cdac_data<GC_NAMESPACE::gc_heap>::FreeableUohSegment)
#endif // BACKGROUND_GC
#ifdef USE_REGIONS
CDAC_GLOBAL_POINTER(GCHeapFreeRegions, cdac_data<GC_NAMESPACE::gc_heap>::FreeRegions)
#endif // USE_REGIONS
#endif // !SERVER_GC

CDAC_GLOBAL_POINTER(HandleTableMap, &g_HandleTableMap)
CDAC_GLOBAL_POINTER(BookkeepingStart, cdac_data<GC_NAMESPACE::gc_heap>::BookkeepingStart)
#ifdef USE_REGIONS
CDAC_GLOBAL_POINTER(GlobalFreeHugeRegions, cdac_data<GC_NAMESPACE::gc_heap>::GlobalFreeHugeRegions)
CDAC_GLOBAL_POINTER(GlobalRegionsToDecommit, cdac_data<GC_NAMESPACE::gc_heap>::GlobalRegionsToDecommit)
#endif // USE_REGIONS

#ifdef SERVER_GC
#define GC_TYPE server
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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<GCMemoryRegionData> GetHandleTableMemoryRegions() => throw new NotImplementedException();
IReadOnlyList<GCMemoryRegionData> GetGCBookkeepingMemoryRegions() => throw new NotImplementedException();
IReadOnlyList<GCMemoryRegionData> GetGCFreeRegions() => throw new NotImplementedException();
}

public readonly struct GC : IGC
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -157,4 +157,6 @@ public enum DataType
HandleTableBucket,
HandleTable,
TableSegment,
CardTableInfo,
RegionFreeList,
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
Expand All @@ -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; }
}
Loading
Loading