Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
103 changes: 102 additions & 1 deletion docs/design/datacontracts/GC.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,11 @@ public readonly struct GCOomData
IReadOnlyList<GCMemoryRegionData> GetGCBookkeepingMemoryRegions();
// Gets GC free regions (free region lists and freeable segments)
IReadOnlyList<GCMemoryRegionData> GetGCFreeRegions();

// Enumerates every GC heap segment of every heap. Each yielded GCHeapSegmentInfo describes a
// single segment with the inclusive start and exclusive end of its memory range, its generation
// tag (or Ephemeral), and the index of the heap that owns it (always 0 for workstation GC).
IEnumerable<GCHeapSegmentInfo> EnumerateHeapSegments(GCHeapData heapData);
```

Comment thread
rcj1 marked this conversation as resolved.
```csharp
Expand All @@ -145,6 +150,26 @@ public readonly struct GCMemoryRegionData
public ulong ExtraData { get; init; }
public int Heap { get; init; }
}

public enum GCSegmentClassification
{
Unknown,
Gen0,
Gen1,
Gen2,
LOH,
POH,
NonGC,
// Segments-GC only: marker used by IGC.EnumerateHeapSegments to denote the ephemeral
// segment on the gen2 list. The caller is responsible for splitting it into the Gen1
// piece and an optional Gen2 prefix.
Ephemeral,
}

public readonly record struct GCHeapSegmentInfo(
TargetPointer Start,
TargetPointer End,
GCSegmentClassification Generation);
```

## Version 1
Expand Down Expand Up @@ -286,6 +311,7 @@ Constants used:
| Name | Type | Purpose | Value |
| --- | --- | --- | --- |
| `WRK_HEAP_COUNT` | uint | The number of heaps in the `workstation` GC type | `1` |
| `HEAP_SEGMENT_FLAGS_READONLY` | ulong | `HeapSegment.Flags` bit identifying a readonly (e.g. frozen, non-GC) segment. | `1` |

```csharp
GCHeapType IGC.GetGCIdentifiers()
Expand Down Expand Up @@ -858,7 +884,7 @@ IReadOnlyList<GCMemoryRegionData> IGC.GetHandleTableMemoryRegions()
// Safety caps matching native DAC
const int MaxHandleTableRegions = 8192;
const int MaxBookkeepingRegions = 32;
const int MaxSegmentListIterations = 2048;
const int MaxSegmentListIterations = 65536;

