diff --git a/docs/design/datacontracts/GC.md b/docs/design/datacontracts/GC.md index bbb6d3ebafb121..ec9061680b69c4 100644 --- a/docs/design/datacontracts/GC.md +++ b/docs/design/datacontracts/GC.md @@ -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 | diff --git a/docs/design/datacontracts/Thread.md b/docs/design/datacontracts/Thread.md index 606b3d924ad6ad..fbb8def08a5b23 100644 --- a/docs/design/datacontracts/Thread.md +++ b/docs/design/datacontracts/Thread.md @@ -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 | diff --git a/src/coreclr/vm/datadescriptor/datadescriptor.inc b/src/coreclr/vm/datadescriptor/datadescriptor.inc index 5e203f8a07c6cd..b611c29d46b67c 100644 --- a/src/coreclr/vm/datadescriptor/datadescriptor.inc +++ b/src/coreclr/vm/datadescriptor/datadescriptor.inc @@ -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 diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IThread.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IThread.cs index 6dabae55c2d6ad..296552dfa73836 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IThread.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IThread.cs @@ -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(); diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/Thread_1.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/Thread_1.cs index c1e31f70c9cd9e..3b91d1d2bb138e 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/Thread_1.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/Thread_1.cs @@ -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(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(threadPointer); diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/GCAllocContext.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/GCAllocContext.cs index 02b22b0e335dad..bc6b6e1a98a8a1 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/GCAllocContext.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/GCAllocContext.cs @@ -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(address + (ulong)type.Fields[nameof(AllocBytes)].Offset); + AllocBytesLoh = target.Read(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; } } 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 6851ade98cdffa..e048d68daa7a75 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/ISOSDacInterface.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/ISOSDacInterface.cs @@ -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 @@ -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); 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 54d24e0f26737c..473967ef96006f 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.cs @@ -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) { diff --git a/src/native/managed/cdac/tests/DumpTests/ThreadDumpTests.cs b/src/native/managed/cdac/tests/DumpTests/ThreadDumpTests.cs index d46a4dbc47fc8c..8e24fafc4d12f6 100644 --- a/src/native/managed/cdac/tests/DumpTests/ThreadDumpTests.cs +++ b/src/native/managed/cdac/tests/DumpTests/ThreadDumpTests.cs @@ -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; + } + } } diff --git a/src/native/managed/cdac/tests/MockDescriptors/MockDescriptors.Thread.cs b/src/native/managed/cdac/tests/MockDescriptors/MockDescriptors.Thread.cs index 2d778d2d2778b5..806822df964d2e 100644 --- a/src/native/managed/cdac/tests/MockDescriptors/MockDescriptors.Thread.cs +++ b/src/native/managed/cdac/tests/MockDescriptors/MockDescriptors.Thread.cs @@ -72,13 +72,49 @@ public Thread(MockMemorySpace.Builder builder, (ulong Start, ulong End) allocati private static Dictionary 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) @@ -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 data = thread.Data.AsSpan(); helpers.Write( @@ -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 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) diff --git a/src/native/managed/cdac/tests/ThreadTests.cs b/src/native/managed/cdac/tests/ThreadTests.cs index 64a2a103421ccd..ef768016a2a9f9 100644 --- a/src/native/managed/cdac/tests/ThreadTests.cs +++ b/src/native/managed/cdac/tests/ThreadTests.cs @@ -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; + 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)