Skip to content
2 changes: 2 additions & 0 deletions docs/design/datacontracts/GC.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,8 @@ Data descriptors used:
| `OomHistory` | LohP | GC | Large object heap flag indicating if OOM was related to LOH |
| `GCAllocContext` | Pointer | VM | Current GCAllocContext pointer |
| `GCAllocContext` | Limit | VM | Pointer to the GCAllocContext limit |
| `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 |

Global variables used:
| Global Name | Type | Source | Purpose |
Expand Down
2 changes: 2 additions & 0 deletions docs/design/datacontracts/Thread.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,8 @@ The contract additionally depends on these data descriptors
| `ExceptionInfo` | `ExceptionWatsonBucketTrackerBuckets` | Pointer to Watson unhandled buckets on non-Unix |
| `GCAllocContext` | `Pointer` | GC allocation pointer |
| `GCAllocContext` | `Limit` | Allocation limit pointer |
| `GCAllocContext` | `AllocBytes` | Number of bytes allocated on SOH by this context |
| `GCAllocContext` | `AllocBytesLoh` | Number of bytes allocated not on SOH by this context |
| `IdDispenser` | `HighestId` | Highest possible small thread ID |
| `IdDispenser` | `IdToThread` | Array mapping small thread IDs to thread pointers |
| `InflightTLSData` | `Next` | Pointer to next in-flight TLS data entry |
Expand Down
2 changes: 2 additions & 0 deletions src/coreclr/vm/datadescriptor/datadescriptor.inc
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,8 @@ CDAC_TYPE_BEGIN(GCAllocContext)
CDAC_TYPE_INDETERMINATE(GCAllocContext)
CDAC_TYPE_FIELD(GCAllocContext, /*pointer*/, Pointer, offsetof(gc_alloc_context, alloc_ptr))
CDAC_TYPE_FIELD(GCAllocContext, /*pointer*/, Limit, offsetof(gc_alloc_context, alloc_limit))
CDAC_TYPE_FIELD(GCAllocContext, /*int64*/, AllocBytes, offsetof(gc_alloc_context, alloc_bytes))
CDAC_TYPE_FIELD(GCAllocContext, /*int64*/, AllocBytesLoh, offsetof(gc_alloc_context, alloc_bytes_uoh))
CDAC_TYPE_END(GCAllocContext)

// Exception
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ public interface IThread : IContract
ThreadStoreData GetThreadStoreData() => throw new NotImplementedException();
ThreadStoreCounts GetThreadCounts() => throw new NotImplementedException();
ThreadData GetThreadData(TargetPointer thread) => throw new NotImplementedException();
void GetThreadAllocContext(TargetPointer thread, out long allocBytes, out long allocBytesLoh) => throw new NotImplementedException();
void GetStackLimitData(TargetPointer threadPointer, out TargetPointer stackBase,
out TargetPointer stackLimit, out TargetPointer frameAddress) => throw new NotImplementedException();
TargetPointer IdToThread(uint id) => throw new NotImplementedException();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,14 @@ ThreadData IThread.GetThreadData(TargetPointer threadPointer)
GetThreadFromLink(thread.LinkNext));
}

void IThread.GetThreadAllocContext(TargetPointer threadPointer, out long allocBytes, out long allocBytesLoh)
{
Data.Thread thread = _target.ProcessedData.GetOrAdd<Data.Thread>(threadPointer);

allocBytes = thread.RuntimeThreadLocals?.AllocContext.GCAllocationContext.AllocBytes ?? 0;
allocBytesLoh = thread.RuntimeThreadLocals?.AllocContext.GCAllocationContext.AllocBytesLoh ?? 0;
}

void IThread.GetStackLimitData(TargetPointer threadPointer, out TargetPointer stackBase, out TargetPointer stackLimit, out TargetPointer frameAddress)
{
Data.Thread thread = _target.ProcessedData.GetOrAdd<Data.Thread>(threadPointer);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,12 @@ public GCAllocContext(Target target, TargetPointer address)
Target.TypeInfo type = target.GetTypeInfo(DataType.GCAllocContext);
Pointer = target.ReadPointer(address + (ulong)type.Fields[nameof(Pointer)].Offset);
Limit = target.ReadPointer(address + (ulong)type.Fields[nameof(Limit)].Offset);
AllocBytes = target.Read<long>(address + (ulong)type.Fields[nameof(AllocBytes)].Offset);
AllocBytesLoh = target.Read<long>(address + (ulong)type.Fields[nameof(AllocBytesLoh)].Offset);
}

public TargetPointer Pointer { get; init; }
public TargetPointer Limit { get; init; }
public long AllocBytes { get; init; }
public long AllocBytesLoh { get; init; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,12 @@ public struct DacpThreadData
public ClrDataAddress nextThread;
}

public struct DacpAllocData
{
public ClrDataAddress allocBytes;
public ClrDataAddress allocBytesLoh;
}

public struct DacpModuleData
{
public enum TransientFlags : uint
Expand Down Expand Up @@ -629,7 +635,7 @@ public unsafe partial interface ISOSDacInterface
int GetRegisterName(int regName, uint count, char* buffer, uint* pNeeded);

[PreserveSig]
int GetThreadAllocData(ClrDataAddress thread, /*struct DacpAllocData */ void* data);
int GetThreadAllocData(ClrDataAddress thread, DacpAllocData* data);
[PreserveSig]
int GetHeapAllocData(uint count, /*struct DacpGenerationAllocData */ void* data, uint* pNeeded);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3038,8 +3038,42 @@ int ISOSDacInterface.GetSyncBlockCleanupData(ClrDataAddress addr, void* data)
=> _legacyImpl is not null ? _legacyImpl.GetSyncBlockCleanupData(addr, data) : HResults.E_NOTIMPL;
int ISOSDacInterface.GetSyncBlockData(uint number, void* data)
=> _legacyImpl is not null ? _legacyImpl.GetSyncBlockData(number, data) : HResults.E_NOTIMPL;
int ISOSDacInterface.GetThreadAllocData(ClrDataAddress thread, void* data)
=> _legacyImpl is not null ? _legacyImpl.GetThreadAllocData(thread, data) : HResults.E_NOTIMPL;

int ISOSDacInterface.GetThreadAllocData(ClrDataAddress thread, DacpAllocData* data)
{
int hr = HResults.S_OK;
try
{
if (thread == 0)
throw new ArgumentException();
if (data is null)
throw new NullReferenceException();

Contracts.IThread contract = _target.Contracts.Thread;
contract.GetThreadAllocContext(thread.ToTargetPointer(_target), out long allocBytes, out long allocBytesLoh);
data->allocBytes = (ClrDataAddress)(ulong)allocBytes;
data->allocBytesLoh = (ClrDataAddress)(ulong)allocBytesLoh;
}
catch (global::System.Exception ex)
{
hr = ex.HResult;
}

#if DEBUG
if (_legacyImpl is not null)
{
DacpAllocData dataLocal = default;
int hrLocal = _legacyImpl.GetThreadAllocData(thread, &dataLocal);
Debug.Assert(hrLocal == hr, $"cDAC: {hr:x}, DAC: {hrLocal:x}");
if (hr == HResults.S_OK)
{
Debug.Assert(data->allocBytes == dataLocal.allocBytes, $"cDAC: {data->allocBytes:x}, DAC: {dataLocal.allocBytes:x}");
Debug.Assert(data->allocBytesLoh == dataLocal.allocBytesLoh, $"cDAC: {data->allocBytesLoh:x}, DAC: {dataLocal.allocBytesLoh:x}");
}
}
#endif
return hr;
}

int ISOSDacInterface.GetThreadData(ClrDataAddress thread, DacpThreadData* data)
{
Expand Down
20 changes: 20 additions & 0 deletions src/native/managed/cdac/tests/DumpTests/ThreadDumpTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -111,4 +111,24 @@ public void ThreadCounts_AreNonNegative(TestConfiguration config)
Assert.True(counts.PendingThreadCount >= 0, $"PendingThreadCount should be non-negative, got {counts.PendingThreadCount}");
Assert.True(counts.DeadThreadCount >= 0, $"DeadThreadCount should be non-negative, got {counts.DeadThreadCount}");
}

[ConditionalTheory]
[MemberData(nameof(TestConfigurations))]
public void GetThreadAllocContext_CanReadForAllThreads(TestConfiguration config)
{
InitializeDumpTest(config);
IThread threadContract = Target.Contracts.Thread;
ThreadStoreData storeData = threadContract.GetThreadStoreData();

TargetPointer currentThread = storeData.FirstThread;
while (currentThread != TargetPointer.Null)
{
threadContract.GetThreadAllocContext(currentThread, out long allocBytes, out long allocBytesLoh);
Assert.True(allocBytes >= 0, $"AllocBytes should be non-negative, got {allocBytes}");
Assert.True(allocBytesLoh >= 0, $"AllocBytesLoh should be non-negative, got {allocBytesLoh}");

ThreadData threadData = threadContract.GetThreadData(currentThread);
currentThread = threadData.NextThread;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -72,13 +72,49 @@ public Thread(MockMemorySpace.Builder builder, (ulong Start, ulong End) allocati

private static Dictionary<DataType, Target.TypeInfo> GetTypes(TargetTestHelpers helpers)
{
return GetTypesForTypeFields(
var types = GetTypesForTypeFields(
helpers,
[
ExceptionInfoFields,
ThreadFields,
ThreadStoreFields,
]);

// Compute layouts for embedded struct types (GCAllocContext -> EEAllocContext -> RuntimeThreadLocals)
var gcAllocContextLayout = helpers.LayoutFields(
[
new(nameof(Data.GCAllocContext.Pointer), DataType.pointer),
new(nameof(Data.GCAllocContext.Limit), DataType.pointer),
new(nameof(Data.GCAllocContext.AllocBytes), DataType.int64),
new(nameof(Data.GCAllocContext.AllocBytesLoh), DataType.int64),
]);
types[DataType.GCAllocContext] = new Target.TypeInfo()
{
Fields = gcAllocContextLayout.Fields,
Size = gcAllocContextLayout.Stride,
};

var eeAllocContextLayout = helpers.LayoutFields(
[
new(nameof(Data.EEAllocContext.GCAllocationContext), DataType.GCAllocContext, gcAllocContextLayout.Stride),
]);
types[DataType.EEAllocContext] = new Target.TypeInfo()
{
Fields = eeAllocContextLayout.Fields,
Size = eeAllocContextLayout.Stride,
};

var runtimeThreadLocalsLayout = helpers.LayoutFields(
[
new(nameof(Data.RuntimeThreadLocals.AllocContext), DataType.EEAllocContext, eeAllocContextLayout.Stride),
]);
types[DataType.RuntimeThreadLocals] = new Target.TypeInfo()
{
Fields = runtimeThreadLocalsLayout.Fields,
Size = runtimeThreadLocalsLayout.Stride,
};

return types;
}

internal void SetThreadCounts(int threadCount, int unstartedCount, int backgroundCount, int pendingCount, int deadCount)
Expand All @@ -104,11 +140,19 @@ internal void SetThreadCounts(int threadCount, int unstartedCount, int backgroun
}

internal TargetPointer AddThread(uint id, TargetNUInt osId)
=> AddThread(id, osId, allocBytes: 0, allocBytesLoh: 0);

internal TargetPointer AddThread(uint id, TargetNUInt osId, long allocBytes, long allocBytesLoh)
{
TargetTestHelpers helpers = Builder.TargetTestHelpers;
Target.TypeInfo threadType = Types[DataType.Thread];
Target.TypeInfo exceptionInfoType = Types[DataType.ExceptionInfo];
Target.TypeInfo runtimeThreadLocalsType = Types[DataType.RuntimeThreadLocals];
Target.TypeInfo eeAllocContextType = Types[DataType.EEAllocContext];
Target.TypeInfo gcAllocContextType = Types[DataType.GCAllocContext];

MockMemorySpace.HeapFragment exceptionInfo = _allocator.Allocate(exceptionInfoType.Size.Value, "ExceptionInfo");
MockMemorySpace.HeapFragment runtimeThreadLocals = _allocator.Allocate(runtimeThreadLocalsType.Size.Value, "RuntimeThreadLocals");
MockMemorySpace.HeapFragment thread = _allocator.Allocate(threadType.Size.Value, "Thread");
Span<byte> data = thread.Data.AsSpan();
helpers.Write(
Expand All @@ -120,8 +164,28 @@ internal TargetPointer AddThread(uint id, TargetNUInt osId)
helpers.WritePointer(
data.Slice(threadType.Fields[nameof(Data.Thread.ExceptionTracker)].Offset),
exceptionInfo.Address);
helpers.WritePointer(
data.Slice(threadType.Fields[nameof(Data.Thread.RuntimeThreadLocals)].Offset),
runtimeThreadLocals.Address);

// Write alloc bytes into the GCAllocContext embedded within RuntimeThreadLocals
int allocContextOffset = runtimeThreadLocalsType.Fields[nameof(Data.RuntimeThreadLocals.AllocContext)].Offset;
int gcAllocationContextOffset = eeAllocContextType.Fields[nameof(Data.EEAllocContext.GCAllocationContext)].Offset;
int allocBytesOffset = gcAllocContextType.Fields[nameof(Data.GCAllocContext.AllocBytes)].Offset;
int allocBytesLohOffset = gcAllocContextType.Fields[nameof(Data.GCAllocContext.AllocBytesLoh)].Offset;

Span<byte> runtimeThreadLocalsData = runtimeThreadLocals.Data.AsSpan();
int baseOffset = allocContextOffset + gcAllocationContextOffset;
helpers.Write(
runtimeThreadLocalsData.Slice(baseOffset + allocBytesOffset),
(ulong)allocBytes);
helpers.Write(
runtimeThreadLocalsData.Slice(baseOffset + allocBytesLohOffset),
(ulong)allocBytesLoh);

Builder.AddHeapFragment(thread);
Builder.AddHeapFragment(exceptionInfo);
Builder.AddHeapFragment(runtimeThreadLocals);

ulong threadLinkOffset = (ulong)threadType.Fields[nameof(Data.Thread.LinkNext)].Offset;
if (_previousThread != TargetPointer.Null)
Expand Down
41 changes: 41 additions & 0 deletions src/native/managed/cdac/tests/ThreadTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,47 @@ public void IterateThreads(MockTarget.Architecture arch)
Assert.Equal(expectedCount, count);
}

[Theory]
[ClassData(typeof(MockTarget.StdArch))]
public void GetThreadAllocContext(MockTarget.Architecture arch)
{
TargetTestHelpers helpers = new(arch);
MockMemorySpace.Builder builder = new(helpers);
MockDescriptors.Thread thread = new(builder);

long allocBytes = 1024;
long allocBytesLoh = 4096;

TargetPointer addr = thread.AddThread(1, new TargetNUInt(1234), allocBytes, allocBytesLoh);

Target target = CreateTarget(thread);
IThread contract = target.Contracts.Thread;
Comment on lines 126 to 140
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

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

GetThreadAllocContext test references a thread variable that isn't declared in the method scope (unlike the other tests that create TargetTestHelpers/MockMemorySpace.Builder/MockDescriptors.Thread). This will not compile and also means the arch parameter isn't used to build the target. Create a local MockDescriptors.Thread using the provided arch before calling AddThread/CreateTarget.

Copilot uses AI. Check for mistakes.
Assert.NotNull(contract);

contract.GetThreadAllocContext(addr, out long resultAllocBytes, out long resultAllocBytesLoh);
Assert.Equal(allocBytes, resultAllocBytes);
Assert.Equal(allocBytesLoh, resultAllocBytesLoh);
}

[Theory]
[ClassData(typeof(MockTarget.StdArch))]
public void GetThreadAllocContext_ZeroValues(MockTarget.Architecture arch)
{
TargetTestHelpers helpers = new(arch);
MockMemorySpace.Builder builder = new(helpers);
MockDescriptors.Thread thread = new(builder);

TargetPointer addr = thread.AddThread(1, new TargetNUInt(1234));

Target target = CreateTarget(thread);
IThread contract = target.Contracts.Thread;
Assert.NotNull(contract);

contract.GetThreadAllocContext(addr, out long allocBytes, out long allocBytesLoh);
Assert.Equal(0, allocBytes);
Assert.Equal(0, allocBytesLoh);
}

[Theory]
[ClassData(typeof(MockTarget.StdArch))]
public void GetStackLimits(MockTarget.Architecture arch)
Expand Down
Loading