int maxRegions = MaxHandleTableRegions;
TargetPointer handleTableMap = target.ReadGlobalPointer("HandleTableMap");
Expand Down Expand Up @@ -1027,3 +1053,78 @@ TargetNUInt IGC.GetHandleExtraInfo(TargetPointer handle)
return target.ReadNUInt(extraInfoAddr);
}
```

EnumerateHeapSegments

Returns the raw GC heap segments for a single heap by walking the per-generation segment
lists.
```csharp
IEnumerable<GCHeapSegmentInfo> IGC.EnumerateHeapSegments(GCHeapData heapData)
{
// The generation table is laid out as gen0, gen1, gen2, LOH, POH (plus optional extras).
var gens = heapData.GenerationTable;
bool regions = GetGCIdentifiers().Contains("regions");

TargetPointer ephemeralSegment = heapData.EphemeralHeapSegment;
TargetPointer allocAllocated = heapData.AllocAllocated;

if (regions)
{
// In regions mode each generation has its own segment list. Readonly entries on
// the gen2 list represent non-GC (e.g. frozen) regions and are reported as NonGC.
foreach (var (seg, _) in WalkSegmentList(gens[2].StartSegment))
{
var type = (seg.Flags & HEAP_SEGMENT_FLAGS_READONLY) != 0
? GCSegmentClassification.NonGC
: GCSegmentClassification.Gen2;
yield return new GCHeapSegmentInfo(seg.Mem, seg.Allocated, type);
}
foreach (var (seg, _) in WalkSegmentList(gens[1].StartSegment))
yield return new GCHeapSegmentInfo(seg.Mem, seg.Allocated, GCSegmentClassification.Gen1);
foreach (var (seg, segAddr) in WalkSegmentList(gens[0].StartSegment))
{
// For the gen0 segment that matches the ephemeral_heap_segment, end is alloc_allocated.
TargetPointer end = segAddr == ephemeralSegment ? allocAllocated : seg.Allocated;
yield return new GCHeapSegmentInfo(seg.Mem, end, GCSegmentClassification.Gen0);
}
}
else
{
// In segments mode the gen2 list contains every SOH segment. The ephemeral
// segment is tagged Ephemeral as the layer-2 split marker; non-ephemeral entries
// are reported with their true generation (Gen2 or NonGC for readonly).
foreach (var (seg, segAddr) in WalkSegmentList(gens[2].StartSegment))
{
GCSegmentClassification type;
if (segAddr == ephemeralSegment)
type = GCSegmentClassification.Ephemeral;
else if ((seg.Flags & HEAP_SEGMENT_FLAGS_READONLY) != 0)
type = GCSegmentClassification.NonGC;
else
type = GCSegmentClassification.Gen2;
yield return new GCHeapSegmentInfo(seg.Mem, seg.Allocated, type);
}
}

// LOH and POH segments are always reported as-is regardless of GC mode.
foreach (var (seg, _) in WalkSegmentList(gens[3].StartSegment))
yield return new GCHeapSegmentInfo(seg.Mem, seg.Allocated, GCSegmentClassification.LOH);
foreach (var (seg, _) in WalkSegmentList(gens[4].StartSegment))
yield return new GCHeapSegmentInfo(seg.Mem, seg.Allocated, GCSegmentClassification.POH);
}

