diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/DacDbiImpl.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/DacDbiImpl.cs index 5748401c6b3b70..8a5214526e3c46 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/DacDbiImpl.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/DacDbiImpl.cs @@ -3180,7 +3180,90 @@ public int GetThreadOwningMonitorLock(ulong vmObject, DacDbiMonitorLockInfo* pRe } public int EnumerateMonitorEventWaitList(ulong vmObject, nint fpCallback, nint pUserData) - => LegacyFallbackHelper.CanFallback() && _legacy is not null ? _legacy.EnumerateMonitorEventWaitList(vmObject, fpCallback, pUserData) : HResults.E_NOTIMPL; + { + int hr = HResults.S_OK; +#if DEBUG + List? cdacThreads = _legacy is not null ? new() : null; +#endif + try + { + var callback = (delegate* unmanaged)fpCallback; + if (callback == null) + throw new ArgumentNullException(nameof(fpCallback)); + + TargetPointer syncBlock = _target.Contracts.Object.GetSyncBlockAddress(vmObject); + + // no sync block means no wait list + if (syncBlock != TargetPointer.Null + && _target.Contracts.ManagedTypeSource.TryGetStaticFieldAddress( + "System.Threading.Monitor", "s_conditionTable", out TargetPointer conditionTableSlot)) + { + TargetPointer conditionTable = _target.ReadPointer(conditionTableSlot); + + if (conditionTable != TargetPointer.Null + && _target.Contracts.ConditionalWeakTable.TryGetValue(conditionTable, new TargetPointer(vmObject), out TargetPointer condition)) + { + // Build a map of Waiter objects to their owning Threads. + var waiterToThreadMap = new Dictionary(); + Contracts.IThread threadContract = _target.Contracts.Thread; + Contracts.ThreadStoreData threadStore = threadContract.GetThreadStoreData(); + TargetPointer currentThread = threadStore.FirstThread; + while (currentThread != TargetPointer.Null) + { + Contracts.ThreadData threadData = threadContract.GetThreadData(currentThread); + + if (_target.Contracts.ManagedTypeSource.TryGetThreadStaticFieldAddress( + "System.Threading.Condition", "t_waiterForCurrentThread", currentThread, out TargetPointer waiterSlot)) + { + TargetPointer waiterObj = _target.ReadPointer(waiterSlot); + if (waiterObj != TargetPointer.Null) + waiterToThreadMap[waiterObj.Value] = currentThread; + } + + currentThread = threadData.NextThread; + } + + // Walk the waiters linked list and invoke callback for each matching thread + Target.TypeInfo conditionType = _target.Contracts.ManagedTypeSource.GetTypeInfo("System.Threading.Condition"); + TargetPointer waiter = _target.ReadPointer(condition + (ulong)conditionType.Fields["_waitersHead"].Offset); + + Target.TypeInfo waiterType = _target.Contracts.ManagedTypeSource.GetTypeInfo("System.Threading.Condition+Waiter"); + while (waiter != TargetPointer.Null) + { + if (waiterToThreadMap.TryGetValue(waiter.Value, out TargetPointer thread)) + { + callback(thread.Value, pUserData); +#if DEBUG + cdacThreads?.Add(thread.Value); +#endif + } + + waiter = _target.ReadPointer(waiter + (ulong)waiterType.Fields["next"].Offset); + } + } + } + } + catch (System.Exception ex) + { + hr = ex.HResult; + } +#if DEBUG + if (_legacy is not null && fpCallback != 0) + List dacThreads = new(); + GCHandle dacHandle = GCHandle.Alloc(dacThreads); + int hrLocal = _legacy.EnumerateMonitorEventWaitList(vmObject, (nint)(delegate* unmanaged)&CollectEnumerationCallback, GCHandle.ToIntPtr(dacHandle)); + dacHandle.Free(); + Debug.ValidateHResult(hr, hrLocal); + if (hr == HResults.S_OK) + { + Debug.Assert( + cdacThreads!.SequenceEqual(dacThreads), + $"MonitorEventWaitList mismatch - cDAC: [{string.Join(",", cdacThreads!.Select(t => $"0x{t:x}"))}], DAC: [{string.Join(",", dacThreads.Select(t => $"0x{t:x}"))}]"); + } + } +#endif + return hr; + } public int GetAttachStateFlags(int* pRetVal) { diff --git a/src/native/managed/cdac/tests/UnitTests/DacDbiImplTests.cs b/src/native/managed/cdac/tests/UnitTests/DacDbiImplTests.cs index 291e498a3e6c97..55badbf51bd145 100644 --- a/src/native/managed/cdac/tests/UnitTests/DacDbiImplTests.cs +++ b/src/native/managed/cdac/tests/UnitTests/DacDbiImplTests.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Runtime.InteropServices; using Microsoft.Diagnostics.DataContractReader.Contracts; using Microsoft.Diagnostics.DataContractReader.Contracts.StackWalkHelpers; @@ -901,4 +902,227 @@ public void GetManagedStoppedContext_NoContextAvailable(MockTarget.Architecture Assert.Equal(System.HResults.S_OK, hr); Assert.Equal(0UL, retVal); } + + private const int ConditionWaitersHeadOffset = 8; + private const int WaiterNextOffset = 8; + + private static (DacDbiImpl DacDbi, TestPlaceholderTarget Target) CreateMonitorWaitListDacDbi( + MockTarget.Architecture arch, + TargetPointer objectAddr, + TargetPointer syncBlockAddr, + TargetPointer conditionTableAddr, + bool cwtFindsCondition, + (TargetPointer Thread, bool IsWaiting)[]? threads = null) + { + var helpers = new TargetTestHelpers(arch); + int ptrSize = arch.Is64Bit ? 8 : 4; + var builder = new TestPlaceholderTarget.Builder(arch); + var allocator = builder.MemoryBuilder.CreateAllocator(0x0040_0000, 0x0040_F000); + var mockMts = new Mock(); + + var globalFragment = allocator.Allocate((ulong)ptrSize, "ConditionTableGlobal"); + helpers.WritePointer(globalFragment.Data, conditionTableAddr.Value); + TargetPointer conditionTableSlotAddr = new(globalFragment.Address); + mockMts.Setup(m => m.TryGetStaticFieldAddress( + "System.Threading.Monitor", "s_conditionTable", out conditionTableSlotAddr)) + .Returns(true); + + mockMts.Setup(m => m.GetTypeInfo("System.Threading.Condition")).Returns(new Target.TypeInfo + { + Size = (uint)(ConditionWaitersHeadOffset + ptrSize), + Fields = new Dictionary + { + ["_waitersHead"] = new Target.FieldInfo { Offset = ConditionWaitersHeadOffset, TypeName = null }, + } + }); + mockMts.Setup(m => m.GetTypeInfo("System.Threading.Condition+Waiter")).Returns(new Target.TypeInfo + { + Size = (uint)(WaiterNextOffset + ptrSize), + Fields = new Dictionary + { + ["next"] = new Target.FieldInfo { Offset = WaiterNextOffset, TypeName = null }, + } + }); + + var waitingThreads = threads?.Where(t => t.IsWaiting).ToArray() ?? []; + var waiterFragments = new MockMemorySpace.HeapFragment[waitingThreads.Length]; + for (int i = 0; i < waitingThreads.Length; i++) + { + waiterFragments[i] = allocator.Allocate((ulong)(WaiterNextOffset + ptrSize), $"Waiter_{i}"); + } + + for (int i = 0; i < waiterFragments.Length; i++) + { + ulong nextAddr = i + 1 < waiterFragments.Length ? waiterFragments[i + 1].Address : 0; + helpers.WritePointer(waiterFragments[i].Data.AsSpan(WaiterNextOffset), nextAddr); + } + + var conditionFragment = allocator.Allocate((ulong)(ConditionWaitersHeadOffset + ptrSize), "Condition"); + ulong waitersHeadValue = waiterFragments.Length > 0 ? waiterFragments[0].Address : 0; + helpers.WritePointer(conditionFragment.Data.AsSpan(ConditionWaitersHeadOffset), waitersHeadValue); + + var mockObject = new Mock(); + mockObject.Setup(o => o.GetSyncBlockAddress(objectAddr)).Returns(syncBlockAddr); + + var mockCwt = new Mock(); + TargetPointer cwtOutCondition = new(conditionFragment.Address); + mockCwt.Setup(c => c.TryGetValue(conditionTableAddr, objectAddr, out cwtOutCondition)) + .Returns(cwtFindsCondition); + + var mockThread = new Mock(); + if (threads is not null && threads.Length > 0) + { + var threadStore = new ThreadStoreData(threads.Length, threads[0].Thread, TargetPointer.Null, TargetPointer.Null); + mockThread.Setup(t => t.GetThreadStoreData()).Returns(threadStore); + for (int i = 0; i < threads.Length; i++) + { + TargetPointer nextThread = i + 1 < threads.Length ? threads[i + 1].Thread : TargetPointer.Null; + var threadData = new ThreadData( + threads[i].Thread, 0, default, default, false, + TargetPointer.Null, TargetPointer.Null, TargetPointer.Null, + TargetPointer.Null, TargetPointer.Null, TargetPointer.Null, + TargetPointer.Null, false, false, nextThread, + TargetPointer.Null, false, TargetPointer.Null); + mockThread.Setup(t => t.GetThreadData(threads[i].Thread)).Returns(threadData); + } + } + else + { + var threadStore = new ThreadStoreData(0, TargetPointer.Null, TargetPointer.Null, TargetPointer.Null); + mockThread.Setup(t => t.GetThreadStoreData()).Returns(threadStore); + } + + int waiterIdx = 0; + if (threads is not null) + { + foreach (var (thread, isWaiting) in threads) + { + if (isWaiting) + { + var slotFragment = allocator.Allocate((ulong)ptrSize, $"WaiterSlot_{thread.Value:x}"); + helpers.WritePointer(slotFragment.Data, waiterFragments[waiterIdx].Address); + + TargetPointer outAddr = new(slotFragment.Address); + mockMts.Setup(m => m.TryGetThreadStaticFieldAddress( + "System.Threading.Condition", "t_waiterForCurrentThread", thread, out outAddr)) + .Returns(true); + waiterIdx++; + } + else + { + TargetPointer nullAddr = TargetPointer.Null; + mockMts.Setup(m => m.TryGetThreadStaticFieldAddress( + "System.Threading.Condition", "t_waiterForCurrentThread", thread, out nullAddr)) + .Returns(false); + } + } + } + + builder.AddMockContract(mockObject); + builder.AddMockContract(mockCwt); + builder.AddMockContract(mockThread); + builder.AddMockContract(mockMts); + + var target = builder.Build(); + var dacDbi = new DacDbiImpl(target, legacyObj: null); + return (dacDbi, target); + } + + [UnmanagedCallersOnly] + private static void CollectThreadCallback(ulong value, nint pUserData) + { + GCHandle handle = GCHandle.FromIntPtr(pUserData); + ((List)handle.Target!).Add(value); + } + + private static (int Hr, List Threads) RunEnumerateMonitorEventWaitList(DacDbiImpl dacDbi, ulong vmObject) + { + List threads = new(); + GCHandle gcHandle = GCHandle.Alloc(threads); + int hr = dacDbi.EnumerateMonitorEventWaitList( + vmObject, + (nint)(delegate* unmanaged)&CollectThreadCallback, + GCHandle.ToIntPtr(gcHandle)); + gcHandle.Free(); + return (hr, threads); + } + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void EnumerateMonitorEventWaitList_NoSyncBlock(MockTarget.Architecture arch) + { + var mockObject = new Mock(); + mockObject.Setup(o => o.GetSyncBlockAddress(new TargetPointer(0x1000))).Returns(TargetPointer.Null); + + var target = new TestPlaceholderTarget.Builder(arch) + .UseReader((_, _) => -1) + .AddMockContract(mockObject) + .Build(); + var dacDbi = new DacDbiImpl(target, legacyObj: null); + + var (hr, threads) = RunEnumerateMonitorEventWaitList(dacDbi, 0x1000); + Assert.Equal(System.HResults.S_OK, hr); + Assert.Empty(threads); + } + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void EnumerateMonitorEventWaitList_NoConditionOrEmptyList(MockTarget.Architecture arch) + { + // Object not found in ConditionalWeakTable, so no waiters are returned. + var (dacDbi1, _) = CreateMonitorWaitListDacDbi(arch, new(0x1000), new(0x2000), new(0x3000), cwtFindsCondition: false); + var (hr1, threads1) = RunEnumerateMonitorEventWaitList(dacDbi1, 0x1000); + Assert.Equal(System.HResults.S_OK, hr1); + Assert.Empty(threads1); + + // Condition exists but waiter list is empty, so no waiters are returned. + var (dacDbi2, _) = CreateMonitorWaitListDacDbi(arch, new(0x1000), new(0x2000), new(0x3000), cwtFindsCondition: true); + var (hr2, threads2) = RunEnumerateMonitorEventWaitList(dacDbi2, 0x1000); + Assert.Equal(System.HResults.S_OK, hr2); + Assert.Empty(threads2); + } + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void EnumerateMonitorEventWaitList_NullCallback(MockTarget.Architecture arch) + { + var mockObject = new Mock(); + mockObject.Setup(o => o.GetSyncBlockAddress(new TargetPointer(0x1000))).Returns(new TargetPointer(0x2000)); + var target = new TestPlaceholderTarget.Builder(arch) + .UseReader((_, _) => -1) + .AddMockContract(mockObject) + .Build(); + var dacDbi = new DacDbiImpl(target, legacyObj: null); + int hr = dacDbi.EnumerateMonitorEventWaitList(0x1000, 0, 0); + Assert.NotEqual(System.HResults.S_OK, hr); + } + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void EnumerateMonitorEventWaitList_WaitersReturnMatchingThreads(MockTarget.Architecture arch) + { + TargetPointer thread1 = new(0x6000); + TargetPointer thread2 = new(0x6100); + + // Single waiter + var (dacDbi1, _) = CreateMonitorWaitListDacDbi(arch, new(0x1000), new(0x2000), new(0x3000), + cwtFindsCondition: true, threads: [(thread1, true)]); + var (hr1, result1) = RunEnumerateMonitorEventWaitList(dacDbi1, 0x1000); + Assert.Equal(System.HResults.S_OK, hr1); + Assert.Equal(new[] { thread1.Value }, result1); + + // Two waiters + var (dacDbi2, _) = CreateMonitorWaitListDacDbi(arch, new(0x1000), new(0x2000), new(0x3000), + cwtFindsCondition: true, threads: [(thread1, true), (thread2, true)]); + var (hr2, result2) = RunEnumerateMonitorEventWaitList(dacDbi2, 0x1000); + Assert.Equal(System.HResults.S_OK, hr2); + Assert.Equal(new[] { thread1.Value, thread2.Value }, result2); + + // Non-waiting thread skipped + var (dacDbi3, _) = CreateMonitorWaitListDacDbi(arch, new(0x1000), new(0x2000), new(0x3000), + cwtFindsCondition: true, threads: [(thread1, true), (thread2, false)]); + var (hr3, result3) = RunEnumerateMonitorEventWaitList(dacDbi3, 0x1000); + Assert.Equal(System.HResults.S_OK, hr3); + Assert.Equal(new[] { thread1.Value }, result3); + } }