From b31ec461d220c5ed972b9ae32d8396cdc1ebf5ef Mon Sep 17 00:00:00 2001 From: Max Charlamb Date: Wed, 10 Jun 2026 22:27:59 -0400 Subject: [PATCH 01/18] Add ISOSDacInterface17 for StressLog enumeration via cDAC Add a new ISOSDacInterface17 COM interface that exposes the cDAC's IStressLog contract to SOS and clrmd consumers. This enables reading stress logs from both CoreCLR and NativeAOT without hardcoded struct offsets. Changes: - Add StartTime (wall-clock FILETIME) field to cDAC data descriptors, Data/StressLog.cs, StressLogData record, and contract implementation - Add StressLog to ContractRegistry - Add Address field to ThreadStressLogData for thread identification - Define ISOSDacInterface17 with IsStressLogAvailable, GetStressLogData, GetStressLogThreadEnumerator, GetStressLogMessageEnumerator, and GetStressLogMemoryRanges - Define ISOSStressLogThreadEnum, ISOSStressLogMsgEnum, and ISOSStressLogMemoryEnum enumerator interfaces - Implement SOSDacImpl bridge delegating to _target.Contracts.StressLog - Add IDL definitions in sospriv.idl (native DAC does not implement this interface; QueryInterface for the IID will fail on the legacy DAC) - Add StressLog debuggee and StressLogDumpTests for dump-based validation - Add design document explaining the architecture and migration path Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/design/datacontracts/StressLog.md | 6 + src/coreclr/inc/sospriv.idl | 119 +++++++ .../Runtime/datadescriptor/datadescriptor.inc | 1 + .../vm/datadescriptor/datadescriptor.inc | 1 + .../ContractRegistry.cs | 4 + .../Contracts/IStressLog.cs | 2 + .../Contracts/StressLog.cs | 2 + .../Data/StressLog.cs | 1 + .../ISOSDacInterface.cs | 109 +++++++ .../SOSDacImpl.cs | 307 +++++++++++++++++- .../DumpTests/Debuggees/StressLog/Program.cs | 28 ++ .../Debuggees/StressLog/StressLog.csproj | 5 + .../tests/DumpTests/StressLogDumpTests.cs | 68 ++++ 13 files changed, 652 insertions(+), 1 deletion(-) create mode 100644 src/native/managed/cdac/tests/DumpTests/Debuggees/StressLog/Program.cs create mode 100644 src/native/managed/cdac/tests/DumpTests/Debuggees/StressLog/StressLog.csproj create mode 100644 src/native/managed/cdac/tests/DumpTests/StressLogDumpTests.cs diff --git a/docs/design/datacontracts/StressLog.md b/docs/design/datacontracts/StressLog.md index 3b90a216cfdf50..c761b8b06938be 100644 --- a/docs/design/datacontracts/StressLog.md +++ b/docs/design/datacontracts/StressLog.md @@ -13,9 +13,11 @@ internal record struct StressLogData( int TotalChunks, ulong TickFrequency, ulong StartTimestamp, + ulong StartTime, TargetPointer Logs); internal record struct ThreadStressLogData( + TargetPointer Address, TargetPointer NextPointer, ulong ThreadId, bool WriteHasWrapped, @@ -52,6 +54,7 @@ Data descriptors used: | StressLog | TotalChunks | Total number of chunks across all thread-specific logs | | StressLog | TickFrequency | Number of ticks per second for stresslog timestamps | | StressLog | StartTimestamp | Timestamp when the stress log was started | +| StressLog | StartTime | Wall-clock time when the stress log was started (FILETIME, 100ns units since Jan 1 1601) | | StressLog | ModuleOffset | Offset of the module in the stress log | | StressLog | Modules | Offset of the stress log's module table (if StressLogHasModuleTable is `1`) | | StressLog | Logs | Pointer to the thread-specific logs | @@ -104,6 +107,7 @@ StressLogData GetStressLogData() stressLog.TotalChunks, stressLog.TickFrequency, stressLog.StartTimestamp, + stressLog.StartTime, stressLog.Logs); } @@ -118,6 +122,7 @@ StressLogData GetStressLogData(TargetPointer stressLogPointer) stressLog.TotalChunks, stressLog.TickFrequency, stressLog.StartTimestamp, + stressLog.StartTime, stressLog.Logs); } @@ -151,6 +156,7 @@ IEnumerable GetThreadStressLogs(TargetPointer logs) } yield return new ThreadStressLogData( + currentPointer, threadStressLog.Next, threadStressLog.ThreadId, threadStressLog.WriteHasWrapped, diff --git a/src/coreclr/inc/sospriv.idl b/src/coreclr/inc/sospriv.idl index 0820d20386b9e0..ea4e928f49b0d5 100644 --- a/src/coreclr/inc/sospriv.idl +++ b/src/coreclr/inc/sospriv.idl @@ -572,3 +572,122 @@ interface ISOSDacInterface16 : IUnknown { HRESULT GetGCDynamicAdaptationMode(int* pDynamicAdaptationMode); } + +cpp_quote("#ifndef _DacpStressLogData_") +cpp_quote("#define _DacpStressLogData_") + +typedef struct _DacpStressLogData +{ + unsigned int LoggedFacilities; + unsigned int Level; + unsigned int MaxSizePerThread; + unsigned int MaxSizeTotal; + int TotalChunks; + UINT64 TickFrequency; + UINT64 StartTimestamp; + UINT64 StartTime; + CLRDATA_ADDRESS Logs; + unsigned int StressMsgHeaderSize; + unsigned int ChunkSize; + unsigned int MaxMessageSize; + unsigned int PointerSize; +} DacpStressLogData; + +cpp_quote("#endif //_DacpStressLogData_") + +cpp_quote("#ifndef _DacpThreadStressLogData_") +cpp_quote("#define _DacpThreadStressLogData_") + +typedef struct _DacpThreadStressLogData +{ + CLRDATA_ADDRESS ThreadLogAddress; + UINT64 ThreadId; + int WriteHasWrapped; + CLRDATA_ADDRESS CurrentPointer; + CLRDATA_ADDRESS ChunkListHead; + CLRDATA_ADDRESS ChunkListTail; + CLRDATA_ADDRESS CurrentWriteChunk; +} DacpThreadStressLogData; + +cpp_quote("#endif //_DacpThreadStressLogData_") + +cpp_quote("#ifndef _DacpStressMsgData_") +cpp_quote("#define _DacpStressMsgData_") + +typedef struct _DacpStressMsgData +{ + unsigned int Facility; + CLRDATA_ADDRESS FormatString; + UINT64 Timestamp; + unsigned int ArgumentCount; + CLRDATA_ADDRESS MessageAddress; +} DacpStressMsgData; + +cpp_quote("#endif //_DacpStressMsgData_") + +cpp_quote("#ifndef _DacpStressLogMemoryRange_") +cpp_quote("#define _DacpStressLogMemoryRange_") + +typedef struct _DacpStressLogMemoryRange +{ + CLRDATA_ADDRESS Address; + UINT64 Size; +} DacpStressLogMemoryRange; + +cpp_quote("#endif //_DacpStressLogMemoryRange_") + +[ + object, + local, + uuid(94a2bd3d-ab3d-43bf-81d8-3ae96b8e33cd) +] +interface ISOSStressLogThreadEnum : ISOSEnum +{ + HRESULT Next([in] unsigned int count, + [out, size_is(count), length_is(*pFetched)] DacpThreadStressLogData values[], + [out] unsigned int *pFetched); +} + +[ + object, + local, + uuid(437cb033-afe7-4c0f-a4a7-82c891bc049e) +] +interface ISOSStressLogMsgEnum : ISOSEnum +{ + HRESULT Next([in] unsigned int count, + [out, size_is(count), length_is(*pFetched)] DacpStressMsgData values[], + [out] unsigned int *pFetched); + + HRESULT GetArguments([in] unsigned int messageIndex, + [in] unsigned int argCount, + [out, size_is(argCount), length_is(*pFetched)] CLRDATA_ADDRESS args[], + [out] unsigned int *pFetched); +} + +[ + object, + local, + uuid(8e20713e-960c-4be4-bd55-edb9a618bc8d) +] +interface ISOSStressLogMemoryEnum : ISOSEnum +{ + HRESULT Next([in] unsigned int count, + [out, size_is(count), length_is(*pFetched)] DacpStressLogMemoryRange values[], + [out] unsigned int *pFetched); +} + +[ + object, + local, + uuid(2f4bb585-ed50-479e-bbe0-10a95a5da3bb) +] +interface ISOSDacInterface17 : IUnknown +{ + HRESULT IsStressLogAvailable(); + HRESULT GetStressLogData([out] DacpStressLogData *data); + HRESULT GetStressLogThreadEnumerator([out] ISOSStressLogThreadEnum **ppEnum); + HRESULT GetStressLogMessageEnumerator([in] CLRDATA_ADDRESS threadStressLogAddress, + [out] ISOSStressLogMsgEnum **ppEnum); + HRESULT GetStressLogMemoryRanges([out] ISOSStressLogMemoryEnum **ppEnum); +} diff --git a/src/coreclr/nativeaot/Runtime/datadescriptor/datadescriptor.inc b/src/coreclr/nativeaot/Runtime/datadescriptor/datadescriptor.inc index 365abce8061d29..702cbd20889392 100644 --- a/src/coreclr/nativeaot/Runtime/datadescriptor/datadescriptor.inc +++ b/src/coreclr/nativeaot/Runtime/datadescriptor/datadescriptor.inc @@ -94,6 +94,7 @@ CDAC_TYPE_FIELD(StressLog, T_INT32, TotalChunks, offsetof(StressLog, totalChunk) CDAC_TYPE_FIELD(StressLog, T_POINTER, Logs, offsetof(StressLog, logs)) CDAC_TYPE_FIELD(StressLog, T_UINT64, TickFrequency, offsetof(StressLog, tickFrequency)) CDAC_TYPE_FIELD(StressLog, T_UINT64, StartTimestamp, offsetof(StressLog, startTimeStamp)) +CDAC_TYPE_FIELD(StressLog, T_UINT64, StartTime, offsetof(StressLog, startTime)) CDAC_TYPE_END(StressLog) CDAC_TYPE_BEGIN(ThreadStressLog) diff --git a/src/coreclr/vm/datadescriptor/datadescriptor.inc b/src/coreclr/vm/datadescriptor/datadescriptor.inc index 0cc320903e7560..9df65e123cc5bd 100644 --- a/src/coreclr/vm/datadescriptor/datadescriptor.inc +++ b/src/coreclr/vm/datadescriptor/datadescriptor.inc @@ -568,6 +568,7 @@ CDAC_TYPE_FIELD(StressLog, T_UINT32, TotalChunks, cdac_offsets::total CDAC_TYPE_FIELD(StressLog, T_POINTER, Logs, cdac_offsets::logs) CDAC_TYPE_FIELD(StressLog, T_UINT64, TickFrequency, cdac_offsets::tickFrequency) CDAC_TYPE_FIELD(StressLog, T_UINT64, StartTimestamp, cdac_offsets::startTimeStamp) +CDAC_TYPE_FIELD(StressLog, T_UINT64, StartTime, cdac_offsets::startTime) CDAC_TYPE_FIELD(StressLog, T_NUINT, ModuleOffset, cdac_offsets::moduleOffset) CDAC_TYPE_FIELD(StressLog, T_ARRAY(TYPE(StressLogModuleDesc)), Modules, cdac_offsets::modules) CDAC_TYPE_END(StressLog) diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/ContractRegistry.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/ContractRegistry.cs index 5c7120fc517158..76b7ca71eb0537 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/ContractRegistry.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/ContractRegistry.cs @@ -128,6 +128,10 @@ public abstract class ContractRegistry /// Gets an instance of the Debugger contract for the target. /// public virtual IDebugger Debugger => GetContract(); + /// + /// Gets an instance of the StressLog contract for the target. + /// + public virtual IStressLog StressLog => GetContract(); /// /// Gets an instance of the RuntimeMutableTypeSystem contract for the target. diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IStressLog.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IStressLog.cs index 40535e3d1c834d..4bd41735561628 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IStressLog.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IStressLog.cs @@ -14,9 +14,11 @@ public record struct StressLogData( int TotalChunks, ulong TickFrequency, ulong StartTimestamp, + ulong StartTime, TargetPointer Logs); public record struct ThreadStressLogData( + TargetPointer Address, TargetPointer NextPointer, ulong ThreadId, bool WriteHasWrapped, diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StressLog.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StressLog.cs index b3eb52892a0e17..c5d0d7b82ce7cd 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StressLog.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StressLog.cs @@ -46,6 +46,7 @@ public StressLogData GetStressLogData(TargetPointer stressLogPointer) stressLog.TotalChunks, stressLog.TickFrequency, stressLog.StartTimestamp, + stressLog.StartTime, stressLog.Logs); } @@ -79,6 +80,7 @@ public IEnumerable GetThreadStressLogs(TargetPointer Logs) } yield return new ThreadStressLogData( + currentPointer, threadStressLog.Next, threadStressLog.ThreadId, threadStressLog.WriteHasWrapped, diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/StressLog.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/StressLog.cs index abc16be4369e45..b39fa051dfa5ff 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/StressLog.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/StressLog.cs @@ -13,6 +13,7 @@ internal sealed partial class StressLog : IData [Field] public int TotalChunks { get; } [Field] public ulong TickFrequency { get; } [Field] public ulong StartTimestamp { get; } + [Field] public ulong StartTime { get; } [Field] public TargetNUInt ModuleOffset { get; } [Field] public TargetPointer? Modules { get; } [Field] public TargetPointer Logs { get; } 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 06c887b7ffd89b..669f4996b6452e 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/ISOSDacInterface.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/ISOSDacInterface.cs @@ -1191,3 +1191,112 @@ public unsafe partial interface ISOSDacInterface16 [PreserveSig] int GetGCDynamicAdaptationMode(int* pDynamicAdaptationMode); } + +// StressLog data structures for ISOSDacInterface17 + +public struct DacpStressLogData +{ + public uint LoggedFacilities; + public uint Level; + public uint MaxSizePerThread; + public uint MaxSizeTotal; + public int TotalChunks; + public ulong TickFrequency; + public ulong StartTimestamp; + public ulong StartTime; + public ClrDataAddress Logs; + public uint StressMsgHeaderSize; + public uint ChunkSize; + public uint MaxMessageSize; + public uint PointerSize; +} + +public struct DacpThreadStressLogData +{ + public ClrDataAddress ThreadLogAddress; + public ulong ThreadId; + public int WriteHasWrapped; + public ClrDataAddress CurrentPointer; + public ClrDataAddress ChunkListHead; + public ClrDataAddress ChunkListTail; + public ClrDataAddress CurrentWriteChunk; +} + +public struct DacpStressMsgData +{ + public uint Facility; + public ClrDataAddress FormatString; + public ulong Timestamp; + public uint ArgumentCount; + public ClrDataAddress MessageAddress; +} + +public struct DacpStressLogMemoryRange +{ + public ClrDataAddress Address; + public ulong Size; +} + +[GeneratedComInterface] +[Guid("94a2bd3d-ab3d-43bf-81d8-3ae96b8e33cd")] +public unsafe partial interface ISOSStressLogThreadEnum : ISOSEnum +{ + [PreserveSig] + int Next(uint count, + [In, Out, MarshalUsing(CountElementName = nameof(count))] + DacpThreadStressLogData[] values, + uint* pFetched); +} + +[GeneratedComInterface] +[Guid("437cb033-afe7-4c0f-a4a7-82c891bc049e")] +public unsafe partial interface ISOSStressLogMsgEnum : ISOSEnum +{ + [PreserveSig] + int Next(uint count, + [In, Out, MarshalUsing(CountElementName = nameof(count))] + DacpStressMsgData[] values, + uint* pFetched); + + [PreserveSig] + int GetArguments(uint messageIndex, + uint argCount, + [In, Out, MarshalUsing(CountElementName = nameof(argCount))] + ClrDataAddress[] args, + uint* pFetched); +} + +[GeneratedComInterface] +[Guid("8e20713e-960c-4be4-bd55-edb9a618bc8d")] +public unsafe partial interface ISOSStressLogMemoryEnum : ISOSEnum +{ + [PreserveSig] + int Next(uint count, + [In, Out, MarshalUsing(CountElementName = nameof(count))] + DacpStressLogMemoryRange[] values, + uint* pFetched); +} + +[GeneratedComInterface] +[Guid("2f4bb585-ed50-479e-bbe0-10a95a5da3bb")] +public unsafe partial interface ISOSDacInterface17 +{ + [PreserveSig] + int IsStressLogAvailable(); + + [PreserveSig] + int GetStressLogData(DacpStressLogData* data); + + [PreserveSig] + int GetStressLogThreadEnumerator( + DacComNullableByRef ppEnum); + + [PreserveSig] + int GetStressLogMessageEnumerator( + ClrDataAddress threadStressLogAddress, + DacComNullableByRef ppEnum); + + [PreserveSig] + int GetStressLogMemoryRanges( + DacComNullableByRef ppEnum); +} 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 2007bb81728a1e..e67d828a4129a0 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.cs @@ -33,7 +33,8 @@ public sealed unsafe partial class SOSDacImpl : ISOSDacInterface, ISOSDacInterface2, ISOSDacInterface3, ISOSDacInterface4, ISOSDacInterface5, ISOSDacInterface6, ISOSDacInterface7, ISOSDacInterface8, ISOSDacInterface9, ISOSDacInterface10, ISOSDacInterface11, ISOSDacInterface12, ISOSDacInterface13, ISOSDacInterface14, ISOSDacInterface15, - ISOSDacInterface16 + ISOSDacInterface16, + ISOSDacInterface17 { private readonly Target _target; @@ -7113,4 +7114,308 @@ int ISOSDacInterface16.GetGCDynamicAdaptationMode(int* pDynamicAdaptationMode) return hr; } #endregion ISOSDacInterface16 + + #region ISOSDacInterface17 + + internal sealed unsafe partial class SOSStressLogThreadEnum : ISOSStressLogThreadEnum + { + private readonly DacpThreadStressLogData[] _threads; + private uint _index; + + public SOSStressLogThreadEnum(DacpThreadStressLogData[] threads) + { + _threads = threads; + } + + int ISOSStressLogThreadEnum.Next(uint count, DacpThreadStressLogData[] values, uint* pFetched) + { + if (pFetched is null || values is null) + return HResults.E_POINTER; + + count = Math.Min(count, (uint)values.Length); + uint written = 0; + while (written < count && _index < _threads.Length) + values[written++] = _threads[(int)_index++]; + + *pFetched = written; + return _index < _threads.Length ? HResults.S_FALSE : HResults.S_OK; + } + + int ISOSEnum.Skip(uint count) + { + _index = Math.Min(_index + count, (uint)_threads.Length); + 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)_threads.Length; + return HResults.S_OK; + } + } + + internal sealed unsafe partial class SOSStressLogMsgEnum : ISOSStressLogMsgEnum + { + private readonly Target _target; + private readonly IEnumerator _enumerator; + private DacpStressMsgData[]? _lastBatch; + private Contracts.StressMsgData[]? _lastBatchRaw; + private bool _exhausted; + + public SOSStressLogMsgEnum(Target target, IEnumerable messages) + { + _target = target; + _enumerator = messages.GetEnumerator(); + } + + int ISOSStressLogMsgEnum.Next(uint count, DacpStressMsgData[] values, uint* pFetched) + { + if (pFetched is null || values is null) + return HResults.E_POINTER; + + count = Math.Min(count, (uint)values.Length); + _lastBatch = new DacpStressMsgData[count]; + _lastBatchRaw = new Contracts.StressMsgData[count]; + uint written = 0; + + while (written < count && !_exhausted) + { + if (!_enumerator.MoveNext()) + { + _exhausted = true; + break; + } + + Contracts.StressMsgData msg = _enumerator.Current; + _lastBatchRaw[written] = msg; + values[written] = new DacpStressMsgData + { + Facility = msg.Facility, + FormatString = msg.FormatString.ToClrDataAddress(_target), + Timestamp = msg.Timestamp, + ArgumentCount = (uint)msg.Args.Count, + MessageAddress = 0, + }; + _lastBatch[written] = values[written]; + written++; + } + + *pFetched = written; + return _exhausted ? HResults.S_OK : HResults.S_FALSE; + } + + int ISOSStressLogMsgEnum.GetArguments(uint messageIndex, uint argCount, ClrDataAddress[] args, uint* pFetched) + { + if (pFetched is null || args is null) + return HResults.E_POINTER; + + if (_lastBatchRaw is null || messageIndex >= _lastBatchRaw.Length) + return HResults.E_INVALIDARG; + + Contracts.StressMsgData msg = _lastBatchRaw[messageIndex]; + uint toFetch = Math.Min(argCount, (uint)msg.Args.Count); + toFetch = Math.Min(toFetch, (uint)args.Length); + + for (uint i = 0; i < toFetch; i++) + args[i] = msg.Args[(int)i].ToClrDataAddress(_target); + + *pFetched = toFetch; + return HResults.S_OK; + } + + int ISOSEnum.Skip(uint count) + { + for (uint i = 0; i < count && !_exhausted; i++) + { + if (!_enumerator.MoveNext()) + _exhausted = true; + } + return HResults.S_OK; + } + + int ISOSEnum.Reset() + { + return HResults.E_NOTIMPL; + } + + int ISOSEnum.GetCount(uint* pCount) + { + return HResults.E_NOTIMPL; + } + } + + internal sealed unsafe partial class SOSStressLogMemoryEnum : ISOSStressLogMemoryEnum + { + private readonly DacpStressLogMemoryRange[] _ranges; + private uint _index; + + public SOSStressLogMemoryEnum(DacpStressLogMemoryRange[] ranges) + { + _ranges = ranges; + } + + int ISOSStressLogMemoryEnum.Next(uint count, DacpStressLogMemoryRange[] values, uint* pFetched) + { + if (pFetched is null || values is null) + return HResults.E_POINTER; + + count = Math.Min(count, (uint)values.Length); + uint written = 0; + while (written < count && _index < _ranges.Length) + values[written++] = _ranges[(int)_index++]; + + *pFetched = written; + return _index < _ranges.Length ? HResults.S_FALSE : HResults.S_OK; + } + + int ISOSEnum.Skip(uint count) + { + _index = Math.Min(_index + count, (uint)_ranges.Length); + 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)_ranges.Length; + return HResults.S_OK; + } + } + + int ISOSDacInterface17.IsStressLogAvailable() + { + try + { + return _target.Contracts.StressLog.HasStressLog() + ? HResults.S_OK + : HResults.S_FALSE; + } + catch + { + return HResults.E_FAIL; + } + } + + int ISOSDacInterface17.GetStressLogData(DacpStressLogData* data) + { + int hr = HResults.S_OK; + try + { + if (data is null) + throw new ArgumentException(); + + Contracts.IStressLog stressLogContract = _target.Contracts.StressLog; + if (!stressLogContract.HasStressLog()) + { + *data = default; + return HResults.S_FALSE; + } + + Contracts.StressLogData logData = stressLogContract.GetStressLogData(); + data->LoggedFacilities = logData.LoggedFacilities; + data->Level = logData.Level; + data->MaxSizePerThread = logData.MaxSizePerThread; + data->MaxSizeTotal = logData.MaxSizeTotal; + data->TotalChunks = logData.TotalChunks; + data->TickFrequency = logData.TickFrequency; + data->StartTimestamp = logData.StartTimestamp; + data->StartTime = logData.StartTime; + data->Logs = logData.Logs.ToClrDataAddress(_target); + data->StressMsgHeaderSize = _target.GetTypeInfo(DataType.StressMsgHeader).Size ?? 0; + data->ChunkSize = _target.ReadGlobal(Constants.Globals.StressLogChunkSize); + data->MaxMessageSize = _target.ReadGlobal(Constants.Globals.StressLogMaxMessageSize); + data->PointerSize = (uint)_target.PointerSize; + } + catch (System.Exception ex) + { + hr = ex.HResult; + } + + return hr; + } + + int ISOSDacInterface17.GetStressLogThreadEnumerator(DacComNullableByRef ppEnum) + { + int hr = HResults.S_OK; + try + { + Contracts.IStressLog stressLogContract = _target.Contracts.StressLog; + Contracts.StressLogData logData = stressLogContract.GetStressLogData(); + + var threads = stressLogContract.GetThreadStressLogs(logData.Logs) + .Select(t => new DacpThreadStressLogData + { + ThreadLogAddress = t.Address.ToClrDataAddress(_target), + ThreadId = t.ThreadId, + WriteHasWrapped = t.WriteHasWrapped ? 1 : 0, + CurrentPointer = t.CurrentPointer.ToClrDataAddress(_target), + ChunkListHead = t.ChunkListHead.ToClrDataAddress(_target), + ChunkListTail = t.ChunkListTail.ToClrDataAddress(_target), + CurrentWriteChunk = t.CurrentWriteChunk.ToClrDataAddress(_target), + }) + .ToArray(); + ppEnum.Interface = new SOSStressLogThreadEnum(threads); + } + catch (System.Exception ex) + { + hr = ex.HResult; + } + + return hr; + } + + int ISOSDacInterface17.GetStressLogMessageEnumerator( + ClrDataAddress threadStressLogAddress, + DacComNullableByRef ppEnum) + { + int hr = HResults.S_OK; + try + { + Contracts.IStressLog stressLogContract = _target.Contracts.StressLog; + Contracts.StressLogData logData = stressLogContract.GetStressLogData(); + + // Find the matching thread + Contracts.ThreadStressLogData? matchedThread = null; + foreach (var thread in stressLogContract.GetThreadStressLogs(logData.Logs)) + { + if (thread.Address == (TargetPointer)(ulong)threadStressLogAddress) + { + matchedThread = thread; + break; + } + } + + if (matchedThread is null) + return HResults.E_INVALIDARG; + + IEnumerable messages = stressLogContract.GetStressMessages(matchedThread.Value); + ppEnum.Interface = new SOSStressLogMsgEnum(_target, messages); + } + catch (System.Exception ex) + { + hr = ex.HResult; + } + + return hr; + } + + int ISOSDacInterface17.GetStressLogMemoryRanges(DacComNullableByRef ppEnum) + { + return HResults.E_NOTIMPL; + } + + #endregion ISOSDacInterface17 } diff --git a/src/native/managed/cdac/tests/DumpTests/Debuggees/StressLog/Program.cs b/src/native/managed/cdac/tests/DumpTests/Debuggees/StressLog/Program.cs new file mode 100644 index 00000000000000..ba1fef6add0858 --- /dev/null +++ b/src/native/managed/cdac/tests/DumpTests/Debuggees/StressLog/Program.cs @@ -0,0 +1,28 @@ +// 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; + +/// +/// Debuggee app for cDAC stress log dump tests. +/// Performs allocations and GC to generate stress log entries, then crashes +/// so a dump is produced for analysis. +/// +internal static class Program +{ + private static void Main() + { + // Perform some work to generate stress log entries. + // GC operations produce many log messages across facilities. + var list = new List(); + for (int i = 0; i < 1000; i++) + list.Add(new byte[1024]); + + GC.Collect(); + GC.WaitForPendingFinalizers(); + GC.Collect(); + + Environment.FailFast("cDAC dump test: StressLog debuggee intentional crash"); + } +} diff --git a/src/native/managed/cdac/tests/DumpTests/Debuggees/StressLog/StressLog.csproj b/src/native/managed/cdac/tests/DumpTests/Debuggees/StressLog/StressLog.csproj new file mode 100644 index 00000000000000..e1cc07a08d570a --- /dev/null +++ b/src/native/managed/cdac/tests/DumpTests/Debuggees/StressLog/StressLog.csproj @@ -0,0 +1,5 @@ + + + DOTNET_StressLog=1;DOTNET_LogFacility=0xffffffbf;DOTNET_LogLevel=6;DOTNET_StressLogSize=65536 + + diff --git a/src/native/managed/cdac/tests/DumpTests/StressLogDumpTests.cs b/src/native/managed/cdac/tests/DumpTests/StressLogDumpTests.cs new file mode 100644 index 00000000000000..5a8b1c3da67bc3 --- /dev/null +++ b/src/native/managed/cdac/tests/DumpTests/StressLogDumpTests.cs @@ -0,0 +1,68 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Linq; +using Microsoft.Diagnostics.DataContractReader.Contracts; +using Microsoft.Diagnostics.DataContractReader.TestInfrastructure; +using Xunit; + +namespace Microsoft.Diagnostics.DataContractReader.DumpTests; + +/// +/// Dump-based integration tests for the StressLog contract. +/// Uses the StressLog debuggee dump, which enables stress logging via environment +/// variables, performs allocations and GC, then crashes. +/// +public class StressLogDumpTests : DumpTestBase +{ + protected override string DebuggeeName => "StressLog"; + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + public void StressLogIsAvailable(TestConfiguration config) + { + InitializeDumpTest(config); + IStressLog stressLog = Target.Contracts.StressLog; + Assert.True(stressLog.HasStressLog()); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + public void StressLogDataIsValid(TestConfiguration config) + { + InitializeDumpTest(config); + IStressLog stressLog = Target.Contracts.StressLog; + StressLogData data = stressLog.GetStressLogData(); + + Assert.NotEqual(0UL, data.TickFrequency); + Assert.NotEqual(0UL, data.StartTimestamp); + Assert.NotEqual(0UL, data.StartTime); + Assert.NotEqual(TargetPointer.Null, data.Logs); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + public void CanEnumerateThreadsAndMessages(TestConfiguration config) + { + InitializeDumpTest(config); + IStressLog stressLog = Target.Contracts.StressLog; + StressLogData data = stressLog.GetStressLogData(); + + var threads = stressLog.GetThreadStressLogs(data.Logs).ToList(); + Assert.NotEmpty(threads); + + bool foundMessages = false; + foreach (ThreadStressLogData thread in threads) + { + var messages = stressLog.GetStressMessages(thread).Take(10).ToList(); + if (messages.Count > 0) + { + foundMessages = true; + Assert.NotEqual(0UL, messages[0].Timestamp); + Assert.NotEqual(TargetPointer.Null, messages[0].FormatString); + break; + } + } + Assert.True(foundMessages, "Expected at least one thread with stress log messages"); + } +} From bd3de83c6195bdbda612e67672fb1248a1014d28 Mon Sep 17 00:00:00 2001 From: Max Charlamb Date: Thu, 11 Jun 2026 11:21:12 -0400 Subject: [PATCH 02/18] Address Copilot review feedback - Add [GeneratedComClass] to all three enumerator classes - Use ToTargetPointer for ClrDataAddress comparison - Shrink _lastBatchRaw to actual fetched size - Return ex.HResult instead of E_FAIL in IsStressLogAvailable Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../SOSDacImpl.cs | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) 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 e67d828a4129a0..82e002ba0b5a19 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.cs @@ -7117,6 +7117,7 @@ int ISOSDacInterface16.GetGCDynamicAdaptationMode(int* pDynamicAdaptationMode) #region ISOSDacInterface17 + [GeneratedComClass] internal sealed unsafe partial class SOSStressLogThreadEnum : ISOSStressLogThreadEnum { private readonly DacpThreadStressLogData[] _threads; @@ -7161,6 +7162,7 @@ int ISOSEnum.GetCount(uint* pCount) } } + [GeneratedComClass] internal sealed unsafe partial class SOSStressLogMsgEnum : ISOSStressLogMsgEnum { private readonly Target _target; @@ -7208,6 +7210,14 @@ int ISOSStressLogMsgEnum.Next(uint count, DacpStressMsgData[] values, uint* pFet } *pFetched = written; + + // Shrink to actual size so GetArguments bounds check is safe + if (written < count) + { + Array.Resize(ref _lastBatchRaw, (int)written); + Array.Resize(ref _lastBatch, (int)written); + } + return _exhausted ? HResults.S_OK : HResults.S_FALSE; } @@ -7251,6 +7261,7 @@ int ISOSEnum.GetCount(uint* pCount) } } + [GeneratedComClass] internal sealed unsafe partial class SOSStressLogMemoryEnum : ISOSStressLogMemoryEnum { private readonly DacpStressLogMemoryRange[] _ranges; @@ -7303,9 +7314,9 @@ int ISOSDacInterface17.IsStressLogAvailable() ? HResults.S_OK : HResults.S_FALSE; } - catch + catch (System.Exception ex) { - return HResults.E_FAIL; + return ex.HResult; } } @@ -7391,7 +7402,7 @@ int ISOSDacInterface17.GetStressLogMessageEnumerator( Contracts.ThreadStressLogData? matchedThread = null; foreach (var thread in stressLogContract.GetThreadStressLogs(logData.Logs)) { - if (thread.Address == (TargetPointer)(ulong)threadStressLogAddress) + if (thread.Address == threadStressLogAddress.ToTargetPointer(_target)) { matchedThread = thread; break; From f31def657a29be3e961e23feaa7b2455c2266241 Mon Sep 17 00:00:00 2001 From: Max Charlamb Date: Tue, 16 Jun 2026 11:50:14 -0400 Subject: [PATCH 03/18] Remove unused StressLogMemoryRange API and improve SOSStressLogMsgEnum - Remove DacpStressLogMemoryRange, ISOSStressLogMemoryEnum, and GetStressLogMemoryRanges from IDL, managed interface, and impl since no SOS consumer uses them. - Refactor SOSStressLogMsgEnum to eagerly materialize messages into an array, enabling Reset and GetCount support consistent with other ISOSEnum implementations. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/coreclr/inc/sospriv.idl | 24 ----- .../ISOSDacInterface.cs | 20 ---- .../SOSDacImpl.cs | 95 +++---------------- 3 files changed, 14 insertions(+), 125 deletions(-) diff --git a/src/coreclr/inc/sospriv.idl b/src/coreclr/inc/sospriv.idl index ea4e928f49b0d5..e916e89a22bda1 100644 --- a/src/coreclr/inc/sospriv.idl +++ b/src/coreclr/inc/sospriv.idl @@ -625,17 +625,6 @@ typedef struct _DacpStressMsgData cpp_quote("#endif //_DacpStressMsgData_") -cpp_quote("#ifndef _DacpStressLogMemoryRange_") -cpp_quote("#define _DacpStressLogMemoryRange_") - -typedef struct _DacpStressLogMemoryRange -{ - CLRDATA_ADDRESS Address; - UINT64 Size; -} DacpStressLogMemoryRange; - -cpp_quote("#endif //_DacpStressLogMemoryRange_") - [ object, local, @@ -665,18 +654,6 @@ interface ISOSStressLogMsgEnum : ISOSEnum [out] unsigned int *pFetched); } -[ - object, - local, - uuid(8e20713e-960c-4be4-bd55-edb9a618bc8d) -] -interface ISOSStressLogMemoryEnum : ISOSEnum -{ - HRESULT Next([in] unsigned int count, - [out, size_is(count), length_is(*pFetched)] DacpStressLogMemoryRange values[], - [out] unsigned int *pFetched); -} - [ object, local, @@ -689,5 +666,4 @@ interface ISOSDacInterface17 : IUnknown HRESULT GetStressLogThreadEnumerator([out] ISOSStressLogThreadEnum **ppEnum); HRESULT GetStressLogMessageEnumerator([in] CLRDATA_ADDRESS threadStressLogAddress, [out] ISOSStressLogMsgEnum **ppEnum); - HRESULT GetStressLogMemoryRanges([out] ISOSStressLogMemoryEnum **ppEnum); } 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 669f4996b6452e..9e8918548ad108 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/ISOSDacInterface.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/ISOSDacInterface.cs @@ -1231,12 +1231,6 @@ public struct DacpStressMsgData public ClrDataAddress MessageAddress; } -public struct DacpStressLogMemoryRange -{ - public ClrDataAddress Address; - public ulong Size; -} - [GeneratedComInterface] [Guid("94a2bd3d-ab3d-43bf-81d8-3ae96b8e33cd")] public unsafe partial interface ISOSStressLogThreadEnum : ISOSEnum @@ -1266,17 +1260,6 @@ int GetArguments(uint messageIndex, uint* pFetched); } -[GeneratedComInterface] -[Guid("8e20713e-960c-4be4-bd55-edb9a618bc8d")] -public unsafe partial interface ISOSStressLogMemoryEnum : ISOSEnum -{ - [PreserveSig] - int Next(uint count, - [In, Out, MarshalUsing(CountElementName = nameof(count))] - DacpStressLogMemoryRange[] values, - uint* pFetched); -} - [GeneratedComInterface] [Guid("2f4bb585-ed50-479e-bbe0-10a95a5da3bb")] public unsafe partial interface ISOSDacInterface17 @@ -1296,7 +1279,4 @@ int GetStressLogMessageEnumerator( ClrDataAddress threadStressLogAddress, DacComNullableByRef ppEnum); - [PreserveSig] - int GetStressLogMemoryRanges( - DacComNullableByRef ppEnum); } 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 82e002ba0b5a19..453937580dd7b9 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.cs @@ -7166,15 +7166,15 @@ int ISOSEnum.GetCount(uint* pCount) internal sealed unsafe partial class SOSStressLogMsgEnum : ISOSStressLogMsgEnum { private readonly Target _target; - private readonly IEnumerator _enumerator; - private DacpStressMsgData[]? _lastBatch; - private Contracts.StressMsgData[]? _lastBatchRaw; - private bool _exhausted; + private readonly Contracts.StressMsgData[] _messages; + private uint _index; + private uint _lastBatchStart; + private uint _lastBatchCount; public SOSStressLogMsgEnum(Target target, IEnumerable messages) { _target = target; - _enumerator = messages.GetEnumerator(); + _messages = messages.ToArray(); } int ISOSStressLogMsgEnum.Next(uint count, DacpStressMsgData[] values, uint* pFetched) @@ -7183,20 +7183,12 @@ int ISOSStressLogMsgEnum.Next(uint count, DacpStressMsgData[] values, uint* pFet return HResults.E_POINTER; count = Math.Min(count, (uint)values.Length); - _lastBatch = new DacpStressMsgData[count]; - _lastBatchRaw = new Contracts.StressMsgData[count]; + _lastBatchStart = _index; uint written = 0; - while (written < count && !_exhausted) + while (written < count && _index < _messages.Length) { - if (!_enumerator.MoveNext()) - { - _exhausted = true; - break; - } - - Contracts.StressMsgData msg = _enumerator.Current; - _lastBatchRaw[written] = msg; + Contracts.StressMsgData msg = _messages[(int)_index++]; values[written] = new DacpStressMsgData { Facility = msg.Facility, @@ -7205,20 +7197,12 @@ int ISOSStressLogMsgEnum.Next(uint count, DacpStressMsgData[] values, uint* pFet ArgumentCount = (uint)msg.Args.Count, MessageAddress = 0, }; - _lastBatch[written] = values[written]; written++; } + _lastBatchCount = written; *pFetched = written; - - // Shrink to actual size so GetArguments bounds check is safe - if (written < count) - { - Array.Resize(ref _lastBatchRaw, (int)written); - Array.Resize(ref _lastBatch, (int)written); - } - - return _exhausted ? HResults.S_OK : HResults.S_FALSE; + return _index < _messages.Length ? HResults.S_FALSE : HResults.S_OK; } int ISOSStressLogMsgEnum.GetArguments(uint messageIndex, uint argCount, ClrDataAddress[] args, uint* pFetched) @@ -7226,10 +7210,10 @@ int ISOSStressLogMsgEnum.GetArguments(uint messageIndex, uint argCount, ClrDataA if (pFetched is null || args is null) return HResults.E_POINTER; - if (_lastBatchRaw is null || messageIndex >= _lastBatchRaw.Length) + if (messageIndex >= _lastBatchCount) return HResults.E_INVALIDARG; - Contracts.StressMsgData msg = _lastBatchRaw[messageIndex]; + Contracts.StressMsgData msg = _messages[(int)(_lastBatchStart + messageIndex)]; uint toFetch = Math.Min(argCount, (uint)msg.Args.Count); toFetch = Math.Min(toFetch, (uint)args.Length); @@ -7242,53 +7226,7 @@ int ISOSStressLogMsgEnum.GetArguments(uint messageIndex, uint argCount, ClrDataA int ISOSEnum.Skip(uint count) { - for (uint i = 0; i < count && !_exhausted; i++) - { - if (!_enumerator.MoveNext()) - _exhausted = true; - } - return HResults.S_OK; - } - - int ISOSEnum.Reset() - { - return HResults.E_NOTIMPL; - } - - int ISOSEnum.GetCount(uint* pCount) - { - return HResults.E_NOTIMPL; - } - } - - [GeneratedComClass] - internal sealed unsafe partial class SOSStressLogMemoryEnum : ISOSStressLogMemoryEnum - { - private readonly DacpStressLogMemoryRange[] _ranges; - private uint _index; - - public SOSStressLogMemoryEnum(DacpStressLogMemoryRange[] ranges) - { - _ranges = ranges; - } - - int ISOSStressLogMemoryEnum.Next(uint count, DacpStressLogMemoryRange[] values, uint* pFetched) - { - if (pFetched is null || values is null) - return HResults.E_POINTER; - - count = Math.Min(count, (uint)values.Length); - uint written = 0; - while (written < count && _index < _ranges.Length) - values[written++] = _ranges[(int)_index++]; - - *pFetched = written; - return _index < _ranges.Length ? HResults.S_FALSE : HResults.S_OK; - } - - int ISOSEnum.Skip(uint count) - { - _index = Math.Min(_index + count, (uint)_ranges.Length); + _index = Math.Min(_index + count, (uint)_messages.Length); return HResults.S_OK; } @@ -7301,7 +7239,7 @@ int ISOSEnum.Reset() int ISOSEnum.GetCount(uint* pCount) { if (pCount is null) return HResults.E_POINTER; - *pCount = (uint)_ranges.Length; + *pCount = (uint)_messages.Length; return HResults.S_OK; } } @@ -7423,10 +7361,5 @@ int ISOSDacInterface17.GetStressLogMessageEnumerator( return hr; } - int ISOSDacInterface17.GetStressLogMemoryRanges(DacComNullableByRef ppEnum) - { - return HResults.E_NOTIMPL; - } - #endregion ISOSDacInterface17 } From f365a0a6393f3eaeda237d540b91cf9855a3349f Mon Sep 17 00:00:00 2001 From: Max Charlamb Date: Tue, 16 Jun 2026 13:31:01 -0400 Subject: [PATCH 04/18] Rename stress log structs to SOS prefix and follow IDL conventions - Rename DacpStressLogData -> SOSStressLogData, DacpThreadStressLogData -> SOSThreadStressLogData, DacpStressMsgData -> SOSStressMsgData - Define structs inline in sospriv.idl with typedef struct and cpp_quote guards, following the SOSMethodData/SOSMemoryRegion convention used by newer interfaces - Remove Dacp forward declarations and dacprivate.h definitions - Put ISOSDacInterface16, ISOSDacInterface17 on same line in class decl Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/coreclr/inc/sospriv.idl | 36 +++++++++---------- .../ISOSDacInterface.cs | 12 +++---- .../SOSDacImpl.cs | 17 +++++---- 3 files changed, 32 insertions(+), 33 deletions(-) diff --git a/src/coreclr/inc/sospriv.idl b/src/coreclr/inc/sospriv.idl index e916e89a22bda1..b97efcb388559d 100644 --- a/src/coreclr/inc/sospriv.idl +++ b/src/coreclr/inc/sospriv.idl @@ -573,10 +573,10 @@ interface ISOSDacInterface16 : IUnknown HRESULT GetGCDynamicAdaptationMode(int* pDynamicAdaptationMode); } -cpp_quote("#ifndef _DacpStressLogData_") -cpp_quote("#define _DacpStressLogData_") +cpp_quote("#ifndef _SOS_StressLogData") +cpp_quote("#define _SOS_StressLogData") -typedef struct _DacpStressLogData +typedef struct _SOSStressLogData { unsigned int LoggedFacilities; unsigned int Level; @@ -591,14 +591,14 @@ typedef struct _DacpStressLogData unsigned int ChunkSize; unsigned int MaxMessageSize; unsigned int PointerSize; -} DacpStressLogData; +} SOSStressLogData; -cpp_quote("#endif //_DacpStressLogData_") +cpp_quote("#endif //_SOS_StressLogData") -cpp_quote("#ifndef _DacpThreadStressLogData_") -cpp_quote("#define _DacpThreadStressLogData_") +cpp_quote("#ifndef _SOS_ThreadStressLogData") +cpp_quote("#define _SOS_ThreadStressLogData") -typedef struct _DacpThreadStressLogData +typedef struct _SOSThreadStressLogData { CLRDATA_ADDRESS ThreadLogAddress; UINT64 ThreadId; @@ -607,23 +607,23 @@ typedef struct _DacpThreadStressLogData CLRDATA_ADDRESS ChunkListHead; CLRDATA_ADDRESS ChunkListTail; CLRDATA_ADDRESS CurrentWriteChunk; -} DacpThreadStressLogData; +} SOSThreadStressLogData; -cpp_quote("#endif //_DacpThreadStressLogData_") +cpp_quote("#endif //_SOS_ThreadStressLogData") -cpp_quote("#ifndef _DacpStressMsgData_") -cpp_quote("#define _DacpStressMsgData_") +cpp_quote("#ifndef _SOS_StressMsgData") +cpp_quote("#define _SOS_StressMsgData") -typedef struct _DacpStressMsgData +typedef struct _SOSStressMsgData { unsigned int Facility; CLRDATA_ADDRESS FormatString; UINT64 Timestamp; unsigned int ArgumentCount; CLRDATA_ADDRESS MessageAddress; -} DacpStressMsgData; +} SOSStressMsgData; -cpp_quote("#endif //_DacpStressMsgData_") +cpp_quote("#endif //_SOS_StressMsgData") [ object, @@ -633,7 +633,7 @@ cpp_quote("#endif //_DacpStressMsgData_") interface ISOSStressLogThreadEnum : ISOSEnum { HRESULT Next([in] unsigned int count, - [out, size_is(count), length_is(*pFetched)] DacpThreadStressLogData values[], + [out, size_is(count), length_is(*pFetched)] SOSThreadStressLogData values[], [out] unsigned int *pFetched); } @@ -645,7 +645,7 @@ interface ISOSStressLogThreadEnum : ISOSEnum interface ISOSStressLogMsgEnum : ISOSEnum { HRESULT Next([in] unsigned int count, - [out, size_is(count), length_is(*pFetched)] DacpStressMsgData values[], + [out, size_is(count), length_is(*pFetched)] SOSStressMsgData values[], [out] unsigned int *pFetched); HRESULT GetArguments([in] unsigned int messageIndex, @@ -662,7 +662,7 @@ interface ISOSStressLogMsgEnum : ISOSEnum interface ISOSDacInterface17 : IUnknown { HRESULT IsStressLogAvailable(); - HRESULT GetStressLogData([out] DacpStressLogData *data); + HRESULT GetStressLogData([out] SOSStressLogData *data); HRESULT GetStressLogThreadEnumerator([out] ISOSStressLogThreadEnum **ppEnum); HRESULT GetStressLogMessageEnumerator([in] CLRDATA_ADDRESS threadStressLogAddress, [out] ISOSStressLogMsgEnum **ppEnum); 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 9e8918548ad108..0673576cac9375 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/ISOSDacInterface.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/ISOSDacInterface.cs @@ -1194,7 +1194,7 @@ public unsafe partial interface ISOSDacInterface16 // StressLog data structures for ISOSDacInterface17 -public struct DacpStressLogData +public struct SOSStressLogData { public uint LoggedFacilities; public uint Level; @@ -1211,7 +1211,7 @@ public struct DacpStressLogData public uint PointerSize; } -public struct DacpThreadStressLogData +public struct SOSThreadStressLogData { public ClrDataAddress ThreadLogAddress; public ulong ThreadId; @@ -1222,7 +1222,7 @@ public struct DacpThreadStressLogData public ClrDataAddress CurrentWriteChunk; } -public struct DacpStressMsgData +public struct SOSStressMsgData { public uint Facility; public ClrDataAddress FormatString; @@ -1238,7 +1238,7 @@ public unsafe partial interface ISOSStressLogThreadEnum : ISOSEnum [PreserveSig] int Next(uint count, [In, Out, MarshalUsing(CountElementName = nameof(count))] - DacpThreadStressLogData[] values, + SOSThreadStressLogData[] values, uint* pFetched); } @@ -1249,7 +1249,7 @@ public unsafe partial interface ISOSStressLogMsgEnum : ISOSEnum [PreserveSig] int Next(uint count, [In, Out, MarshalUsing(CountElementName = nameof(count))] - DacpStressMsgData[] values, + SOSStressMsgData[] values, uint* pFetched); [PreserveSig] @@ -1268,7 +1268,7 @@ public unsafe partial interface ISOSDacInterface17 int IsStressLogAvailable(); [PreserveSig] - int GetStressLogData(DacpStressLogData* data); + int GetStressLogData(SOSStressLogData* data); [PreserveSig] int GetStressLogThreadEnumerator( 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 453937580dd7b9..f7437d5b04d54a 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.cs @@ -33,8 +33,7 @@ public sealed unsafe partial class SOSDacImpl : ISOSDacInterface, ISOSDacInterface2, ISOSDacInterface3, ISOSDacInterface4, ISOSDacInterface5, ISOSDacInterface6, ISOSDacInterface7, ISOSDacInterface8, ISOSDacInterface9, ISOSDacInterface10, ISOSDacInterface11, ISOSDacInterface12, ISOSDacInterface13, ISOSDacInterface14, ISOSDacInterface15, - ISOSDacInterface16, - ISOSDacInterface17 + ISOSDacInterface16, ISOSDacInterface17 { private readonly Target _target; @@ -7120,15 +7119,15 @@ int ISOSDacInterface16.GetGCDynamicAdaptationMode(int* pDynamicAdaptationMode) [GeneratedComClass] internal sealed unsafe partial class SOSStressLogThreadEnum : ISOSStressLogThreadEnum { - private readonly DacpThreadStressLogData[] _threads; + private readonly SOSThreadStressLogData[] _threads; private uint _index; - public SOSStressLogThreadEnum(DacpThreadStressLogData[] threads) + public SOSStressLogThreadEnum(SOSThreadStressLogData[] threads) { _threads = threads; } - int ISOSStressLogThreadEnum.Next(uint count, DacpThreadStressLogData[] values, uint* pFetched) + int ISOSStressLogThreadEnum.Next(uint count, SOSThreadStressLogData[] values, uint* pFetched) { if (pFetched is null || values is null) return HResults.E_POINTER; @@ -7177,7 +7176,7 @@ public SOSStressLogMsgEnum(Target target, IEnumerable m _messages = messages.ToArray(); } - int ISOSStressLogMsgEnum.Next(uint count, DacpStressMsgData[] values, uint* pFetched) + int ISOSStressLogMsgEnum.Next(uint count, SOSStressMsgData[] values, uint* pFetched) { if (pFetched is null || values is null) return HResults.E_POINTER; @@ -7189,7 +7188,7 @@ int ISOSStressLogMsgEnum.Next(uint count, DacpStressMsgData[] values, uint* pFet while (written < count && _index < _messages.Length) { Contracts.StressMsgData msg = _messages[(int)_index++]; - values[written] = new DacpStressMsgData + values[written] = new SOSStressMsgData { Facility = msg.Facility, FormatString = msg.FormatString.ToClrDataAddress(_target), @@ -7258,7 +7257,7 @@ int ISOSDacInterface17.IsStressLogAvailable() } } - int ISOSDacInterface17.GetStressLogData(DacpStressLogData* data) + int ISOSDacInterface17.GetStressLogData(SOSStressLogData* data) { int hr = HResults.S_OK; try @@ -7305,7 +7304,7 @@ int ISOSDacInterface17.GetStressLogThreadEnumerator(DacComNullableByRef new DacpThreadStressLogData + .Select(t => new SOSThreadStressLogData { ThreadLogAddress = t.Address.ToClrDataAddress(_target), ThreadId = t.ThreadId, From 68b4dbc7304a0809134935f2785514e60e0ae1a1 Mon Sep 17 00:00:00 2001 From: Max Charlamb Date: Wed, 17 Jun 2026 09:50:34 -0400 Subject: [PATCH 05/18] Fix S_OK/S_FALSE return semantics and null pointer handling - Use 'written < count' pattern for S_FALSE (standard COM IEnumXxx convention matching SOSMethodEnum/SOSMemoryEnum) - Return E_POINTER directly for null output in GetStressLogData Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../SOSDacImpl.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 f7437d5b04d54a..a6231af417089a 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.cs @@ -7138,7 +7138,7 @@ int ISOSStressLogThreadEnum.Next(uint count, SOSThreadStressLogData[] values, ui values[written++] = _threads[(int)_index++]; *pFetched = written; - return _index < _threads.Length ? HResults.S_FALSE : HResults.S_OK; + return written < count ? HResults.S_FALSE : HResults.S_OK; } int ISOSEnum.Skip(uint count) @@ -7201,7 +7201,7 @@ int ISOSStressLogMsgEnum.Next(uint count, SOSStressMsgData[] values, uint* pFetc _lastBatchCount = written; *pFetched = written; - return _index < _messages.Length ? HResults.S_FALSE : HResults.S_OK; + return written < count ? HResults.S_FALSE : HResults.S_OK; } int ISOSStressLogMsgEnum.GetArguments(uint messageIndex, uint argCount, ClrDataAddress[] args, uint* pFetched) @@ -7263,7 +7263,7 @@ int ISOSDacInterface17.GetStressLogData(SOSStressLogData* data) try { if (data is null) - throw new ArgumentException(); + throw new NullReferenceException(); Contracts.IStressLog stressLogContract = _target.Contracts.StressLog; if (!stressLogContract.HasStressLog()) From 8eb5668aa82995b53a19ebb86af7793431e2261f Mon Sep 17 00:00:00 2001 From: Max Charlamb Date: Wed, 17 Jun 2026 10:13:56 -0400 Subject: [PATCH 06/18] Remove raw-parsing fields from SOSStressLogData StressMsgHeaderSize, ChunkSize, MaxMessageSize, and PointerSize are implementation details for raw memory walking that the message enumerator now abstracts away. Consumers no longer need them. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/coreclr/inc/sospriv.idl | 4 ---- .../ISOSDacInterface.cs | 4 ---- .../SOSDacImpl.cs | 4 ---- 3 files changed, 12 deletions(-) diff --git a/src/coreclr/inc/sospriv.idl b/src/coreclr/inc/sospriv.idl index b97efcb388559d..d4c84d8bb38b0d 100644 --- a/src/coreclr/inc/sospriv.idl +++ b/src/coreclr/inc/sospriv.idl @@ -587,10 +587,6 @@ typedef struct _SOSStressLogData UINT64 StartTimestamp; UINT64 StartTime; CLRDATA_ADDRESS Logs; - unsigned int StressMsgHeaderSize; - unsigned int ChunkSize; - unsigned int MaxMessageSize; - unsigned int PointerSize; } SOSStressLogData; cpp_quote("#endif //_SOS_StressLogData") 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 0673576cac9375..0f7c99e416a0b5 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/ISOSDacInterface.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/ISOSDacInterface.cs @@ -1205,10 +1205,6 @@ public struct SOSStressLogData public ulong StartTimestamp; public ulong StartTime; public ClrDataAddress Logs; - public uint StressMsgHeaderSize; - public uint ChunkSize; - public uint MaxMessageSize; - public uint PointerSize; } public struct SOSThreadStressLogData 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 a6231af417089a..e71f65d7d1aabb 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.cs @@ -7282,10 +7282,6 @@ int ISOSDacInterface17.GetStressLogData(SOSStressLogData* data) data->StartTimestamp = logData.StartTimestamp; data->StartTime = logData.StartTime; data->Logs = logData.Logs.ToClrDataAddress(_target); - data->StressMsgHeaderSize = _target.GetTypeInfo(DataType.StressMsgHeader).Size ?? 0; - data->ChunkSize = _target.ReadGlobal(Constants.Globals.StressLogChunkSize); - data->MaxMessageSize = _target.ReadGlobal(Constants.Globals.StressLogMaxMessageSize); - data->PointerSize = (uint)_target.PointerSize; } catch (System.Exception ex) { From ffa09ffabb5cbfdd596532a55bcd605aa7e4b675 Mon Sep 17 00:00:00 2001 From: Max Charlamb Date: Wed, 17 Jun 2026 10:18:17 -0400 Subject: [PATCH 07/18] Remove IsStressLogAvailable; return S_FALSE from other APIs instead Callers can just call GetStressLogData/GetStressLogThreadEnumerator directly -- they return S_FALSE when stress log is not enabled. No need for a separate availability check method. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/coreclr/inc/sospriv.idl | 1 - .../ISOSDacInterface.cs | 3 --- .../SOSDacImpl.cs | 22 ++++++------------- 3 files changed, 7 insertions(+), 19 deletions(-) diff --git a/src/coreclr/inc/sospriv.idl b/src/coreclr/inc/sospriv.idl index d4c84d8bb38b0d..15d5ba94dc7561 100644 --- a/src/coreclr/inc/sospriv.idl +++ b/src/coreclr/inc/sospriv.idl @@ -657,7 +657,6 @@ interface ISOSStressLogMsgEnum : ISOSEnum ] interface ISOSDacInterface17 : IUnknown { - HRESULT IsStressLogAvailable(); HRESULT GetStressLogData([out] SOSStressLogData *data); HRESULT GetStressLogThreadEnumerator([out] ISOSStressLogThreadEnum **ppEnum); HRESULT GetStressLogMessageEnumerator([in] CLRDATA_ADDRESS threadStressLogAddress, 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 0f7c99e416a0b5..90c5e65b1cc321 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/ISOSDacInterface.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/ISOSDacInterface.cs @@ -1260,9 +1260,6 @@ int GetArguments(uint messageIndex, [Guid("2f4bb585-ed50-479e-bbe0-10a95a5da3bb")] public unsafe partial interface ISOSDacInterface17 { - [PreserveSig] - int IsStressLogAvailable(); - [PreserveSig] int GetStressLogData(SOSStressLogData* data); 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 e71f65d7d1aabb..1e055ae44299e4 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.cs @@ -7243,27 +7243,13 @@ int ISOSEnum.GetCount(uint* pCount) } } - int ISOSDacInterface17.IsStressLogAvailable() - { - try - { - return _target.Contracts.StressLog.HasStressLog() - ? HResults.S_OK - : HResults.S_FALSE; - } - catch (System.Exception ex) - { - return ex.HResult; - } - } - int ISOSDacInterface17.GetStressLogData(SOSStressLogData* data) { int hr = HResults.S_OK; try { if (data is null) - throw new NullReferenceException(); + return HResults.E_POINTER; Contracts.IStressLog stressLogContract = _target.Contracts.StressLog; if (!stressLogContract.HasStressLog()) @@ -7297,6 +7283,9 @@ int ISOSDacInterface17.GetStressLogThreadEnumerator(DacComNullableByRef Date: Wed, 17 Jun 2026 10:48:56 -0400 Subject: [PATCH 08/18] Address review feedback: COM boundary safety and interface tests - Initialize *data = default early in GetStressLogData for clean output on all failure paths - Wrap enumerator Next methods in try/catch to avoid leaking exceptions across the COM boundary - Add ISOSDacInterface17 dump tests exercising GetStressLogData, GetStressLogThreadEnumerator, GetStressLogMessageEnumerator, and GetArguments through the SOSDacImpl COM bridge Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../SOSDacImpl.cs | 79 ++++++++++------ .../tests/DumpTests/StressLogDumpTests.cs | 93 +++++++++++++++++++ 2 files changed, 142 insertions(+), 30 deletions(-) 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 1e055ae44299e4..27219aa0771659 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.cs @@ -7129,16 +7129,26 @@ public SOSStressLogThreadEnum(SOSThreadStressLogData[] threads) int ISOSStressLogThreadEnum.Next(uint count, SOSThreadStressLogData[] values, uint* pFetched) { - if (pFetched is null || values is null) - return HResults.E_POINTER; + int hr = HResults.S_OK; + try + { + if (pFetched is null || values is null) + throw new NullReferenceException(); - count = Math.Min(count, (uint)values.Length); - uint written = 0; - while (written < count && _index < _threads.Length) - values[written++] = _threads[(int)_index++]; + count = Math.Min(count, (uint)values.Length); + uint written = 0; + while (written < count && _index < _threads.Length) + values[written++] = _threads[(int)_index++]; - *pFetched = written; - return written < count ? HResults.S_FALSE : HResults.S_OK; + *pFetched = written; + hr = written < count ? HResults.S_FALSE : HResults.S_OK; + } + catch (System.Exception ex) + { + hr = ex.HResult; + } + + return hr; } int ISOSEnum.Skip(uint count) @@ -7178,30 +7188,40 @@ public SOSStressLogMsgEnum(Target target, IEnumerable m int ISOSStressLogMsgEnum.Next(uint count, SOSStressMsgData[] values, uint* pFetched) { - if (pFetched is null || values is null) - return HResults.E_POINTER; + int hr = HResults.S_OK; + try + { + if (pFetched is null || values is null) + throw new NullReferenceException(); - count = Math.Min(count, (uint)values.Length); - _lastBatchStart = _index; - uint written = 0; + count = Math.Min(count, (uint)values.Length); + _lastBatchStart = _index; + uint written = 0; - while (written < count && _index < _messages.Length) - { - Contracts.StressMsgData msg = _messages[(int)_index++]; - values[written] = new SOSStressMsgData + while (written < count && _index < _messages.Length) { - Facility = msg.Facility, - FormatString = msg.FormatString.ToClrDataAddress(_target), - Timestamp = msg.Timestamp, - ArgumentCount = (uint)msg.Args.Count, - MessageAddress = 0, - }; - written++; + Contracts.StressMsgData msg = _messages[(int)_index++]; + values[written] = new SOSStressMsgData + { + Facility = msg.Facility, + FormatString = msg.FormatString.ToClrDataAddress(_target), + Timestamp = msg.Timestamp, + ArgumentCount = (uint)msg.Args.Count, + MessageAddress = 0, + }; + written++; + } + + _lastBatchCount = written; + *pFetched = written; + hr = written < count ? HResults.S_FALSE : HResults.S_OK; + } + catch (System.Exception ex) + { + hr = ex.HResult; } - _lastBatchCount = written; - *pFetched = written; - return written < count ? HResults.S_FALSE : HResults.S_OK; + return hr; } int ISOSStressLogMsgEnum.GetArguments(uint messageIndex, uint argCount, ClrDataAddress[] args, uint* pFetched) @@ -7251,12 +7271,11 @@ int ISOSDacInterface17.GetStressLogData(SOSStressLogData* data) if (data is null) return HResults.E_POINTER; + *data = default; + Contracts.IStressLog stressLogContract = _target.Contracts.StressLog; if (!stressLogContract.HasStressLog()) - { - *data = default; return HResults.S_FALSE; - } Contracts.StressLogData logData = stressLogContract.GetStressLogData(); data->LoggedFacilities = logData.LoggedFacilities; diff --git a/src/native/managed/cdac/tests/DumpTests/StressLogDumpTests.cs b/src/native/managed/cdac/tests/DumpTests/StressLogDumpTests.cs index 5a8b1c3da67bc3..1a734f872ddac6 100644 --- a/src/native/managed/cdac/tests/DumpTests/StressLogDumpTests.cs +++ b/src/native/managed/cdac/tests/DumpTests/StressLogDumpTests.cs @@ -3,6 +3,7 @@ using System.Linq; using Microsoft.Diagnostics.DataContractReader.Contracts; +using Microsoft.Diagnostics.DataContractReader.Legacy; using Microsoft.Diagnostics.DataContractReader.TestInfrastructure; using Xunit; @@ -65,4 +66,96 @@ public void CanEnumerateThreadsAndMessages(TestConfiguration config) } Assert.True(foundMessages, "Expected at least one thread with stress log messages"); } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + public unsafe void ISOSDacInterface17_GetStressLogData(TestConfiguration config) + { + InitializeDumpTest(config); + ISOSDacInterface17 sosDac = (ISOSDacInterface17)new SOSDacImpl(Target, legacyObj: null); + + SOSStressLogData data; + int hr = sosDac.GetStressLogData(&data); + Assert.Equal(System.HResults.S_OK, hr); + Assert.NotEqual(0UL, data.TickFrequency); + Assert.NotEqual(0UL, data.StartTimestamp); + Assert.NotEqual(0UL, data.StartTime); + Assert.NotEqual((ClrDataAddress)0, data.Logs); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + public unsafe void ISOSDacInterface17_GetStressLogThreadEnumerator(TestConfiguration config) + { + InitializeDumpTest(config); + ISOSDacInterface17 sosDac = (ISOSDacInterface17)new SOSDacImpl(Target, legacyObj: null); + + var ppEnum = new DacComNullableByRef(isNullRef: false); + int hr = sosDac.GetStressLogThreadEnumerator(ppEnum); + Assert.Equal(System.HResults.S_OK, hr); + Assert.NotNull(ppEnum.Interface); + + uint count; + hr = ppEnum.Interface.GetCount(&count); + Assert.Equal(System.HResults.S_OK, hr); + Assert.True(count > 0, "Expected at least one thread with stress log"); + + SOSThreadStressLogData[] threads = new SOSThreadStressLogData[count]; + uint fetched; + hr = ppEnum.Interface.Next(count, threads, &fetched); + Assert.True(hr == System.HResults.S_OK || hr == System.HResults.S_FALSE); + Assert.True(fetched > 0); + Assert.NotEqual((ClrDataAddress)0, threads[0].ThreadLogAddress); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + public unsafe void ISOSDacInterface17_GetStressLogMessageEnumerator(TestConfiguration config) + { + InitializeDumpTest(config); + ISOSDacInterface17 sosDac = (ISOSDacInterface17)new SOSDacImpl(Target, legacyObj: null); + + var ppThreadEnum = new DacComNullableByRef(isNullRef: false); + int hr = sosDac.GetStressLogThreadEnumerator(ppThreadEnum); + Assert.Equal(System.HResults.S_OK, hr); + + uint threadCount; + ppThreadEnum.Interface.GetCount(&threadCount); + + SOSThreadStressLogData[] threads = new SOSThreadStressLogData[threadCount]; + uint fetched; + ppThreadEnum.Interface.Next(threadCount, threads, &fetched); + + bool foundMessages = false; + for (uint i = 0; i < fetched; i++) + { + var ppMsgEnum = new DacComNullableByRef(isNullRef: false); + hr = sosDac.GetStressLogMessageEnumerator(threads[i].ThreadLogAddress, ppMsgEnum); + Assert.Equal(System.HResults.S_OK, hr); + Assert.NotNull(ppMsgEnum.Interface); + + SOSStressMsgData[] messages = new SOSStressMsgData[10]; + uint msgFetched; + hr = ppMsgEnum.Interface.Next(10, messages, &msgFetched); + Assert.True(hr == System.HResults.S_OK || hr == System.HResults.S_FALSE); + + if (msgFetched > 0) + { + foundMessages = true; + Assert.NotEqual(0UL, messages[0].Timestamp); + Assert.NotEqual((ClrDataAddress)0, messages[0].FormatString); + + if (messages[0].ArgumentCount > 0) + { + ClrDataAddress[] args = new ClrDataAddress[messages[0].ArgumentCount]; + uint argFetched; + hr = ppMsgEnum.Interface.GetArguments(0, messages[0].ArgumentCount, args, &argFetched); + Assert.Equal(System.HResults.S_OK, hr); + Assert.Equal(messages[0].ArgumentCount, argFetched); + } + break; + } + } + Assert.True(foundMessages, "Expected at least one thread with stress log messages via ISOSDacInterface17"); + } } From 248326c49b8d38f8f79c924699c719fd7a450e56 Mon Sep 17 00:00:00 2001 From: Max Charlamb Date: Wed, 17 Jun 2026 12:49:15 -0400 Subject: [PATCH 09/18] Fix nullable dereference warnings in StressLogDumpTests Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../tests/DumpTests/StressLogDumpTests.cs | 29 ++++++++++++------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/src/native/managed/cdac/tests/DumpTests/StressLogDumpTests.cs b/src/native/managed/cdac/tests/DumpTests/StressLogDumpTests.cs index 1a734f872ddac6..0cc59b0007438d 100644 --- a/src/native/managed/cdac/tests/DumpTests/StressLogDumpTests.cs +++ b/src/native/managed/cdac/tests/DumpTests/StressLogDumpTests.cs @@ -90,19 +90,21 @@ public unsafe void ISOSDacInterface17_GetStressLogThreadEnumerator(TestConfigura InitializeDumpTest(config); ISOSDacInterface17 sosDac = (ISOSDacInterface17)new SOSDacImpl(Target, legacyObj: null); - var ppEnum = new DacComNullableByRef(isNullRef: false); + DacComNullableByRef ppEnum = new(isNullRef: false); int hr = sosDac.GetStressLogThreadEnumerator(ppEnum); Assert.Equal(System.HResults.S_OK, hr); - Assert.NotNull(ppEnum.Interface); + + ISOSStressLogThreadEnum? threadEnum = ppEnum.Interface; + Assert.NotNull(threadEnum); uint count; - hr = ppEnum.Interface.GetCount(&count); + hr = threadEnum.GetCount(&count); Assert.Equal(System.HResults.S_OK, hr); Assert.True(count > 0, "Expected at least one thread with stress log"); SOSThreadStressLogData[] threads = new SOSThreadStressLogData[count]; uint fetched; - hr = ppEnum.Interface.Next(count, threads, &fetched); + hr = threadEnum.Next(count, threads, &fetched); Assert.True(hr == System.HResults.S_OK || hr == System.HResults.S_FALSE); Assert.True(fetched > 0); Assert.NotEqual((ClrDataAddress)0, threads[0].ThreadLogAddress); @@ -115,28 +117,33 @@ public unsafe void ISOSDacInterface17_GetStressLogMessageEnumerator(TestConfigur InitializeDumpTest(config); ISOSDacInterface17 sosDac = (ISOSDacInterface17)new SOSDacImpl(Target, legacyObj: null); - var ppThreadEnum = new DacComNullableByRef(isNullRef: false); + DacComNullableByRef ppThreadEnum = new(isNullRef: false); int hr = sosDac.GetStressLogThreadEnumerator(ppThreadEnum); Assert.Equal(System.HResults.S_OK, hr); + ISOSStressLogThreadEnum? threadEnum = ppThreadEnum.Interface; + Assert.NotNull(threadEnum); + uint threadCount; - ppThreadEnum.Interface.GetCount(&threadCount); + threadEnum.GetCount(&threadCount); SOSThreadStressLogData[] threads = new SOSThreadStressLogData[threadCount]; uint fetched; - ppThreadEnum.Interface.Next(threadCount, threads, &fetched); + threadEnum.Next(threadCount, threads, &fetched); bool foundMessages = false; for (uint i = 0; i < fetched; i++) { - var ppMsgEnum = new DacComNullableByRef(isNullRef: false); + DacComNullableByRef ppMsgEnum = new(isNullRef: false); hr = sosDac.GetStressLogMessageEnumerator(threads[i].ThreadLogAddress, ppMsgEnum); Assert.Equal(System.HResults.S_OK, hr); - Assert.NotNull(ppMsgEnum.Interface); + + ISOSStressLogMsgEnum? msgEnum = ppMsgEnum.Interface; + Assert.NotNull(msgEnum); SOSStressMsgData[] messages = new SOSStressMsgData[10]; uint msgFetched; - hr = ppMsgEnum.Interface.Next(10, messages, &msgFetched); + hr = msgEnum.Next(10, messages, &msgFetched); Assert.True(hr == System.HResults.S_OK || hr == System.HResults.S_FALSE); if (msgFetched > 0) @@ -149,7 +156,7 @@ public unsafe void ISOSDacInterface17_GetStressLogMessageEnumerator(TestConfigur { ClrDataAddress[] args = new ClrDataAddress[messages[0].ArgumentCount]; uint argFetched; - hr = ppMsgEnum.Interface.GetArguments(0, messages[0].ArgumentCount, args, &argFetched); + hr = msgEnum.GetArguments(0, messages[0].ArgumentCount, args, &argFetched); Assert.Equal(System.HResults.S_OK, hr); Assert.Equal(messages[0].ArgumentCount, argFetched); } From b408eb63e96fb9bc362331e19d926b3402d51016 Mon Sep 17 00:00:00 2001 From: Max Charlamb Date: Wed, 17 Jun 2026 15:52:46 -0400 Subject: [PATCH 10/18] Fix StressLog.TotalChunks data descriptor type mismatch CoreCLR datadescriptor.inc declared TotalChunks as T_UINT32 but the actual C++ field is Volatile (signed int32). NativeAOT already had the correct T_INT32. Fix CoreCLR to match. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/coreclr/vm/datadescriptor/datadescriptor.inc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/coreclr/vm/datadescriptor/datadescriptor.inc b/src/coreclr/vm/datadescriptor/datadescriptor.inc index 9df65e123cc5bd..3ea511df0e1f0a 100644 --- a/src/coreclr/vm/datadescriptor/datadescriptor.inc +++ b/src/coreclr/vm/datadescriptor/datadescriptor.inc @@ -564,7 +564,7 @@ CDAC_TYPE_FIELD(StressLog, T_UINT32, LoggedFacilities, cdac_offsets:: CDAC_TYPE_FIELD(StressLog, T_UINT32, Level, cdac_offsets::levelToLog) CDAC_TYPE_FIELD(StressLog, T_UINT32, MaxSizePerThread, cdac_offsets::MaxSizePerThread) CDAC_TYPE_FIELD(StressLog, T_UINT32, MaxSizeTotal, cdac_offsets::MaxSizeTotal) -CDAC_TYPE_FIELD(StressLog, T_UINT32, TotalChunks, cdac_offsets::totalChunk) +CDAC_TYPE_FIELD(StressLog, T_INT32, TotalChunks, cdac_offsets::totalChunk) CDAC_TYPE_FIELD(StressLog, T_POINTER, Logs, cdac_offsets::logs) CDAC_TYPE_FIELD(StressLog, T_UINT64, TickFrequency, cdac_offsets::tickFrequency) CDAC_TYPE_FIELD(StressLog, T_UINT64, StartTimestamp, cdac_offsets::startTimeStamp) From db3a424160d436831540ea70a61afa5e7a67620f Mon Sep 17 00:00:00 2001 From: Max Charlamb Date: Wed, 17 Jun 2026 17:13:01 -0400 Subject: [PATCH 11/18] Clear batch state on Reset and assert HRESULTs in tests - SOSStressLogMsgEnum.Reset() now clears _lastBatchStart and _lastBatchCount to prevent stale GetArguments after Reset - Assert HRESULT return values from GetCount/Next in the message enumerator test for actionable failure messages Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../SOSDacImpl.cs | 2 ++ .../managed/cdac/tests/DumpTests/StressLogDumpTests.cs | 6 ++++-- 2 files changed, 6 insertions(+), 2 deletions(-) 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 27219aa0771659..4d414d90757cdd 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.cs @@ -7252,6 +7252,8 @@ int ISOSEnum.Skip(uint count) int ISOSEnum.Reset() { _index = 0; + _lastBatchStart = 0; + _lastBatchCount = 0; return HResults.S_OK; } diff --git a/src/native/managed/cdac/tests/DumpTests/StressLogDumpTests.cs b/src/native/managed/cdac/tests/DumpTests/StressLogDumpTests.cs index 0cc59b0007438d..7aa143d4d854b3 100644 --- a/src/native/managed/cdac/tests/DumpTests/StressLogDumpTests.cs +++ b/src/native/managed/cdac/tests/DumpTests/StressLogDumpTests.cs @@ -125,11 +125,13 @@ public unsafe void ISOSDacInterface17_GetStressLogMessageEnumerator(TestConfigur Assert.NotNull(threadEnum); uint threadCount; - threadEnum.GetCount(&threadCount); + hr = threadEnum.GetCount(&threadCount); + Assert.Equal(System.HResults.S_OK, hr); SOSThreadStressLogData[] threads = new SOSThreadStressLogData[threadCount]; uint fetched; - threadEnum.Next(threadCount, threads, &fetched); + hr = threadEnum.Next(threadCount, threads, &fetched); + Assert.True(hr == System.HResults.S_OK || hr == System.HResults.S_FALSE); bool foundMessages = false; for (uint i = 0; i < fetched; i++) From ecbeeeb94e7d8df1f3125f4bb0637136feaba6ee Mon Sep 17 00:00:00 2001 From: Max Charlamb Date: Wed, 17 Jun 2026 17:50:56 -0400 Subject: [PATCH 12/18] Remove unused fields from SOS stress log structs Remove fields not consumed by the SOS DumpLog PR: - SOSStressLogData: Logs (callers use enumerator, not raw pointer) - SOSThreadStressLogData: WriteHasWrapped, CurrentPointer, ChunkListHead, ChunkListTail, CurrentWriteChunk (raw chunk-walking details abstracted by the message enumerator) - SOSStressMsgData: MessageAddress (was always 0) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/coreclr/inc/sospriv.idl | 7 ------- .../ISOSDacInterface.cs | 7 ------- .../SOSDacImpl.cs | 7 ------- .../managed/cdac/tests/DumpTests/StressLogDumpTests.cs | 1 - 4 files changed, 22 deletions(-) diff --git a/src/coreclr/inc/sospriv.idl b/src/coreclr/inc/sospriv.idl index 15d5ba94dc7561..94b43d5ad98a1e 100644 --- a/src/coreclr/inc/sospriv.idl +++ b/src/coreclr/inc/sospriv.idl @@ -586,7 +586,6 @@ typedef struct _SOSStressLogData UINT64 TickFrequency; UINT64 StartTimestamp; UINT64 StartTime; - CLRDATA_ADDRESS Logs; } SOSStressLogData; cpp_quote("#endif //_SOS_StressLogData") @@ -598,11 +597,6 @@ typedef struct _SOSThreadStressLogData { CLRDATA_ADDRESS ThreadLogAddress; UINT64 ThreadId; - int WriteHasWrapped; - CLRDATA_ADDRESS CurrentPointer; - CLRDATA_ADDRESS ChunkListHead; - CLRDATA_ADDRESS ChunkListTail; - CLRDATA_ADDRESS CurrentWriteChunk; } SOSThreadStressLogData; cpp_quote("#endif //_SOS_ThreadStressLogData") @@ -616,7 +610,6 @@ typedef struct _SOSStressMsgData CLRDATA_ADDRESS FormatString; UINT64 Timestamp; unsigned int ArgumentCount; - CLRDATA_ADDRESS MessageAddress; } SOSStressMsgData; cpp_quote("#endif //_SOS_StressMsgData") 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 90c5e65b1cc321..cce31d11ab2d80 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/ISOSDacInterface.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/ISOSDacInterface.cs @@ -1204,18 +1204,12 @@ public struct SOSStressLogData public ulong TickFrequency; public ulong StartTimestamp; public ulong StartTime; - public ClrDataAddress Logs; } public struct SOSThreadStressLogData { public ClrDataAddress ThreadLogAddress; public ulong ThreadId; - public int WriteHasWrapped; - public ClrDataAddress CurrentPointer; - public ClrDataAddress ChunkListHead; - public ClrDataAddress ChunkListTail; - public ClrDataAddress CurrentWriteChunk; } public struct SOSStressMsgData @@ -1224,7 +1218,6 @@ public struct SOSStressMsgData public ClrDataAddress FormatString; public ulong Timestamp; public uint ArgumentCount; - public ClrDataAddress MessageAddress; } [GeneratedComInterface] 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 4d414d90757cdd..0a8a3b9b416813 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.cs @@ -7207,7 +7207,6 @@ int ISOSStressLogMsgEnum.Next(uint count, SOSStressMsgData[] values, uint* pFetc FormatString = msg.FormatString.ToClrDataAddress(_target), Timestamp = msg.Timestamp, ArgumentCount = (uint)msg.Args.Count, - MessageAddress = 0, }; written++; } @@ -7288,7 +7287,6 @@ int ISOSDacInterface17.GetStressLogData(SOSStressLogData* data) data->TickFrequency = logData.TickFrequency; data->StartTimestamp = logData.StartTimestamp; data->StartTime = logData.StartTime; - data->Logs = logData.Logs.ToClrDataAddress(_target); } catch (System.Exception ex) { @@ -7314,11 +7312,6 @@ int ISOSDacInterface17.GetStressLogThreadEnumerator(DacComNullableByRef Date: Wed, 17 Jun 2026 18:04:21 -0400 Subject: [PATCH 13/18] Zero out pFetched before error paths in enumerators Initialize *pFetched = 0 early in Next and GetArguments so callers see a well-defined value on all failure paths. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../SOSDacImpl.cs | 4 ++++ 1 file changed, 4 insertions(+) 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 0a8a3b9b416813..e98fdfc4fe1e8a 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.cs @@ -7135,6 +7135,7 @@ int ISOSStressLogThreadEnum.Next(uint count, SOSThreadStressLogData[] values, ui if (pFetched is null || values is null) throw new NullReferenceException(); + *pFetched = 0; count = Math.Min(count, (uint)values.Length); uint written = 0; while (written < count && _index < _threads.Length) @@ -7194,6 +7195,7 @@ int ISOSStressLogMsgEnum.Next(uint count, SOSStressMsgData[] values, uint* pFetc if (pFetched is null || values is null) throw new NullReferenceException(); + *pFetched = 0; count = Math.Min(count, (uint)values.Length); _lastBatchStart = _index; uint written = 0; @@ -7228,6 +7230,8 @@ int ISOSStressLogMsgEnum.GetArguments(uint messageIndex, uint argCount, ClrDataA if (pFetched is null || args is null) return HResults.E_POINTER; + *pFetched = 0; + if (messageIndex >= _lastBatchCount) return HResults.E_INVALIDARG; From 41b4edc3778dfdf6043ab20929dbd918a9676eb3 Mon Sep 17 00:00:00 2001 From: Max Charlamb Date: Wed, 17 Jun 2026 20:35:29 -0400 Subject: [PATCH 14/18] Fix x86 test failure: skip timestamp-0 messages in assertions Messages with Timestamp==0 can appear at chunk boundaries on x86 with small stress log sizes. These represent unwritten/partial message slots and are valid contract output per the spec. Update tests to find the first message with a non-zero timestamp rather than assuming the first message is valid. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../tests/DumpTests/StressLogDumpTests.cs | 45 ++++++++++++------- 1 file changed, 28 insertions(+), 17 deletions(-) diff --git a/src/native/managed/cdac/tests/DumpTests/StressLogDumpTests.cs b/src/native/managed/cdac/tests/DumpTests/StressLogDumpTests.cs index afb3b3f3ec7bd6..28cfac42ce90b0 100644 --- a/src/native/managed/cdac/tests/DumpTests/StressLogDumpTests.cs +++ b/src/native/managed/cdac/tests/DumpTests/StressLogDumpTests.cs @@ -55,14 +55,17 @@ public void CanEnumerateThreadsAndMessages(TestConfiguration config) bool foundMessages = false; foreach (ThreadStressLogData thread in threads) { - var messages = stressLog.GetStressMessages(thread).Take(10).ToList(); - if (messages.Count > 0) + foreach (StressMsgData message in stressLog.GetStressMessages(thread).Take(10)) { - foundMessages = true; - Assert.NotEqual(0UL, messages[0].Timestamp); - Assert.NotEqual(TargetPointer.Null, messages[0].FormatString); - break; + if (message.Timestamp != 0) + { + foundMessages = true; + Assert.NotEqual(TargetPointer.Null, message.FormatString); + break; + } } + if (foundMessages) + break; } Assert.True(foundMessages, "Expected at least one thread with stress log messages"); } @@ -149,19 +152,27 @@ public unsafe void ISOSDacInterface17_GetStressLogMessageEnumerator(TestConfigur if (msgFetched > 0) { - foundMessages = true; - Assert.NotEqual(0UL, messages[0].Timestamp); - Assert.NotEqual((ClrDataAddress)0, messages[0].FormatString); - - if (messages[0].ArgumentCount > 0) + // Find a message with a valid (non-zero) timestamp + for (uint m = 0; m < msgFetched; m++) { - ClrDataAddress[] args = new ClrDataAddress[messages[0].ArgumentCount]; - uint argFetched; - hr = msgEnum.GetArguments(0, messages[0].ArgumentCount, args, &argFetched); - Assert.Equal(System.HResults.S_OK, hr); - Assert.Equal(messages[0].ArgumentCount, argFetched); + if (messages[m].Timestamp != 0) + { + foundMessages = true; + Assert.NotEqual((ClrDataAddress)0, messages[m].FormatString); + + if (messages[m].ArgumentCount > 0) + { + ClrDataAddress[] args = new ClrDataAddress[messages[m].ArgumentCount]; + uint argFetched; + hr = msgEnum.GetArguments(m, messages[m].ArgumentCount, args, &argFetched); + Assert.Equal(System.HResults.S_OK, hr); + Assert.Equal(messages[m].ArgumentCount, argFetched); + } + break; + } } - break; + if (foundMessages) + break; } } Assert.True(foundMessages, "Expected at least one thread with stress log messages via ISOSDacInterface17"); From c8087f821a403dd1a91cb730b378929119e997b4 Mon Sep 17 00:00:00 2001 From: Max Charlamb Date: Thu, 18 Jun 2026 09:44:57 -0400 Subject: [PATCH 15/18] Return S_FALSE from GetArguments when fewer args than requested Matches the convention used by other SOSDAC sized-buffer APIs where S_FALSE indicates fewer items returned than requested. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../SOSDacImpl.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 e98fdfc4fe1e8a..168ea57a6e5e14 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.cs @@ -7243,7 +7243,7 @@ int ISOSStressLogMsgEnum.GetArguments(uint messageIndex, uint argCount, ClrDataA args[i] = msg.Args[(int)i].ToClrDataAddress(_target); *pFetched = toFetch; - return HResults.S_OK; + return toFetch < argCount ? HResults.S_FALSE : HResults.S_OK; } int ISOSEnum.Skip(uint count) From 84166a2e2edc97cb2480215c650bf2691b81f62b Mon Sep 17 00:00:00 2001 From: Max Charlamb Date: Thu, 18 Jun 2026 11:41:40 -0400 Subject: [PATCH 16/18] Fix x86 test: also skip messages with null FormatString On x86 with small stress logs, partially-written messages can have non-zero timestamps but null format strings due to chunk-boundary wrapping. Look for messages with both valid timestamp and format string. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../managed/cdac/tests/DumpTests/StressLogDumpTests.cs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/native/managed/cdac/tests/DumpTests/StressLogDumpTests.cs b/src/native/managed/cdac/tests/DumpTests/StressLogDumpTests.cs index 28cfac42ce90b0..857de53e23f5bd 100644 --- a/src/native/managed/cdac/tests/DumpTests/StressLogDumpTests.cs +++ b/src/native/managed/cdac/tests/DumpTests/StressLogDumpTests.cs @@ -57,10 +57,9 @@ public void CanEnumerateThreadsAndMessages(TestConfiguration config) { foreach (StressMsgData message in stressLog.GetStressMessages(thread).Take(10)) { - if (message.Timestamp != 0) + if (message.Timestamp != 0 && message.FormatString != TargetPointer.Null) { foundMessages = true; - Assert.NotEqual(TargetPointer.Null, message.FormatString); break; } } @@ -152,13 +151,12 @@ public unsafe void ISOSDacInterface17_GetStressLogMessageEnumerator(TestConfigur if (msgFetched > 0) { - // Find a message with a valid (non-zero) timestamp + // Find a message with valid timestamp and format string for (uint m = 0; m < msgFetched; m++) { - if (messages[m].Timestamp != 0) + if (messages[m].Timestamp != 0 && messages[m].FormatString != (ClrDataAddress)0) { foundMessages = true; - Assert.NotEqual((ClrDataAddress)0, messages[m].FormatString); if (messages[m].ArgumentCount > 0) { From 605d26883e7336c0c98238bf4d9f67a9a8a3a8f9 Mon Sep 17 00:00:00 2001 From: Max Charlamb Date: Thu, 18 Jun 2026 13:39:47 -0400 Subject: [PATCH 17/18] Add diagnostic output to stress log tests for x86 investigation Temporary diagnostics to understand why x86 can't find valid messages: prints thread count, pointer size, per-thread metadata, and per-message timestamp/format/facility/argcount. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../managed/cdac/tests/DumpTests/StressLogDumpTests.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/native/managed/cdac/tests/DumpTests/StressLogDumpTests.cs b/src/native/managed/cdac/tests/DumpTests/StressLogDumpTests.cs index 857de53e23f5bd..f9796c607518c8 100644 --- a/src/native/managed/cdac/tests/DumpTests/StressLogDumpTests.cs +++ b/src/native/managed/cdac/tests/DumpTests/StressLogDumpTests.cs @@ -52,21 +52,28 @@ public void CanEnumerateThreadsAndMessages(TestConfiguration config) var threads = stressLog.GetThreadStressLogs(data.Logs).ToList(); Assert.NotEmpty(threads); + System.Text.StringBuilder diag = new(); + diag.AppendLine($"Thread count: {threads.Count}, PointerSize: {Target.PointerSize}"); + bool foundMessages = false; foreach (ThreadStressLogData thread in threads) { + int msgIndex = 0; + diag.AppendLine($" Thread 0x{thread.ThreadId:X}: WriteHasWrapped={thread.WriteHasWrapped}, CurrentPointer=0x{thread.CurrentPointer:X}"); foreach (StressMsgData message in stressLog.GetStressMessages(thread).Take(10)) { + diag.AppendLine($" Msg[{msgIndex}]: Timestamp={message.Timestamp}, FormatString=0x{(ulong)message.FormatString:X}, Facility={message.Facility}, ArgCount={message.Args.Count}"); if (message.Timestamp != 0 && message.FormatString != TargetPointer.Null) { foundMessages = true; break; } + msgIndex++; } if (foundMessages) break; } - Assert.True(foundMessages, "Expected at least one thread with stress log messages"); + Assert.True(foundMessages, $"Expected at least one thread with stress log messages.\nDiagnostics:\n{diag}"); } [ConditionalTheory] From cb02ef6ea8179b9417a456d2279425a4d79b76f8 Mon Sep 17 00:00:00 2001 From: Max Charlamb Date: Thu, 18 Jun 2026 14:49:57 -0400 Subject: [PATCH 18/18] Fix x86 StressLog: use [FieldAddress] for inline Modules array StressLog.modules is an inline array (ModuleDesc modules[MAX_MODULES]), not a pointer. Using [Field] reads the first bytes of the array as a pointer value, which on x86 happens to be the coreclr base address (from modules[0].baseAddress), causing GetFormatPointer's module table lookup to iterate from the PE header instead of the module descriptors. Using [FieldAddress] returns the address of the inline array itself, which is the correct base for iterating the ModuleDesc entries. Verified fix locally against CI x86 dump that previously failed. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Data/StressLog.cs | 2 +- .../tests/DumpTests/StressLogDumpTests.cs | 52 +++++++------------ 2 files changed, 19 insertions(+), 35 deletions(-) diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/StressLog.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/StressLog.cs index b39fa051dfa5ff..330ce4caae2b41 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/StressLog.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/StressLog.cs @@ -15,6 +15,6 @@ internal sealed partial class StressLog : IData [Field] public ulong StartTimestamp { get; } [Field] public ulong StartTime { get; } [Field] public TargetNUInt ModuleOffset { get; } - [Field] public TargetPointer? Modules { get; } + [FieldAddress] public TargetPointer? Modules { get; } [Field] public TargetPointer Logs { get; } } diff --git a/src/native/managed/cdac/tests/DumpTests/StressLogDumpTests.cs b/src/native/managed/cdac/tests/DumpTests/StressLogDumpTests.cs index f9796c607518c8..afb3b3f3ec7bd6 100644 --- a/src/native/managed/cdac/tests/DumpTests/StressLogDumpTests.cs +++ b/src/native/managed/cdac/tests/DumpTests/StressLogDumpTests.cs @@ -52,28 +52,19 @@ public void CanEnumerateThreadsAndMessages(TestConfiguration config) var threads = stressLog.GetThreadStressLogs(data.Logs).ToList(); Assert.NotEmpty(threads); - System.Text.StringBuilder diag = new(); - diag.AppendLine($"Thread count: {threads.Count}, PointerSize: {Target.PointerSize}"); - bool foundMessages = false; foreach (ThreadStressLogData thread in threads) { - int msgIndex = 0; - diag.AppendLine($" Thread 0x{thread.ThreadId:X}: WriteHasWrapped={thread.WriteHasWrapped}, CurrentPointer=0x{thread.CurrentPointer:X}"); - foreach (StressMsgData message in stressLog.GetStressMessages(thread).Take(10)) + var messages = stressLog.GetStressMessages(thread).Take(10).ToList(); + if (messages.Count > 0) { - diag.AppendLine($" Msg[{msgIndex}]: Timestamp={message.Timestamp}, FormatString=0x{(ulong)message.FormatString:X}, Facility={message.Facility}, ArgCount={message.Args.Count}"); - if (message.Timestamp != 0 && message.FormatString != TargetPointer.Null) - { - foundMessages = true; - break; - } - msgIndex++; - } - if (foundMessages) + foundMessages = true; + Assert.NotEqual(0UL, messages[0].Timestamp); + Assert.NotEqual(TargetPointer.Null, messages[0].FormatString); break; + } } - Assert.True(foundMessages, $"Expected at least one thread with stress log messages.\nDiagnostics:\n{diag}"); + Assert.True(foundMessages, "Expected at least one thread with stress log messages"); } [ConditionalTheory] @@ -158,26 +149,19 @@ public unsafe void ISOSDacInterface17_GetStressLogMessageEnumerator(TestConfigur if (msgFetched > 0) { - // Find a message with valid timestamp and format string - for (uint m = 0; m < msgFetched; m++) + foundMessages = true; + Assert.NotEqual(0UL, messages[0].Timestamp); + Assert.NotEqual((ClrDataAddress)0, messages[0].FormatString); + + if (messages[0].ArgumentCount > 0) { - if (messages[m].Timestamp != 0 && messages[m].FormatString != (ClrDataAddress)0) - { - foundMessages = true; - - if (messages[m].ArgumentCount > 0) - { - ClrDataAddress[] args = new ClrDataAddress[messages[m].ArgumentCount]; - uint argFetched; - hr = msgEnum.GetArguments(m, messages[m].ArgumentCount, args, &argFetched); - Assert.Equal(System.HResults.S_OK, hr); - Assert.Equal(messages[m].ArgumentCount, argFetched); - } - break; - } + ClrDataAddress[] args = new ClrDataAddress[messages[0].ArgumentCount]; + uint argFetched; + hr = msgEnum.GetArguments(0, messages[0].ArgumentCount, args, &argFetched); + Assert.Equal(System.HResults.S_OK, hr); + Assert.Equal(messages[0].ArgumentCount, argFetched); } - if (foundMessages) - break; + break; } } Assert.True(foundMessages, "Expected at least one thread with stress log messages via ISOSDacInterface17");