IEnumerable<(HeapSegment Segment, TargetPointer Address)> WalkSegmentList(TargetPointer startSegment)
{
// Bounded traversal of the singly-linked HeapSegment list, guarding against cycles or
// corrupt links via a fixed iteration cap (MaxSegmentListIterations = 2048).
int iterationMax = MaxSegmentListIterations;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A large enough GC heap in regions mode can probably exceed this limit. Try a test app that creates 10+ GB of small objects to see how it behaves.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, with 10GB I get 2823 segments. In the native DAC we cap the number of regions at 8192. Maybe both should be just a little more liberal, say 16384?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The biggest app heaps I hear of today are in the 50-100GB range and we probably want to add headroom to grow even bigger. A limit at 50K segments would put us ~200GB.

TargetPointer current = startSegment;
while (current != TargetPointer.Null)
{
HeapSegment seg = /* read HeapSegment at current */;
yield return (seg, current);
current = seg.Next;
if (iterationMax-- <= 0) throw /* cycle detected */;
}
}
```
2 changes: 1 addition & 1 deletion src/coreclr/debug/daccess/daccess.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -8065,7 +8065,7 @@ void DacFreeRegionEnumerator::AddSingleSegment(const dac_heap_segment &curr, Fre

void DacFreeRegionEnumerator::AddSegmentList(DPTR(dac_heap_segment) start, FreeRegionKind kind, int heap)
{
int iterationMax = 2048;
int iterationMax = 65536;

DPTR(dac_heap_segment) curr = start;
while (curr != nullptr)
Expand Down
81 changes: 23 additions & 58 deletions src/coreclr/debug/daccess/dacdbiimpl.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7031,10 +7031,14 @@ HRESULT STDMETHODCALLTYPE DacDbiInterfaceImpl::WalkHeap(HeapWalkHandle handle,



HRESULT STDMETHODCALLTYPE DacDbiInterfaceImpl::GetHeapSegments(OUT DacDbiArrayList<COR_SEGMENT> *pSegments)
HRESULT STDMETHODCALLTYPE DacDbiInterfaceImpl::EnumerateHeapSegments(
FP_HEAPSEGMENT_CALLBACK fpCallback,
CALLBACK_DATA pUserData)
{
DD_ENTER_MAY_THROW;

if (fpCallback == NULL)
return E_POINTER;

size_t heapCount = 0;
HeapData *heaps = 0;
Expand All @@ -7049,53 +7053,28 @@ HRESULT STDMETHODCALLTYPE DacDbiInterfaceImpl::GetHeapSegments(OUT DacDbiArrayLi

NewArrayHolder<HeapData> _heapHolder = heaps;

// Count the number of segments to know how much to allocate.
int total = 0;
for (size_t i = 0; i < heapCount; ++i)
{
total += (int)heaps[i].SegmentCount;
if (!region)
{
// SegmentCount is +1 due to the ephemeral segment containing more than one
// generation (Gen1 + Gen0, and sometimes part of Gen2).
total++;

// It's possible that part of Gen2 lives on the ephemeral segment. If so,
// we need to add one more to the output.
const size_t eph = heaps[i].EphemeralSegment;
_ASSERTE(eph < heaps[i].SegmentCount);
if (heaps[i].Segments[eph].Start != heaps[i].Gen1Start)
total++;
}
}

pSegments->Alloc(total);
if (FAILED(hr))
return hr;

// Now walk all segments and write them to the array.
int curr = 0;
// Now walk all segments and emit them through the callback.
for (size_t i = 0; i < heapCount; ++i)
{
_ASSERTE(curr < total);
if (!region)
{
// Generation 0 is not in the segment list.
COR_SEGMENT &seg = (*pSegments)[curr++];
seg.start = heaps[i].Gen0Start;
seg.end = heaps[i].Gen0End;
seg.type = CorDebug_Gen0;
seg.heap = (ULONG)i;
fpCallback(heaps[i].Gen0Start, heaps[i].Gen0End, (int)CorDebug_Gen0, (ULONG)i, pUserData);
}

for (size_t j = 0; j < heaps[i].SegmentCount; ++j)
{
if (region)
{
_ASSERTE(curr < total);
COR_SEGMENT &seg = (*pSegments)[curr++];
seg.start = heaps[i].Segments[j].Start;
seg.end = heaps[i].Segments[j].End;
seg.type = (CorDebugGenerationTypes)heaps[i].Segments[j].Generation;
seg.heap = (ULONG)i;
fpCallback(
heaps[i].Segments[j].Start,
heaps[i].Segments[j].End,
(int)heaps[i].Segments[j].Generation,
(ULONG)i,
pUserData);
}
else if (heaps[i].Segments[j].Generation == 1)
{
Expand All @@ -7104,43 +7083,29 @@ HRESULT STDMETHODCALLTYPE DacDbiInterfaceImpl::GetHeapSegments(OUT DacDbiArrayLi
_ASSERTE(heaps[i].Segments[j].Start <= heaps[i].Gen1Start);
_ASSERTE(heaps[i].Segments[j].End > heaps[i].Gen1Start);

{
_ASSERTE(curr < total);
COR_SEGMENT &seg = (*pSegments)[curr++];
seg.start = heaps[i].Gen1Start;
seg.end = heaps[i].Gen0Start;
seg.type = CorDebug_Gen1;
seg.heap = (ULONG)i;
}
fpCallback(heaps[i].Gen1Start, heaps[i].Gen0Start, (int)CorDebug_Gen1, (ULONG)i, pUserData);

// It's possible for Gen2 to take up a portion of the ephemeral segment.
// We test for that here.
if (heaps[i].Segments[j].Start != heaps[i].Gen1Start)
{
_ASSERTE(curr < total);
COR_SEGMENT &seg = (*pSegments)[curr++];
seg.start = heaps[i].Segments[j].Start;
seg.end = heaps[i].Gen1Start;
seg.type = CorDebug_Gen2;
seg.heap = (ULONG)i;
fpCallback(heaps[i].Segments[j].Start, heaps[i].Gen1Start, (int)CorDebug_Gen2, (ULONG)i, pUserData);
}
}
else
{
// Otherwise, we have a gen2, POH, LOH or NonGC
_ASSERTE(curr < total);
COR_SEGMENT &seg = (*pSegments)[curr++];
seg.start = heaps[i].Segments[j].Start;
seg.end = heaps[i].Segments[j].End;

_ASSERTE(heaps[i].Segments[j].Generation <= CorDebug_NonGC);
seg.type = (CorDebugGenerationTypes)heaps[i].Segments[j].Generation;
seg.heap = (ULONG)i;
fpCallback(
heaps[i].Segments[j].Start,
heaps[i].Segments[j].End,
(int)heaps[i].Segments[j].Generation,
(ULONG)i,
pUserData);
}
}
}

_ASSERTE(total == curr);
return hr;
}

Expand Down
2 changes: 1 addition & 1 deletion src/coreclr/debug/daccess/dacdbiimpl.h
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ class DacDbiInterfaceImpl :
OUT COR_HEAPOBJECT * objects,
OUT ULONG *fetched);

HRESULT STDMETHODCALLTYPE GetHeapSegments(OUT DacDbiArrayList<COR_SEGMENT> *pSegments);
HRESULT STDMETHODCALLTYPE EnumerateHeapSegments(FP_HEAPSEGMENT_CALLBACK fpCallback, CALLBACK_DATA pUserData);


HRESULT STDMETHODCALLTYPE IsValidObject(CORDB_ADDRESS obj, OUT BOOL * pResult);
Expand Down
49 changes: 45 additions & 4 deletions src/coreclr/debug/di/process.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2201,6 +2201,43 @@ HRESULT CordbProcess::GetGCHeapInformation(COR_HEAPINFO *pHeapInfo)
return hr;
}

namespace
{
struct HeapSegmentAccumulator
{
CQuickArrayList<COR_SEGMENT> segments;
HRESULT hrError;
};

void HeapSegmentCallback(
CORDB_ADDRESS rangeStart,
CORDB_ADDRESS rangeEnd,
int generation,
ULONG heap,
CALLBACK_DATA pUserData)
{
HeapSegmentAccumulator *acc =
reinterpret_cast<HeapSegmentAccumulator*>(pUserData);

if (FAILED(acc->hrError))
return;

COR_SEGMENT s;
s.start = rangeStart;
s.end = rangeEnd;
s.type = (CorDebugGenerationTypes)generation;
s.heap = heap;
HRESULT hr = S_OK;
EX_TRY
{
acc->segments.Push(s);
}
EX_CATCH_HRESULT(hr);
if (FAILED(hr))
acc->hrError = hr;
}
}

HRESULT CordbProcess::EnumerateHeapRegions(ICorDebugHeapSegmentEnum **ppRegions)
{
if (!ppRegions)
Expand All @@ -2212,14 +2249,18 @@ HRESULT CordbProcess::EnumerateHeapRegions(ICorDebugHeapSegmentEnum **ppRegions)

EX_TRY
{
DacDbiArrayList<COR_SEGMENT> segments;
hr = GetDAC()->GetHeapSegments(&segments);
HeapSegmentAccumulator acc;
acc.hrError = S_OK;

hr = GetDAC()->EnumerateHeapSegments(&HeapSegmentCallback, &acc);
if (SUCCEEDED(hr) && FAILED(acc.hrError))
hr = acc.hrError;

if (SUCCEEDED(hr))
{
if (!segments.IsEmpty())
if (acc.segments.Size() != 0)
{
CordbHeapSegmentEnumerator *segEnum = new CordbHeapSegmentEnumerator(this, &segments[0], (DWORD)segments.Count());
CordbHeapSegmentEnumerator *segEnum = new CordbHeapSegmentEnumerator(this, acc.segments.Ptr(), (DWORD)acc.segments.Size());
GetContinueNeuterList()->Add(this, segEnum);
hr = segEnum->QueryInterface(__uuidof(ICorDebugHeapSegmentEnum), (void**)ppRegions);
}
Expand Down
18 changes: 17 additions & 1 deletion src/coreclr/debug/inc/dacdbiinterface.h
Original file line number Diff line number Diff line change
Expand Up @@ -2002,7 +2002,23 @@ IDacDbiInterface : public IUnknown
OUT COR_HEAPOBJECT * objects,
OUT ULONG * pFetched) = 0;

virtual HRESULT STDMETHODCALLTYPE GetHeapSegments(OUT DacDbiArrayList<COR_SEGMENT> * pSegments) = 0;
// Callback invoked for each GC heap segment.
//
// Arguments:
// rangeStart - start address of the segment (inclusive).
// rangeEnd - end address of the segment (exclusive).
// generation - CorDebugGenerationTypes value identifying the generation/kind of the segment.
// heap - index of the heap the segment belongs to (0 for workstation GC).
// pUserData - user data passed to EnumerateHeapSegments.
typedef void (*FP_HEAPSEGMENT_CALLBACK)(CORDB_ADDRESS rangeStart, CORDB_ADDRESS rangeEnd, int generation, ULONG heap, CALLBACK_DATA pUserData);

// Enumerate the GC heap segments.
//
// The callback is invoked once per segment and must not throw. To accumulate segments, callers
// typically stash a buffer (and any failure flag) in pUserData. The walker runs to completion
// regardless of any per-segment failure the callback decides to record; the caller surfaces the
// recorded failure after EnumerateHeapSegments returns.
virtual HRESULT STDMETHODCALLTYPE EnumerateHeapSegments(FP_HEAPSEGMENT_CALLBACK fpCallback, CALLBACK_DATA pUserData) = 0;

virtual HRESULT STDMETHODCALLTYPE IsValidObject(CORDB_ADDRESS obj, OUT BOOL * pResult) = 0;

Expand Down
3 changes: 2 additions & 1 deletion src/coreclr/inc/dacdbi.idl
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ typedef void (*FP_ASSEMBLY_ENUMERATION_CALLBACK)(VMPTR_Assembly vmAssembly, CALL
typedef void (*FP_MODULE_ENUMERATION_CALLBACK)(VMPTR_Assembly vmAssembly, CALLBACK_DATA pUserData);
typedef void (*FP_THREAD_ENUMERATION_CALLBACK)(VMPTR_Thread vmThread, CALLBACK_DATA pUserData);
typedef BOOL (*FP_INTERNAL_FRAME_ENUMERATION_CALLBACK)(FramePointer fpFrame, CALLBACK_DATA pUserData);
typedef void (*FP_HEAPSEGMENT_CALLBACK)(CORDB_ADDRESS rangeStart, CORDB_ADDRESS rangeEnd, int generation, ULONG heap, CALLBACK_DATA pUserData);


//
Expand Down Expand Up @@ -381,7 +382,7 @@ interface IDacDbiInterface : IUnknown
HRESULT CreateHeapWalk([out] HeapWalkHandle * pHandle);
HRESULT DeleteHeapWalk([in] HeapWalkHandle handle);
HRESULT WalkHeap([in] HeapWalkHandle handle, [in] ULONG count, [out] COR_HEAPOBJECT * objects, [out] ULONG * pFetched);
HRESULT GetHeapSegments([out] DacDbiArrayList_COR_SEGMENT * pSegments);
HRESULT EnumerateHeapSegments([in] FP_HEAPSEGMENT_CALLBACK fpCallback, [in] CALLBACK_DATA pUserData);
HRESULT IsValidObject([in] CORDB_ADDRESS obj, [out] BOOL * pResult);

// Reference Walking
Expand Down
Loading
Loading