From 891c68708d76c6fbeeaa184a7b7e5c9b5c45e2cf Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 10 Jun 2026 14:33:28 +0000 Subject: [PATCH 01/20] Implement DacDbi cDAC APIs CreateRefWalk, DeleteRefWalk, WalkRefs Co-authored-by: rcj1 <77995559+rcj1@users.noreply.github.com> --- src/coreclr/debug/daccess/dacdbiimpl.cpp | 8 +- src/coreclr/debug/daccess/dacdbiimpl.h | 6 +- src/coreclr/debug/di/process.cpp | 6 +- src/coreclr/debug/di/rspriv.h | 2 +- src/coreclr/debug/inc/dacdbiinterface.h | 3 +- src/coreclr/inc/dacdbi.idl | 2 +- .../Contracts/IStackWalk.cs | 1 + .../Contracts/StackWalk/GC/GcScanContext.cs | 3 + .../Contracts/StackWalk/GC/StackRefData.cs | 1 + .../Contracts/StackWalk/StackWalk_1.cs | 1 + .../Dbi/DacDbiImpl.cs | 148 +++++++++++++++- .../Dbi/Helpers/RefWalk.cs | 167 ++++++++++++++++++ .../Dbi/IDacDbiInterface.cs | 27 ++- .../DacDbi/DacDbiRefWalkDumpTests.cs | 123 +++++++++++++ 14 files changed, 477 insertions(+), 21 deletions(-) create mode 100644 src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/Helpers/RefWalk.cs create mode 100644 src/native/managed/cdac/tests/DumpTests/DacDbi/DacDbiRefWalkDumpTests.cs diff --git a/src/coreclr/debug/daccess/dacdbiimpl.cpp b/src/coreclr/debug/daccess/dacdbiimpl.cpp index 54d31a3cdeb424..0a166b36b4870f 100644 --- a/src/coreclr/debug/daccess/dacdbiimpl.cpp +++ b/src/coreclr/debug/daccess/dacdbiimpl.cpp @@ -6861,11 +6861,11 @@ HRESULT STDMETHODCALLTYPE DacDbiInterfaceImpl::IsValidObject(CORDB_ADDRESS obj, return hr; } -HRESULT STDMETHODCALLTYPE DacDbiInterfaceImpl::CreateRefWalk(OUT RefWalkHandle * pHandle, BOOL walkStacks, BOOL walkFQ, UINT32 handleWalkMask) +HRESULT STDMETHODCALLTYPE DacDbiInterfaceImpl::CreateRefWalk(OUT RefWalkHandle * pHandle, BOOL walkStacks, UINT32 handleWalkMask) { DD_ENTER_MAY_THROW; - DacRefWalker *walker = new (nothrow) DacRefWalker(this, walkStacks, walkFQ, handleWalkMask, TRUE); + DacRefWalker *walker = new (nothrow) DacRefWalker(this, walkStacks, handleWalkMask, TRUE); if (walker == NULL) return E_OUTOFMEMORY; @@ -7506,8 +7506,8 @@ HRESULT STDMETHODCALLTYPE DacDbiInterfaceImpl::GetGenericArgTokenIndex(VMPTR_Met return S_OK; } -DacRefWalker::DacRefWalker(ClrDataAccess *dac, BOOL walkStacks, BOOL walkFQ, UINT32 handleMask, BOOL resolvePointers) - : mDac(dac), mWalkStacks(walkStacks), mWalkFQ(walkFQ), mHandleMask(handleMask), mStackWalker(NULL), +DacRefWalker::DacRefWalker(ClrDataAccess *dac, BOOL walkStacks, UINT32 handleMask, BOOL resolvePointers) + : mDac(dac), mWalkStacks(walkStacks), mHandleMask(handleMask), mStackWalker(NULL), mResolvePointers(resolvePointers), mHandleWalker(NULL), mFQStart(PTR_NULL), mFQEnd(PTR_NULL), mFQCurr(PTR_NULL) { } diff --git a/src/coreclr/debug/daccess/dacdbiimpl.h b/src/coreclr/debug/daccess/dacdbiimpl.h index a59bedda6e60c3..0065010c1290d8 100644 --- a/src/coreclr/debug/daccess/dacdbiimpl.h +++ b/src/coreclr/debug/daccess/dacdbiimpl.h @@ -123,7 +123,7 @@ class DacDbiInterfaceImpl : HRESULT STDMETHODCALLTYPE IsValidObject(CORDB_ADDRESS obj, OUT BOOL * pResult); - HRESULT STDMETHODCALLTYPE CreateRefWalk(RefWalkHandle * pHandle, BOOL walkStacks, BOOL walkFQ, UINT32 handleWalkMask); + HRESULT STDMETHODCALLTYPE CreateRefWalk(RefWalkHandle * pHandle, BOOL walkStacks, UINT32 handleWalkMask); HRESULT STDMETHODCALLTYPE DeleteRefWalk(RefWalkHandle handle); HRESULT STDMETHODCALLTYPE WalkRefs(RefWalkHandle handle, ULONG count, OUT DacGcReference * objects, OUT ULONG *pFetched); @@ -964,7 +964,7 @@ class DDHolder class DacRefWalker { public: - DacRefWalker(ClrDataAccess *dac, BOOL walkStacks, BOOL walkFQ, UINT32 handleMask, BOOL resolvePointers); + DacRefWalker(ClrDataAccess *dac, BOOL walkStacks, UINT32 handleMask, BOOL resolvePointers); ~DacRefWalker(); HRESULT Init(); @@ -977,7 +977,7 @@ class DacRefWalker private: ClrDataAccess *mDac; - BOOL mWalkStacks, mWalkFQ; + BOOL mWalkStacks; UINT32 mHandleMask; // Stacks diff --git a/src/coreclr/debug/di/process.cpp b/src/coreclr/debug/di/process.cpp index dc654fe21480d0..09abc68a736ac7 100644 --- a/src/coreclr/debug/di/process.cpp +++ b/src/coreclr/debug/di/process.cpp @@ -2552,13 +2552,13 @@ HRESULT CordbProcess::GetTypeForObject(CORDB_ADDRESS addr, CordbType **ppType, C // CordbRefEnum // ****************************************** CordbRefEnum::CordbRefEnum(CordbProcess *proc, BOOL walkWeakRefs) - : CordbBase(proc, 0, enumCordbHeap), mRefHandle(0), mEnumStacksFQ(TRUE), + : CordbBase(proc, 0, enumCordbHeap), mRefHandle(0), mEnumStacks(TRUE), mHandleMask((UINT32)(walkWeakRefs ? CorHandleAll : CorHandleStrongOnly)) { } CordbRefEnum::CordbRefEnum(CordbProcess *proc, CorGCReferenceType types) - : CordbBase(proc, 0, enumCordbHeap), mRefHandle(0), mEnumStacksFQ(FALSE), + : CordbBase(proc, 0, enumCordbHeap), mRefHandle(0), mEnumStacks(FALSE), mHandleMask((UINT32)types) { } @@ -2657,7 +2657,7 @@ HRESULT CordbRefEnum::Next(ULONG celt, COR_GC_REFERENCE refs[], ULONG *pceltFetc EX_TRY { if (!mRefHandle) - hr = process->GetDAC()->CreateRefWalk(&mRefHandle, mEnumStacksFQ, mEnumStacksFQ, mHandleMask); + hr = process->GetDAC()->CreateRefWalk(&mRefHandle, mEnumStacks, mHandleMask); if (SUCCEEDED(hr)) { diff --git a/src/coreclr/debug/di/rspriv.h b/src/coreclr/debug/di/rspriv.h index c61d532dde6680..91e6b24b055859 100644 --- a/src/coreclr/debug/di/rspriv.h +++ b/src/coreclr/debug/di/rspriv.h @@ -10618,7 +10618,7 @@ class CordbRefEnum : public CordbBase, public ICorDebugGCReferenceEnum private: RefWalkHandle mRefHandle; - BOOL mEnumStacksFQ; + BOOL mEnumStacks; UINT32 mHandleMask; }; diff --git a/src/coreclr/debug/inc/dacdbiinterface.h b/src/coreclr/debug/inc/dacdbiinterface.h index 5665c617629706..ff9f25c38fdd8b 100644 --- a/src/coreclr/debug/inc/dacdbiinterface.h +++ b/src/coreclr/debug/inc/dacdbiinterface.h @@ -2012,13 +2012,12 @@ IDacDbiInterface : public IUnknown // Parameters: // pHandle - out - the reference walk handle to create // walkStacks - in - whether or not to report stack references - // walkFQ - in - whether or not to report references from the finalizer queue // handleWalkMask - in - the types of handles report (see CorGCReferenceType, cordebug.idl) // Returns: // An HRESULT indicating whether it succeeded or failed. // Exceptions: // Returns an HRESULT indicating success or failure. - virtual HRESULT STDMETHODCALLTYPE CreateRefWalk(OUT RefWalkHandle * pHandle, BOOL walkStacks, BOOL walkFQ, UINT32 handleWalkMask) = 0; + virtual HRESULT STDMETHODCALLTYPE CreateRefWalk(OUT RefWalkHandle * pHandle, BOOL walkStacks, UINT32 handleWalkMask) = 0; // Deletes a reference walk. // Parameters: diff --git a/src/coreclr/inc/dacdbi.idl b/src/coreclr/inc/dacdbi.idl index 700bd4c85e2618..53eeb324371e3e 100644 --- a/src/coreclr/inc/dacdbi.idl +++ b/src/coreclr/inc/dacdbi.idl @@ -384,7 +384,7 @@ interface IDacDbiInterface : IUnknown HRESULT IsValidObject([in] CORDB_ADDRESS obj, [out] BOOL * pResult); // Reference Walking - HRESULT CreateRefWalk([out] RefWalkHandle * pHandle, [in] BOOL walkStacks, [in] BOOL walkFQ, [in] UINT32 handleWalkMask); + HRESULT CreateRefWalk([out] RefWalkHandle * pHandle, [in] BOOL walkStacks, [in] UINT32 handleWalkMask); HRESULT DeleteRefWalk([in] RefWalkHandle handle); HRESULT WalkRefs([in] RefWalkHandle handle, [in] ULONG count, [out] struct DacGcReference * refs, [out] ULONG * pFetched); diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IStackWalk.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IStackWalk.cs index c3b73db751a660..f3221d7dc1843e 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IStackWalk.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IStackWalk.cs @@ -37,6 +37,7 @@ public enum StackWalkState public class StackReferenceData { public bool HasRegisterInformation { get; init; } + public bool IsInteriorPointer { get; init; } public int Register { get; init; } public int Offset { get; init; } public TargetPointer Address { get; init; } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanContext.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanContext.cs index 1a2b8b5b2c226e..a1a983532faaf9 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanContext.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanContext.cs @@ -34,6 +34,7 @@ public void RecordDeferredFrame(TargetPointer frameAddress) StackRefs.Add(new StackRefData { HasRegisterInformation = false, + IsInteriorPointer = false, Register = 0, Offset = 0, Address = 0, @@ -71,6 +72,7 @@ public void GCEnumCallback(TargetPointer pObject, GcScanFlags flags, GcScanSlotL StackRefData data = new() { HasRegisterInformation = true, + IsInteriorPointer = flags.HasFlag(GcScanFlags.GC_CALL_INTERIOR), Register = loc.Reg, Offset = loc.RegOffset, Address = addr, @@ -109,6 +111,7 @@ public void GCReportCallback(TargetPointer ppObj, GcScanFlags flags) StackRefData data = new() { HasRegisterInformation = false, + IsInteriorPointer = flags.HasFlag(GcScanFlags.GC_CALL_INTERIOR), Register = 0, Offset = 0, Address = ppObj, diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/StackRefData.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/StackRefData.cs index 46e5bac46f6431..d09499a93c0dae 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/StackRefData.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/StackRefData.cs @@ -12,6 +12,7 @@ public enum SourceTypes } public bool HasRegisterInformation { get; set; } + public bool IsInteriorPointer { get; set; } public int Register { get; set; } public int Offset { get; set; } public TargetPointer Address { get; set; } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/StackWalk_1.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/StackWalk_1.cs index af08b278df0f8a..9341642d71bf30 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/StackWalk_1.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/StackWalk_1.cs @@ -271,6 +271,7 @@ IReadOnlyList IStackWalk.WalkStackReferences(ThreadData thre return scanContext.StackRefs.Select(r => new StackReferenceData { HasRegisterInformation = r.HasRegisterInformation, + IsInteriorPointer = r.IsInteriorPointer, Register = r.Register, Offset = r.Offset, Address = r.Address, 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..b11219b0b4381d 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 @@ -3531,14 +3531,152 @@ public int IsValidObject(ulong obj, Interop.BOOL* pResult) return hr; } - public int CreateRefWalk(nuint* pHandle, Interop.BOOL walkStacks, Interop.BOOL walkFQ, uint handleWalkMask) - => LegacyFallbackHelper.CanFallback() && _legacy is not null ? _legacy.CreateRefWalk(pHandle, walkStacks, walkFQ, handleWalkMask) : HResults.E_NOTIMPL; + public int CreateRefWalk(nuint* pHandle, Interop.BOOL walkStacks, CorGCReferenceType handleWalkMask) + { + int hr = HResults.S_OK; + RefWalk? walk = null; + try + { + if (pHandle is null) + throw new NullReferenceException(nameof(pHandle)); + walk = new RefWalk(_target, walkStacks != Interop.BOOL.FALSE, handleWalkMask); + *pHandle = (nuint)((IEnum)walk).GetHandle(); + } + catch (System.Exception ex) + { + hr = ex.HResult; + } +#if DEBUG + if (_legacy is not null) + { + nuint legacyHandle = 0; + int hrLocal = _legacy.CreateRefWalk(&legacyHandle, walkStacks, handleWalkMask); + Debug.ValidateHResult(hr, hrLocal); + if (hrLocal == HResults.S_OK && walk is not null) + walk.LegacyHandle = legacyHandle; + else if (hrLocal == HResults.S_OK) + _legacy.DeleteRefWalk(legacyHandle); + } +#endif + return hr; + } public int DeleteRefWalk(nuint handle) - => LegacyFallbackHelper.CanFallback() && _legacy is not null ? _legacy.DeleteRefWalk(handle) : HResults.E_NOTIMPL; + { + if (handle == 0) + return HResults.S_OK; + + int hr = HResults.S_OK; + nuint legacyHandle = 0; + try + { + GCHandle gcHandle = GCHandle.FromIntPtr((nint)handle); + if (gcHandle.Target is not RefWalk walk) + throw new ArgumentException("Handle does not reference a valid RefWalk instance.", nameof(handle)); + legacyHandle = walk.LegacyHandle; + ((IEnum)walk).Dispose(); + gcHandle.Free(); + } + catch (System.Exception ex) + { + hr = ex.HResult; + } +#if DEBUG + if (_legacy is not null && legacyHandle != 0) + { + int hrLocal = _legacy.DeleteRefWalk(legacyHandle); + Debug.ValidateHResult(hr, hrLocal); + } +#endif + return hr; + } - public int WalkRefs(nuint handle, uint count, nint refs, uint* pFetched) - => LegacyFallbackHelper.CanFallback() && _legacy is not null ? _legacy.WalkRefs(handle, count, refs, pFetched) : HResults.E_NOTIMPL; + // Should be called repeatedly until it returns S_FALSE. + public int WalkRefs(nuint handle, uint count, DacGcReference* refs, uint* pFetched) + { + RefWalk walk; + try + { + if (pFetched is null) + throw new NullReferenceException(nameof(pFetched)); + if (refs is null && count > 0) + throw new NullReferenceException(nameof(refs)); + if (handle == 0) + throw new ArgumentException("Handle is invalid.", nameof(handle)); + GCHandle gcHandle = GCHandle.FromIntPtr((nint)handle); + if (gcHandle.Target is not RefWalk rw) + throw new ArgumentException("Handle does not reference a valid RefWalk instance.", nameof(handle)); + walk = rw; + *pFetched = 0; + } + catch (System.Exception ex) + { + return ex.HResult; + } + + int hr = HResults.S_OK; + uint i = 0; + try + { + while (i < count && walk.Enumerator.MoveNext()) + refs[i++] = walk.Enumerator.Current; + + // A clean batch reports S_FALSE iff we couldn't fill the caller's request. + if (i < count) + hr = HResults.S_FALSE; + } + catch (System.Exception ex) + { + hr = ex.HResult; + } + + *pFetched = i; + +#if DEBUG + if (_legacy is not null && walk.LegacyHandle != 0 && count > 0) + { + // Parity check covers the handle prefix only. + DacGcReference* legacyRefs = stackalloc DacGcReference[(int)count]; + uint legacyFetched = 0; + int hrLocal = _legacy.WalkRefs(walk.LegacyHandle, count, legacyRefs, &legacyFetched); + Debug.ValidateHResult(hr, hrLocal); + + uint cdacHandlePrefix = CountHandlePrefix(refs, i); + uint legacyHandlePrefix = CountHandlePrefix(legacyRefs, legacyFetched); + Debug.Assert( + cdacHandlePrefix == legacyHandlePrefix, + $"cDAC handle-prefix count {cdacHandlePrefix}, legacy {legacyHandlePrefix}"); + + uint compare = Math.Min(cdacHandlePrefix, legacyHandlePrefix); + for (uint j = 0; j < compare; j++) + { + Debug.Assert(refs[j].dwType == legacyRefs[j].dwType, + $"refs[{j}].dwType cDAC={refs[j].dwType:X}, legacy={legacyRefs[j].dwType:X}"); + Debug.Assert(refs[j].vmDomain == legacyRefs[j].vmDomain, + $"refs[{j}].vmDomain cDAC=0x{refs[j].vmDomain:X}, legacy=0x{legacyRefs[j].vmDomain:X}"); + Debug.Assert(refs[j].objHnd == legacyRefs[j].objHnd, + $"refs[{j}].objHnd cDAC=0x{refs[j].objHnd:X}, legacy=0x{legacyRefs[j].objHnd:X}"); + Debug.Assert(refs[j].i64ExtraData == legacyRefs[j].i64ExtraData, + $"refs[{j}].i64ExtraData cDAC=0x{refs[j].i64ExtraData:X}, legacy=0x{legacyRefs[j].i64ExtraData:X}"); + } + } + + static uint CountHandlePrefix(DacGcReference* buffer, uint length) + { + for (uint j = 0; j < length; j++) + { + CorGCReferenceType dwType = buffer[j].dwType; + if (dwType == CorGCReferenceType.CorReferenceStack) + { + return j; + } + } + return length; + } +#endif + + return hr; + } public int GetTypeID(ulong obj, COR_TYPEID* pType) { diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/Helpers/RefWalk.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/Helpers/RefWalk.cs new file mode 100644 index 00000000000000..6d9813b77254c5 --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/Helpers/RefWalk.cs @@ -0,0 +1,167 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using Microsoft.Diagnostics.DataContractReader.Contracts; + +namespace Microsoft.Diagnostics.DataContractReader.Legacy; + +/// +/// cDAC port of the native DacRefWalker. +/// +internal sealed class RefWalk : IEnum +{ + private const uint CDAC_DEFERRED_FRAME = 0x40000000; + private readonly Target _target; + private readonly IGC _gc; + private readonly bool _walkStacks; + private readonly CorGCReferenceType _handleWalkMask; + + public IEnumerator Enumerator { get; } + public nuint LegacyHandle { get; set; } = 0; + + public RefWalk(Target target, bool walkStacks, CorGCReferenceType handleWalkMask) + { + _target = target; + _gc = target.Contracts.GC; + _walkStacks = walkStacks; + _handleWalkMask = handleWalkMask; + Enumerator = Walk().GetEnumerator(); + } + + private IEnumerable Walk() + { + // The single AppDomain pointer; used to fill vmDomain for both handle and stack references. + TargetPointer appDomain = _target.ReadPointer(_target.ReadGlobalPointer(Constants.Globals.AppDomain)); + + if (_handleWalkMask != 0) + { + foreach (DacGcReference reference in WalkHandles(appDomain)) + yield return reference; + } + + if (_walkStacks) + { + foreach (DacGcReference reference in WalkStacks(appDomain)) + yield return reference; + } + } + + private IEnumerable WalkHandles(TargetPointer appDomain) + { + HandleType[] requestedTypes = GetRequestedHandleTypes(); + if (requestedTypes.Length == 0) + yield break; + + foreach (HandleData handle in _gc.GetHandles(requestedTypes)) + { + if (!TryMapHandle(handle, out CorGCReferenceType dwType, out ulong extraData)) + continue; + yield return new DacGcReference + { + vmDomain = appDomain.Value, + objHnd = handle.Handle.Value, + dwType = dwType, + i64ExtraData = extraData, + }; + } + } + + private HandleType[] GetRequestedHandleTypes() + { + // Mirror native DacRefWalker::GetHandleWalkerMask: translate the CorGCReferenceType bits + // in the mask into the handle types consumed by IGC.GetHandles. + List types = new(); + if (_handleWalkMask.HasFlag(CorGCReferenceType.CorHandleStrong)) + types.Add(HandleType.Strong); + if (_handleWalkMask.HasFlag(CorGCReferenceType.CorHandleStrongPinning)) + types.Add(HandleType.Pinned); + if (_handleWalkMask.HasFlag(CorGCReferenceType.CorHandleWeakShort)) + types.Add(HandleType.WeakShort); + if (_handleWalkMask.HasFlag(CorGCReferenceType.CorHandleWeakLong)) + types.Add(HandleType.WeakLong); + if (_handleWalkMask.HasFlag(CorGCReferenceType.CorHandleWeakRefCount) || _handleWalkMask.HasFlag(CorGCReferenceType.CorHandleStrongRefCount)) + types.Add(HandleType.RefCounted); + if (_handleWalkMask.HasFlag(CorGCReferenceType.CorHandleStrongDependent)) + types.Add(HandleType.Dependent); + + if (types.Count == 0) + return []; + + // Only request types the target actually supports + HashSet supported = new(_gc.GetSupportedHandleTypes()); + types.RemoveAll(t => !supported.Contains(t)); + return types.ToArray(); + } + + private bool TryMapHandle(HandleData handle, out CorGCReferenceType dwType, out ulong extraData) + { + extraData = 0; + switch (_gc.GetHandleTypes([handle.Type])[0]) + { + case HandleType.Strong: + dwType = CorGCReferenceType.CorHandleStrong; + return true; + case HandleType.Pinned: + dwType = CorGCReferenceType.CorHandleStrongPinning; + return true; + case HandleType.WeakShort: + dwType = CorGCReferenceType.CorHandleWeakShort; + return true; + case HandleType.WeakLong: + dwType = CorGCReferenceType.CorHandleWeakLong; + return true; + case HandleType.RefCounted: + extraData = handle.RefCount; + dwType = handle.RefCount != 0 + ? CorGCReferenceType.CorHandleStrongRefCount + : CorGCReferenceType.CorHandleWeakRefCount; + return true; + case HandleType.Dependent: + dwType = CorGCReferenceType.CorHandleStrongDependent; + extraData = handle.Secondary.Value; + return true; + default: + dwType = 0; + return false; + } + } + + private IEnumerable WalkStacks(TargetPointer appDomain) + { + IThread threadContract = _target.Contracts.Thread; + IStackWalk stackWalkContract = _target.Contracts.StackWalk; + + ThreadStoreData threadStore = threadContract.GetThreadStoreData(); + TargetPointer threadAddr = threadStore.FirstThread; + while (threadAddr != TargetPointer.Null) + { + ThreadData threadData = threadContract.GetThreadData(threadAddr); + + foreach (StackReferenceData stackRef in stackWalkContract.WalkStackReferences(threadData)) + { + // Skip cDAC-private deferred-frame markers; they are not real GC references. + if ((stackRef.Flags & CDAC_DEFERRED_FRAME) != 0) + continue; + + DacGcReference reference = new() + { + vmDomain = appDomain.Value, + dwType = CorGCReferenceType.CorReferenceStack, + i64ExtraData = 0, + }; + + // Interior pointers, Frame refs, and enregistered vars are reported as a direct object pointer with the low bit set; + // everything else is reported by the address of the stack slot holding the object. + if (stackRef.IsInteriorPointer || stackRef.Address == TargetPointer.Null) + reference.pObject = stackRef.Object.Value | 1; + else + reference.objHnd = stackRef.Address.Value; + + yield return reference; + } + + threadAddr = threadData.NextThread; + } + } +} diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/IDacDbiInterface.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/IDacDbiInterface.cs index 48f9c08fe109b8..e79582288b63ea 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/IDacDbiInterface.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/IDacDbiInterface.cs @@ -130,6 +130,16 @@ public struct COR_HEAPOBJECT public COR_TYPEID type; } +[StructLayout(LayoutKind.Explicit)] +public struct DacGcReference +{ + [FieldOffset(0)] public ulong vmDomain; + [FieldOffset(8)] public ulong pObject; + [FieldOffset(8)] public ulong objHnd; + [FieldOffset(16)] public CorGCReferenceType dwType; + [FieldOffset(24)] public ulong i64ExtraData; +} + [StructLayout(LayoutKind.Sequential)] public struct COR_SEGMENT { @@ -351,6 +361,19 @@ public enum IlNum : int TYPECTXT_ILNUM = -3, } +[Flags] +public enum CorGCReferenceType : uint +{ + CorHandleStrong = 1 << 0, + CorHandleStrongPinning = 1 << 1, + CorHandleWeakShort = 1 << 2, + CorHandleWeakLong = 1 << 3, + CorHandleWeakRefCount = 1 << 4, + CorHandleStrongRefCount = 1 << 5, + CorHandleStrongDependent = 1 << 6, + CorReferenceStack = 0x80000001, +} + // Name-surface projection of IDacDbiInterface in native method order for COM binding validation. // Parameter shapes are intentionally coarse placeholders and will be refined with method implementation work. [GeneratedComInterface] @@ -669,13 +692,13 @@ int EnumerateTypeHandleParams(ulong vmTypeHandle, int IsValidObject(ulong obj, Interop.BOOL* pResult); [PreserveSig] - int CreateRefWalk(nuint* pHandle, Interop.BOOL walkStacks, Interop.BOOL walkFQ, uint handleWalkMask); + int CreateRefWalk(nuint* pHandle, Interop.BOOL walkStacks, CorGCReferenceType handleWalkMask); [PreserveSig] int DeleteRefWalk(nuint handle); [PreserveSig] - int WalkRefs(nuint handle, uint count, nint refs, uint* pFetched); + int WalkRefs(nuint handle, uint count, DacGcReference* refs, uint* pFetched); [PreserveSig] int GetTypeID(ulong obj, COR_TYPEID* pType); diff --git a/src/native/managed/cdac/tests/DumpTests/DacDbi/DacDbiRefWalkDumpTests.cs b/src/native/managed/cdac/tests/DumpTests/DacDbi/DacDbiRefWalkDumpTests.cs new file mode 100644 index 00000000000000..fe4cdaaa6ee255 --- /dev/null +++ b/src/native/managed/cdac/tests/DumpTests/DacDbi/DacDbiRefWalkDumpTests.cs @@ -0,0 +1,123 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Linq; +using Microsoft.Diagnostics.DataContractReader.Contracts; +using Microsoft.Diagnostics.DataContractReader.Legacy; +using Microsoft.Diagnostics.DataContractReader.TestInfrastructure; +using Xunit; + +namespace Microsoft.Diagnostics.DataContractReader.DumpTests; + +/// +/// Dump-based integration tests for the ref-walking APIs +/// ( / / +/// ), cross-validated against the GC handle table +/// and the stack-reference walk in the GCRoots debuggee. +/// +public class DacDbiRefWalkDumpTests : DumpTestBase +{ + protected override string DebuggeeName => "GCRoots"; + + private DacDbiImpl CreateDacDbi() => new DacDbiImpl(Target, legacyObj: null); + + /// + /// Drives to completion and returns every reference reported. + /// + private static unsafe List WalkAllRefs(DacDbiImpl dbi, bool walkStacks, CorGCReferenceType handleWalkMask, uint batchSize = 32) + { + List refs = new(); + + nuint handle = 0; + int hr = dbi.CreateRefWalk(&handle, walkStacks ? Interop.BOOL.TRUE : Interop.BOOL.FALSE, handleWalkMask); + Assert.Equal(System.HResults.S_OK, hr); + Assert.True(handle != 0, "CreateRefWalk produced a null handle"); + + try + { + DacGcReference[] buffer = new DacGcReference[batchSize]; + while (true) + { + uint fetched = 0; + int walkHr; + fixed (DacGcReference* bufPtr = buffer) + { + walkHr = dbi.WalkRefs(handle, batchSize, bufPtr, &fetched); + } + + Assert.True( + walkHr == System.HResults.S_OK || walkHr == System.HResults.S_FALSE, + $"WalkRefs returned 0x{walkHr:x}"); + + for (uint i = 0; i < fetched; i++) + refs.Add(buffer[i]); + + if (walkHr == System.HResults.S_FALSE) + break; + } + } + finally + { + int delHr = dbi.DeleteRefWalk(handle); + Assert.Equal(System.HResults.S_OK, delHr); + } + + return refs; + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + public unsafe void WalkRefs_StrongHandles_MatchHandleTable(TestConfiguration config) + { + InitializeDumpTest(config); + DacDbiImpl dbi = CreateDacDbi(); + IGC gc = Target.Contracts.GC; + + List refs = WalkAllRefs(dbi, walkStacks: false, handleWalkMask: CorGCReferenceType.CorHandleStrong); + + // Every reference must be a strong handle reported by its (low-bit-clear) handle address. + HashSet walkedHandles = new(); + foreach (DacGcReference r in refs) + { + Assert.Equal(CorGCReferenceType.CorHandleStrong, r.dwType); + Assert.Equal(0ul, r.pObject & 1); + walkedHandles.Add(r.pObject); + } + + HashSet expectedHandles = gc.GetHandles([HandleType.Strong]).Select(h => h.Handle.Value).ToHashSet(); + Assert.True(expectedHandles.Count > 0, "Expected at least one strong handle in GCRoots."); + Assert.Equal(expectedHandles, walkedHandles); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + public unsafe void WalkRefs_StacksOnly_MatchStackReferenceWalk(TestConfiguration config) + { + InitializeDumpTest(config); + DacDbiImpl dbi = CreateDacDbi(); + IThread threadContract = Target.Contracts.Thread; + IStackWalk stackWalk = Target.Contracts.StackWalk; + + List refs = WalkAllRefs(dbi, walkStacks: true, handleWalkMask: CorGCReferenceType.CorReferenceStack); + + // Compute the expected count of stack references directly (excluding cDAC-private + // deferred-frame markers, which WalkRefs filters out). + // Mirrors StackWalkHelpers.GcScanFlags.CDAC_DEFERRED_FRAME (internal to the Contracts assembly). + const uint CdacDeferredFrame = 0x40000000; + int expected = 0; + ThreadStoreData threadStore = threadContract.GetThreadStoreData(); + TargetPointer threadAddr = threadStore.FirstThread; + while (threadAddr != TargetPointer.Null) + { + ThreadData td = threadContract.GetThreadData(threadAddr); + expected += stackWalk.WalkStackReferences(td).Count(r => (r.Flags & CdacDeferredFrame) == 0); + threadAddr = td.NextThread; + } + + foreach (DacGcReference r in refs) + Assert.Equal(CorGCReferenceType.CorReferenceStack, r.dwType); + + Assert.Equal(expected, refs.Count); + } +} From 71d7a3baa3d776604f866df4d5308bc5dd2ac2d5 Mon Sep 17 00:00:00 2001 From: rcj1 Date: Fri, 12 Jun 2026 14:54:53 -0700 Subject: [PATCH 02/20] code review --- .../Dbi/DacDbiImpl.cs | 2 +- .../cdac/tests/DumpTests/DacDbi/DacDbiRefWalkDumpTests.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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 b11219b0b4381d..35932c8489f18b 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 @@ -3636,7 +3636,7 @@ public int WalkRefs(nuint handle, uint count, DacGcReference* refs, uint* pFetch if (_legacy is not null && walk.LegacyHandle != 0 && count > 0) { // Parity check covers the handle prefix only. - DacGcReference* legacyRefs = stackalloc DacGcReference[(int)count]; + DacGcReference* legacyRefs = new DacGcReference[(int)count]; uint legacyFetched = 0; int hrLocal = _legacy.WalkRefs(walk.LegacyHandle, count, legacyRefs, &legacyFetched); Debug.ValidateHResult(hr, hrLocal); diff --git a/src/native/managed/cdac/tests/DumpTests/DacDbi/DacDbiRefWalkDumpTests.cs b/src/native/managed/cdac/tests/DumpTests/DacDbi/DacDbiRefWalkDumpTests.cs index fe4cdaaa6ee255..f70c03764a5345 100644 --- a/src/native/managed/cdac/tests/DumpTests/DacDbi/DacDbiRefWalkDumpTests.cs +++ b/src/native/managed/cdac/tests/DumpTests/DacDbi/DacDbiRefWalkDumpTests.cs @@ -99,7 +99,7 @@ public unsafe void WalkRefs_StacksOnly_MatchStackReferenceWalk(TestConfiguration IThread threadContract = Target.Contracts.Thread; IStackWalk stackWalk = Target.Contracts.StackWalk; - List refs = WalkAllRefs(dbi, walkStacks: true, handleWalkMask: CorGCReferenceType.CorReferenceStack); + List refs = WalkAllRefs(dbi, walkStacks: true, handleWalkMask: (CorGCReferenceType)0); // Compute the expected count of stack references directly (excluding cDAC-private // deferred-frame markers, which WalkRefs filters out). From 89e8153569b884f70cdf8281744d79ab4173f28b Mon Sep 17 00:00:00 2001 From: rcj1 Date: Fri, 12 Jun 2026 17:06:35 -0700 Subject: [PATCH 03/20] use marshal count --- .../Dbi/DacDbiImpl.cs | 8 +++----- .../Dbi/IDacDbiInterface.cs | 2 +- 2 files changed, 4 insertions(+), 6 deletions(-) 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 35932c8489f18b..61ef4cac59c432 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 @@ -3592,15 +3592,13 @@ public int DeleteRefWalk(nuint handle) } // Should be called repeatedly until it returns S_FALSE. - public int WalkRefs(nuint handle, uint count, DacGcReference* refs, uint* pFetched) + public int WalkRefs(nuint handle, uint count, [In, MarshalUsing(CountElementName = "count"), Out] DacGcReference[] refs, uint* pFetched) { RefWalk walk; try { if (pFetched is null) throw new NullReferenceException(nameof(pFetched)); - if (refs is null && count > 0) - throw new NullReferenceException(nameof(refs)); if (handle == 0) throw new ArgumentException("Handle is invalid.", nameof(handle)); GCHandle gcHandle = GCHandle.FromIntPtr((nint)handle); @@ -3636,7 +3634,7 @@ public int WalkRefs(nuint handle, uint count, DacGcReference* refs, uint* pFetch if (_legacy is not null && walk.LegacyHandle != 0 && count > 0) { // Parity check covers the handle prefix only. - DacGcReference* legacyRefs = new DacGcReference[(int)count]; + DacGcReference[] legacyRefs = new DacGcReference[(int)count]; uint legacyFetched = 0; int hrLocal = _legacy.WalkRefs(walk.LegacyHandle, count, legacyRefs, &legacyFetched); Debug.ValidateHResult(hr, hrLocal); @@ -3661,7 +3659,7 @@ public int WalkRefs(nuint handle, uint count, DacGcReference* refs, uint* pFetch } } - static uint CountHandlePrefix(DacGcReference* buffer, uint length) + static uint CountHandlePrefix(DacGcReference[] buffer, uint length) { for (uint j = 0; j < length; j++) { diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/IDacDbiInterface.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/IDacDbiInterface.cs index e79582288b63ea..77ffc82120543c 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/IDacDbiInterface.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/IDacDbiInterface.cs @@ -698,7 +698,7 @@ int EnumerateTypeHandleParams(ulong vmTypeHandle, int DeleteRefWalk(nuint handle); [PreserveSig] - int WalkRefs(nuint handle, uint count, DacGcReference* refs, uint* pFetched); + int WalkRefs(nuint handle, uint count, [In, Out, MarshalUsing(CountElementName = nameof(count))] DacGcReference[] refs, uint* pFetched); [PreserveSig] int GetTypeID(ulong obj, COR_TYPEID* pType); From d1ea1e24c2c7d490410eb933ce51be3a4256b072 Mon Sep 17 00:00:00 2001 From: Rachel Jarvi Date: Fri, 12 Jun 2026 17:19:39 -0700 Subject: [PATCH 04/20] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../cdac/tests/DumpTests/DacDbi/DacDbiRefWalkDumpTests.cs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/native/managed/cdac/tests/DumpTests/DacDbi/DacDbiRefWalkDumpTests.cs b/src/native/managed/cdac/tests/DumpTests/DacDbi/DacDbiRefWalkDumpTests.cs index f70c03764a5345..0aa63e18b0c527 100644 --- a/src/native/managed/cdac/tests/DumpTests/DacDbi/DacDbiRefWalkDumpTests.cs +++ b/src/native/managed/cdac/tests/DumpTests/DacDbi/DacDbiRefWalkDumpTests.cs @@ -40,17 +40,13 @@ private static unsafe List WalkAllRefs(DacDbiImpl dbi, bool walk while (true) { uint fetched = 0; - int walkHr; - fixed (DacGcReference* bufPtr = buffer) - { - walkHr = dbi.WalkRefs(handle, batchSize, bufPtr, &fetched); - } + int walkHr = dbi.WalkRefs(handle, (uint)batchSize, buffer, &fetched); Assert.True( walkHr == System.HResults.S_OK || walkHr == System.HResults.S_FALSE, $"WalkRefs returned 0x{walkHr:x}"); - for (uint i = 0; i < fetched; i++) + for (int i = 0; i < (int)fetched; i++) refs.Add(buffer[i]); if (walkHr == System.HResults.S_FALSE) From cd9fadc535434ba4741a8d71532acb1113d9b472 Mon Sep 17 00:00:00 2001 From: rcj1 Date: Wed, 17 Jun 2026 22:03:49 -0700 Subject: [PATCH 05/20] code review --- .../Contracts/Extensions/IGCExtensions.cs | 36 ++++ .../Contracts/IStackWalk.cs | 2 +- .../LinearReadCache.cs | 95 +++++++++ .../Target.cs | 4 + .../Contracts/Object_1.cs | 4 +- .../Contracts/StackWalk/GC/GcScanContext.cs | 57 ++++- .../Contracts/StackWalk/StackWalk_1.cs | 4 +- .../Dbi/Helpers/HeapWalk.cs | 199 +++--------------- .../Dbi/Helpers/RefWalk.cs | 19 +- .../SOSDacImpl.cs | 2 +- .../ContractDescriptorTarget.cs | 79 ++++++- .../DacDbi/DacDbiRefWalkDumpTests.cs | 2 +- .../DumpTests/StackReferenceDumpTests.cs | 18 +- .../TestPlaceholderTarget.cs | 2 + 14 files changed, 307 insertions(+), 216 deletions(-) create mode 100644 src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/Extensions/IGCExtensions.cs create mode 100644 src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/LinearReadCache.cs diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/Extensions/IGCExtensions.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/Extensions/IGCExtensions.cs new file mode 100644 index 00000000000000..d13f89ca7f105a --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/Extensions/IGCExtensions.cs @@ -0,0 +1,36 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Linq; + +namespace Microsoft.Diagnostics.DataContractReader.Contracts.Extensions; + +public static class IGCExtensions +{ + public static IEnumerable<(GCHeapSegmentInfo Segment, GCHeapData Heap)> EnumerateAllSegments(this IGC gc) + { + string[] gcIdentifiers = gc.GetGCIdentifiers(); + bool isWorkstation = gcIdentifiers.Contains(GCIdentifiers.Workstation); + foreach (GCHeapData heap in EnumerateHeaps(gc, isWorkstation)) + { + foreach (GCHeapSegmentInfo seg in gc.EnumerateHeapSegments(heap)) + { + yield return (seg, heap); + } + } + } + + private static IEnumerable EnumerateHeaps(IGC gc, bool isWorkstation) + { + if (isWorkstation) + { + yield return gc.GetHeapData(); + } + else + { + foreach (TargetPointer heapAddress in gc.GetGCHeaps()) + yield return gc.GetHeapData(heapAddress); + } + } +} diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IStackWalk.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IStackWalk.cs index 6fa76c9a0e637a..a1493fb4ca2d3d 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IStackWalk.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IStackWalk.cs @@ -85,7 +85,7 @@ public interface IStackWalk : IContract static string IContract.Name => nameof(StackWalk); public virtual IEnumerable CreateStackWalk(ThreadData threadData) => throw new NotImplementedException(); - IReadOnlyList WalkStackReferences(ThreadData threadData) => throw new NotImplementedException(); + IReadOnlyList WalkStackReferences(ThreadData threadData, bool resolveInteriorPointers) => throw new NotImplementedException(); byte[] GetRawContext(IStackDataFrameHandle stackDataFrameHandle) => throw new NotImplementedException(); TargetPointer GetFrameAddress(IStackDataFrameHandle stackDataFrameHandle) => throw new NotImplementedException(); string GetFrameName(TargetPointer frameIdentifier) => throw new NotImplementedException(); diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/LinearReadCache.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/LinearReadCache.cs new file mode 100644 index 00000000000000..e6c75a29979f50 --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/LinearReadCache.cs @@ -0,0 +1,95 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +// Linear page cache used by the per-object heap walk. +using System; +using System.Buffers.Binary; +using Microsoft.Diagnostics.DataContractReader; +namespace Microsoft.Diagnostics.DataContractReader.Abstractions; + +public sealed class LinearReadCache +{ + // Typical page size + private const uint PageSize = 0x1000; + + private readonly Target _target; + private readonly byte[] _page = new byte[PageSize]; + private ulong _currPageStart; + private uint _currPageSize; + + public LinearReadCache(Target target) + { + _target = target; + } + + public bool TryReadPointer(ulong addr, out TargetPointer value) + { + Span buffer = stackalloc byte[sizeof(ulong)]; + buffer = buffer.Slice(0, _target.PointerSize); + if (!TryRead(addr, buffer)) + { + value = TargetPointer.Null; + return false; + } + value = _target.ReadPointerFromSpan(buffer); + return true; + } + + public bool TryRead(ulong addr, Span dest) + { + // If the request misses the currently-cached page, try to load the page + // containing it. If that fails (e.g. the page is unmapped), or the request + // straddles the end of the cached page, fall back to a direct read. + if (addr < _currPageStart || addr - _currPageStart >= _currPageSize) + { + if (!MoveToPage(addr)) + return DirectRead(addr, dest); + } + + ulong offset = addr - _currPageStart; + if (offset + (ulong)dest.Length > _currPageSize) + return DirectRead(addr, dest); + + _page.AsSpan((int)offset, dest.Length).CopyTo(dest); + return true; + } + + public void Flush() + { + // Clear the current page cache + _currPageStart = 0; + _currPageSize = 0; + Array.Clear(_page, 0, _page.Length); + } + + private bool MoveToPage(ulong addr) + { + ulong pageStart = addr - (addr % PageSize); + try + { + _target.ReadBuffer(pageStart, _page.AsSpan(0, (int)PageSize)); + _currPageStart = pageStart; + _currPageSize = PageSize; + return true; + } + catch + { + _currPageStart = 0; + _currPageSize = 0; + return false; + } + } + + private bool DirectRead(ulong addr, Span dest) + { + try + { + _target.ReadBuffer(addr, dest); + return true; + } + catch + { + return false; + } + } +} diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Target.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Target.cs index b091152bcc7a66..b1824ab8d41263 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Target.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Target.cs @@ -340,4 +340,8 @@ public virtual void Flush(FlushScope scope) ProcessedData.Clear(); Contracts.Flush(scope); } + + public abstract void ActivateCache(); + + public abstract void DeactivateCache(); } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/Object_1.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/Object_1.cs index 6be137d7e28c2b..ff81dd148477f7 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/Object_1.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/Object_1.cs @@ -203,8 +203,8 @@ public ulong GetSize(TargetPointer address) { // Variable-size object (array or string): add the component data size. // Both Array and String share the m_NumComponents/m_StringLength field layout. - Data.Array arr = _target.ProcessedData.GetOrAdd(address); - size += (ulong)arr.NumComponents * componentSize; + uint numComponents = _target.Read(address.Value + (ulong)_target.GetTypeInfo(DataType.Array).Fields["m_NumComponents"].Offset); + size += (ulong)numComponents * componentSize; } return size; } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanContext.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanContext.cs index 3096661a2357e1..9c47dc61a588d9 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanContext.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanContext.cs @@ -3,6 +3,8 @@ using System; using System.Collections.Generic; +using Microsoft.Diagnostics.DataContractReader.Abstractions; +using Microsoft.Diagnostics.DataContractReader.Contracts.Extensions; namespace Microsoft.Diagnostics.DataContractReader.Contracts.StackWalkHelpers; @@ -10,6 +12,8 @@ internal class GcScanContext { private readonly Target _target; + private readonly LinearReadCache _cache; + private readonly IGC _gc; public bool ResolveInteriorPointers { get; } public List StackRefs { get; } = []; public TargetPointer StackPointer { get; private set; } @@ -25,6 +29,8 @@ public GcScanContext(Target target, bool resolveInteriorPointers) { _target = target; ResolveInteriorPointers = resolveInteriorPointers; + _cache = new LinearReadCache(target); + _gc = target.Contracts.GC; } public void UpdateScanContext(TargetPointer sp, TargetPointer ip, TargetPointer frame, StackRefData.SourceTypes? sourceTypeOverride = null) @@ -89,9 +95,10 @@ public void GCEnumCallback(TargetPointer pObject, GcScanFlags flags, GcScanSlotL if (flags.HasFlag(GcScanFlags.GC_CALL_INTERIOR) && ResolveInteriorPointers) { - // TODO(stackref): handle interior pointers - // https://github.com/dotnet/runtime/issues/125728 - throw new NotImplementedException(); + TargetPointer interiorObj = GetInteriorPointer(obj); + if (interiorObj == TargetPointer.Null) + return; + obj = interiorObj; } StackRefData data = new() @@ -111,17 +118,51 @@ public void GCEnumCallback(TargetPointer pObject, GcScanFlags flags, GcScanSlotL StackRefs.Add(data); } - public void GCReportCallback(TargetPointer ppObj, GcScanFlags flags) + private TargetPointer GetInteriorPointer(TargetPointer obj) { - if (flags.HasFlag(GcScanFlags.GC_CALL_INTERIOR) && ResolveInteriorPointers) + foreach ((GCHeapSegmentInfo seg, GCHeapData _) in _gc.EnumerateAllSegments()) { - // TODO(stackref): handle interior pointers - // https://github.com/dotnet/runtime/issues/125728 - throw new NotImplementedException(); + if (obj.Value < seg.Start.Value || obj.Value >= seg.End.Value) + continue; + + TargetPointer currentObj = _gc.GetPotentialNextObjectAddress(seg.Start, 0, seg); + while (currentObj.Value < obj.Value) + { + ulong size; + try + { + _target.ActivateCache(); + size = _target.Contracts.Object.GetSize(currentObj); + _target.DeactivateCache(); + } + catch + { + _target.DeactivateCache(); + return TargetPointer.Null; + } + + size = _gc.AlignObjectSize(size, seg.Generation); + if (currentObj.Value + size > seg.End.Value || size == 0) + { + return TargetPointer.Null; + } + obj = currentObj; + currentObj = _gc.GetPotentialNextObjectAddress(currentObj, size, seg); + } } + return obj; + } + public void GCReportCallback(TargetPointer ppObj, GcScanFlags flags) + { // Read the object pointer from the stack slot. TargetPointer obj = _target.ReadPointer(ppObj); + if (flags.HasFlag(GcScanFlags.GC_CALL_INTERIOR) && ResolveInteriorPointers) + { + TargetPointer interiorObj = GetInteriorPointer(obj); + if (interiorObj != TargetPointer.Null) + obj = interiorObj; + } StackRefData data = new() { diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/StackWalk_1.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/StackWalk_1.cs index 01f88bd338ca40..eac9c230bd1c69 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/StackWalk_1.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/StackWalk_1.cs @@ -164,7 +164,7 @@ IEnumerable IStackWalk.CreateStackWalk(ThreadData threadD } } - IReadOnlyList IStackWalk.WalkStackReferences(ThreadData threadData) + IReadOnlyList IStackWalk.WalkStackReferences(ThreadData threadData, bool resolveInteriorPointers) { // Initialize the walk data directly IPlatformAgnosticContext context = IPlatformAgnosticContext.GetContextForPlatform(_target); @@ -191,7 +191,7 @@ IReadOnlyList IStackWalk.WalkStackReferences(ThreadData thre if (walkData.State == StackWalkState.Frameless && CheckForSkippedFrames(walkData)) walkData.State = StackWalkState.SkippedFrame; - GcScanContext scanContext = new(_target, resolveInteriorPointers: false); + GcScanContext scanContext = new(_target, resolveInteriorPointers); // Filter drives Next() directly, matching native Filter()+NextRaw() integration. // This prevents funclet-to-parent transitions from re-visiting already-walked frames. diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/Helpers/HeapWalk.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/Helpers/HeapWalk.cs index 3c039ee0148fb4..5cdbf9e9f878c6 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/Helpers/HeapWalk.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/Helpers/HeapWalk.cs @@ -6,42 +6,33 @@ using System.Buffers.Binary; using System.Linq; using Microsoft.Diagnostics.DataContractReader.Contracts; +using Microsoft.Diagnostics.DataContractReader.Contracts.Extensions; namespace Microsoft.Diagnostics.DataContractReader.Legacy; internal sealed class HeapWalk : IEnum { private readonly IGC _gc; - private readonly IRuntimeTypeSystem _rts; + private readonly IObject _object; private readonly TargetPointer _freeObjectMT; - private readonly LinearReadCache _cache; - private readonly uint _numComponentsOffsetArray; - private readonly uint _numComponentsOffsetString; - private readonly uint _methodTableOffset; - private readonly ulong _methodTableMask; - + private readonly Target _target; public IEnumerator Enumerator { get; } public nuint LegacyHandle { get; set; } = 0; public HeapWalk(Target target) { _gc = target.Contracts.GC; - _rts = target.Contracts.RuntimeTypeSystem; - _freeObjectMT = _rts.GetWellKnownMethodTable(WellKnownMethodTable.Free); - _cache = new LinearReadCache(target); - // use these fields directly instead of through RuntimeTypeSystem so that we can use our cache that we really only need for heap walking - _numComponentsOffsetArray = (uint)target.GetTypeInfo(DataType.Array).Fields[Constants.FieldNames.Array.NumComponents].Offset; - _numComponentsOffsetString = (uint)target.GetTypeInfo(DataType.String).Fields["m_StringLength"].Offset; - _methodTableOffset = (uint)target.GetTypeInfo(DataType.Object).Fields["m_pMethTab"].Offset; - _methodTableMask = (ulong)~target.ReadGlobal(Constants.Globals.ObjectToMethodTableUnmask); - + _object = target.Contracts.Object; + _freeObjectMT = target.Contracts.RuntimeTypeSystem.GetWellKnownMethodTable(WellKnownMethodTable.Free); + _target = target; Enumerator = Walk().GetEnumerator(); } private IEnumerable Walk() { bool pendingFailure = false; - foreach ((GCHeapSegmentInfo seg, GCHeapData _) in EnumerateAllSegments()) + + foreach ((GCHeapSegmentInfo seg, GCHeapData _) in _gc.EnumerateAllSegments()) { if (seg.Start.Value >= seg.End.Value) continue; @@ -49,21 +40,36 @@ private IEnumerable Walk() TargetPointer currentObj = _gc.GetPotentialNextObjectAddress(seg.Start, 0, seg); while (currentObj.Value < seg.End.Value) { - if (!_cache.TryReadPointer(currentObj.Value + _methodTableOffset, out TargetPointer mt)) + TargetPointer mt; + ulong size; + try + { + _target.ActivateCache(); + mt = _object.GetMethodTableAddress(currentObj); + _target.DeactivateCache(); + } + catch { + _target.DeactivateCache(); pendingFailure = true; break; } - mt = new TargetPointer(mt.Value & _methodTableMask); - if (!TryGetObjectSize(currentObj, mt, out ulong size) || size == 0) + try { + _target.ActivateCache(); + size = _object.GetSize(currentObj); + _target.DeactivateCache(); + } + catch + { + _target.DeactivateCache(); pendingFailure = true; break; } size = _gc.AlignObjectSize(size, seg.Generation); - if (currentObj.Value + size > seg.End.Value) + if (currentObj.Value + size > seg.End.Value || size == 0) { pendingFailure = true; break; @@ -105,155 +111,4 @@ private IEnumerable Walk() if (pendingFailure) yield return default; } - - private IEnumerable<(GCHeapSegmentInfo Segment, GCHeapData Heap)> EnumerateAllSegments() - { - string[] gcIdentifiers = _gc.GetGCIdentifiers(); - bool isWorkstation = gcIdentifiers.Contains(GCIdentifiers.Workstation); - foreach (GCHeapData heap in EnumerateHeaps(_gc, isWorkstation)) - { - foreach (GCHeapSegmentInfo seg in _gc.EnumerateHeapSegments(heap)) - { - yield return (seg, heap); - } - } - } - - private bool TryGetObjectSize(TargetPointer objAddr, TargetPointer mt, out ulong size) - { - size = 0; - try - { - TypeHandle handle = _rts.GetTypeHandle(mt); - ulong baseSize = _rts.GetBaseSize(handle); - uint componentSize = _rts.GetComponentSize(handle); - uint numComponentsOffset = 0; - if (componentSize != 0) - { - if (_rts.IsArray(handle, out _) || _rts.IsFreeObjectMethodTable(handle)) - numComponentsOffset = _numComponentsOffsetArray; - else if (_rts.IsString(handle)) - numComponentsOffset = _numComponentsOffsetString; - else - return false; // unrecognized component type - if (!_cache.TryReadUInt32(objAddr.Value + numComponentsOffset, out uint numComponents)) - return false; - baseSize += (ulong)componentSize * numComponents; - } - size = baseSize; - return true; - } - catch - { - // The MT may be corrupt — surface as a read failure. - return false; - } - } - - private static IEnumerable EnumerateHeaps(IGC gc, bool isWorkstation) - { - if (isWorkstation) - { - yield return gc.GetHeapData(); - } - else - { - foreach (TargetPointer heapAddress in gc.GetGCHeaps()) - yield return gc.GetHeapData(heapAddress); - } - } - - // Linear page cache used by the per-object heap walk. - private sealed class LinearReadCache - { - // Typical page size - private const uint PageSize = 0x1000; - - private readonly Target _target; - private readonly byte[] _page = new byte[PageSize]; - private ulong _currPageStart; - private uint _currPageSize; - - public LinearReadCache(Target target) - { - _target = target; - } - - public bool TryReadPointer(ulong addr, out TargetPointer value) - { - Span buffer = stackalloc byte[sizeof(ulong)]; - buffer = buffer.Slice(0, _target.PointerSize); - if (!TryRead(addr, buffer)) - { - value = TargetPointer.Null; - return false; - } - value = _target.ReadPointerFromSpan(buffer); - return true; - } - - public bool TryReadUInt32(ulong addr, out uint value) - { - Span buffer = stackalloc byte[sizeof(uint)]; - if (!TryRead(addr, buffer)) - { - value = 0; - return false; - } - value = _target.IsLittleEndian - ? BinaryPrimitives.ReadUInt32LittleEndian(buffer) - : BinaryPrimitives.ReadUInt32BigEndian(buffer); - return true; - } - - private bool TryRead(ulong addr, Span dest) - { - // If the request misses the currently-cached page, try to load the page - // containing it. If that fails (e.g. the page is unmapped), or the request - // straddles the end of the cached page, fall back to a direct read. - if (addr < _currPageStart || addr - _currPageStart >= _currPageSize) - { - if (!MoveToPage(addr)) - return DirectRead(addr, dest); - } - - ulong offset = addr - _currPageStart; - if (offset + (ulong)dest.Length > _currPageSize) - return DirectRead(addr, dest); - - _page.AsSpan((int)offset, dest.Length).CopyTo(dest); - return true; - } - - private bool MoveToPage(ulong addr) - { - ulong pageStart = addr - (addr % PageSize); - try - { - _target.ReadBuffer(pageStart, _page.AsSpan(0, (int)PageSize)); - _currPageStart = pageStart; - _currPageSize = PageSize; - return true; - } - catch - { - _currPageStart = 0; - _currPageSize = 0; - return false; - } - } - - private bool DirectRead(ulong addr, Span dest) - { - try - { - _target.ReadBuffer(addr, dest); - return true; - } - catch - { - return false; - } - } - } } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/Helpers/RefWalk.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/Helpers/RefWalk.cs index 6d9813b77254c5..dfdf3ff5a47bb8 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/Helpers/RefWalk.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/Helpers/RefWalk.cs @@ -16,6 +16,7 @@ internal sealed class RefWalk : IEnum private readonly IGC _gc; private readonly bool _walkStacks; private readonly CorGCReferenceType _handleWalkMask; + private readonly TargetPointer _appDomain; public IEnumerator Enumerator { get; } public nuint LegacyHandle { get; set; } = 0; @@ -26,28 +27,26 @@ public RefWalk(Target target, bool walkStacks, CorGCReferenceType handleWalkMask _gc = target.Contracts.GC; _walkStacks = walkStacks; _handleWalkMask = handleWalkMask; + _appDomain = target.Contracts.Loader.GetAppDomain(); Enumerator = Walk().GetEnumerator(); } private IEnumerable Walk() { - // The single AppDomain pointer; used to fill vmDomain for both handle and stack references. - TargetPointer appDomain = _target.ReadPointer(_target.ReadGlobalPointer(Constants.Globals.AppDomain)); - if (_handleWalkMask != 0) { - foreach (DacGcReference reference in WalkHandles(appDomain)) + foreach (DacGcReference reference in WalkHandles()) yield return reference; } if (_walkStacks) { - foreach (DacGcReference reference in WalkStacks(appDomain)) + foreach (DacGcReference reference in WalkStacks()) yield return reference; } } - private IEnumerable WalkHandles(TargetPointer appDomain) + private IEnumerable WalkHandles() { HandleType[] requestedTypes = GetRequestedHandleTypes(); if (requestedTypes.Length == 0) @@ -59,7 +58,7 @@ private IEnumerable WalkHandles(TargetPointer appDomain) continue; yield return new DacGcReference { - vmDomain = appDomain.Value, + vmDomain = _appDomain.Value, objHnd = handle.Handle.Value, dwType = dwType, i64ExtraData = extraData, @@ -127,7 +126,7 @@ private bool TryMapHandle(HandleData handle, out CorGCReferenceType dwType, out } } - private IEnumerable WalkStacks(TargetPointer appDomain) + private IEnumerable WalkStacks() { IThread threadContract = _target.Contracts.Thread; IStackWalk stackWalkContract = _target.Contracts.StackWalk; @@ -138,7 +137,7 @@ private IEnumerable WalkStacks(TargetPointer appDomain) { ThreadData threadData = threadContract.GetThreadData(threadAddr); - foreach (StackReferenceData stackRef in stackWalkContract.WalkStackReferences(threadData)) + foreach (StackReferenceData stackRef in stackWalkContract.WalkStackReferences(threadData, true)) { // Skip cDAC-private deferred-frame markers; they are not real GC references. if ((stackRef.Flags & CDAC_DEFERRED_FRAME) != 0) @@ -146,7 +145,7 @@ private IEnumerable WalkStacks(TargetPointer appDomain) DacGcReference reference = new() { - vmDomain = appDomain.Value, + vmDomain = _appDomain.Value, dwType = CorGCReferenceType.CorReferenceStack, i64ExtraData = 0, }; 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 58c1f420ef70b0..5306017a19e671 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.cs @@ -4057,7 +4057,7 @@ int ISOSDacInterface.GetStackReferences(int osThreadID, DacComNullableByRef refs = stackWalkContract.WalkStackReferences(matchingThread.Value); + IReadOnlyList refs = stackWalkContract.WalkStackReferences(matchingThread.Value, false); SOSStackRefData[] sosRefs = new SOSStackRefData[refs.Count]; for (int i = 0; i < refs.Count; i++) diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader/ContractDescriptorTarget.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader/ContractDescriptorTarget.cs index 633484f17e2578..f3e75f039a2eb8 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader/ContractDescriptorTarget.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader/ContractDescriptorTarget.cs @@ -12,6 +12,7 @@ using Microsoft.Diagnostics.DataContractReader.Data; using Microsoft.Diagnostics.DataContractReader.Contracts; using System.Collections.Frozen; +using Microsoft.Diagnostics.DataContractReader.Abstractions; namespace Microsoft.Diagnostics.DataContractReader; @@ -27,6 +28,8 @@ namespace Microsoft.Diagnostics.DataContractReader; public sealed unsafe class ContractDescriptorTarget : Target { private const int StackAllocByteThreshold = 1024; + private bool _pageCacheActive; + private readonly LinearReadCache _linearReadCache; private readonly struct Configuration { @@ -135,6 +138,7 @@ private ContractDescriptorTarget(Descriptor mainDescriptor, DataTargetDelegates { Contracts = new CachingContractRegistry(this, this.TryGetContractVersion, contractRegistrations); ProcessedData = new DataCache(this); + _linearReadCache = new LinearReadCache(this); _config = mainDescriptor.Config; _dataTargetDelegates = dataTargetDelegates; @@ -146,6 +150,7 @@ private ContractDescriptorTarget(Descriptor mainDescriptor, DataTargetDelegates public override void Flush(FlushScope scope) { base.Flush(scope); + _linearReadCache.Flush(); BuildDescriptors(); } @@ -341,7 +346,7 @@ private static bool TryReadContractDescriptor( return false; // Flags - uint32_t - if (!TryRead(address, isLittleEndian, dataTargetDelegates, out uint flags)) + if (!TryReadStatic(address, isLittleEndian, dataTargetDelegates, out uint flags)) return false; address += sizeof(uint); @@ -352,19 +357,19 @@ private static bool TryReadContractDescriptor( Configuration config = new Configuration { IsLittleEndian = isLittleEndian, PointerSize = pointerSize }; // Descriptor size - uint32_t - if (!TryRead(address, config.IsLittleEndian, dataTargetDelegates, out uint descriptorSize)) + if (!TryReadStatic(address, config.IsLittleEndian, dataTargetDelegates, out uint descriptorSize)) return false; address += sizeof(uint); // Descriptor - char* - if (!TryReadPointer(address, config, dataTargetDelegates, out TargetPointer descriptorAddr)) + if (!TryReadPointerStatic(address, config, dataTargetDelegates, out TargetPointer descriptorAddr)) return false; address += (uint)pointerSize; // Pointer data count - uint32_t - if (!TryRead(address, config.IsLittleEndian, dataTargetDelegates, out uint pointerDataCount)) + if (!TryReadStatic(address, config.IsLittleEndian, dataTargetDelegates, out uint pointerDataCount)) return false; address += sizeof(uint); @@ -373,7 +378,7 @@ private static bool TryReadContractDescriptor( address += sizeof(uint); // Pointer data - uintptr_t* - if (!TryReadPointer(address, config, dataTargetDelegates, out TargetPointer pointerDataAddr)) + if (!TryReadPointerStatic(address, config, dataTargetDelegates, out TargetPointer pointerDataAddr)) return false; // Read descriptor @@ -391,7 +396,7 @@ private static bool TryReadContractDescriptor( TargetPointer[] pointerData = new TargetPointer[pointerDataCount]; for (int i = 0; i < pointerDataCount; i++) { - if (!TryReadPointer(pointerDataAddr.Value + (uint)(i * pointerSize), config, dataTargetDelegates, out pointerData[i])) + if (!TryReadPointerStatic(pointerDataAddr.Value + (uint)(i * pointerSize), config, dataTargetDelegates, out pointerData[i])) return false; } @@ -460,7 +465,28 @@ public override bool TryRead(ulong address, out T value) return true; } - private static bool TryRead(ulong address, bool isLittleEndian, DataTargetDelegates dataTargetDelegates, out T value) where T : unmanaged, IBinaryInteger, IMinMaxValue + private bool TryRead(ulong address, bool isLittleEndian, DataTargetDelegates dataTargetDelegates, out T value) where T : unmanaged, IBinaryInteger, IMinMaxValue + { + value = default; + Span buffer = stackalloc byte[sizeof(T)]; + if (_pageCacheActive) + { + // Use the linear read cache + if (!_linearReadCache.TryRead(address, buffer)) + return false; + } + else + { + if (dataTargetDelegates.ReadFromTarget(address, buffer) < 0) + return false; + } + + return isLittleEndian + ? T.TryReadLittleEndian(buffer, !IsSigned(), out value) + : T.TryReadBigEndian(buffer, !IsSigned(), out value); + } + + private static bool TryReadStatic(ulong address, bool isLittleEndian, DataTargetDelegates dataTargetDelegates, out T value) where T : unmanaged, IBinaryInteger, IMinMaxValue { value = default; Span buffer = stackalloc byte[sizeof(T)]; @@ -721,7 +747,7 @@ public override TargetNInt ReadNInt(ulong address) return new TargetNInt(value); } - private static bool TryReadPointer(ulong address, Configuration config, DataTargetDelegates dataTargetDelegates, out TargetPointer pointer) + private bool TryReadPointer(ulong address, Configuration config, DataTargetDelegates dataTargetDelegates, out TargetPointer pointer) { pointer = TargetPointer.Null; if (!TryReadNUInt(address, config, dataTargetDelegates, out ulong value)) @@ -731,7 +757,37 @@ private static bool TryReadPointer(ulong address, Configuration config, DataTarg return true; } - private static bool TryReadNUInt(ulong address, Configuration config, DataTargetDelegates dataTargetDelegates, out ulong value) + private static bool TryReadPointerStatic(ulong address, Configuration config, DataTargetDelegates dataTargetDelegates, out TargetPointer pointer) + { + pointer = TargetPointer.Null; + if (!TryReadNUIntStatic(address, config, dataTargetDelegates, out ulong value)) + return false; + + pointer = new TargetPointer(value); + return true; + } + + private static bool TryReadNUIntStatic(ulong address, Configuration config, DataTargetDelegates dataTargetDelegates, out ulong value) + { + value = 0; + if (config.PointerSize == sizeof(uint) + && TryReadStatic(address, config.IsLittleEndian, dataTargetDelegates, out uint value32)) + { + value = value32; + return true; + } + else if (config.PointerSize == sizeof(ulong) + && TryReadStatic(address, config.IsLittleEndian, dataTargetDelegates, out ulong value64)) + { + value = value64; + return true; + } + + return false; + } + + + private bool TryReadNUInt(ulong address, Configuration config, DataTargetDelegates dataTargetDelegates, out ulong value) { value = 0; if (config.PointerSize == sizeof(uint) @@ -750,7 +806,7 @@ private static bool TryReadNUInt(ulong address, Configuration config, DataTarget return false; } - private static bool TryReadNInt(ulong address, Configuration config, DataTargetDelegates dataTargetDelegates, out long value) + private bool TryReadNInt(ulong address, Configuration config, DataTargetDelegates dataTargetDelegates, out long value) { value = 0; if (config.PointerSize == sizeof(uint) @@ -955,4 +1011,7 @@ public int AllocVirtual(ulong size, out ulong allocatedAddress) return allocVirtual(size, out allocatedAddress); } } + + public override void ActivateCache() => _pageCacheActive = true; + public override void DeactivateCache() => _pageCacheActive = false; } diff --git a/src/native/managed/cdac/tests/DumpTests/DacDbi/DacDbiRefWalkDumpTests.cs b/src/native/managed/cdac/tests/DumpTests/DacDbi/DacDbiRefWalkDumpTests.cs index 0aa63e18b0c527..ce40b7438e574b 100644 --- a/src/native/managed/cdac/tests/DumpTests/DacDbi/DacDbiRefWalkDumpTests.cs +++ b/src/native/managed/cdac/tests/DumpTests/DacDbi/DacDbiRefWalkDumpTests.cs @@ -107,7 +107,7 @@ public unsafe void WalkRefs_StacksOnly_MatchStackReferenceWalk(TestConfiguration while (threadAddr != TargetPointer.Null) { ThreadData td = threadContract.GetThreadData(threadAddr); - expected += stackWalk.WalkStackReferences(td).Count(r => (r.Flags & CdacDeferredFrame) == 0); + expected += stackWalk.WalkStackReferences(td, true).Count(r => (r.Flags & CdacDeferredFrame) == 0); threadAddr = td.NextThread; } diff --git a/src/native/managed/cdac/tests/DumpTests/StackReferenceDumpTests.cs b/src/native/managed/cdac/tests/DumpTests/StackReferenceDumpTests.cs index 0a849fc577e514..d2a844e9de8d6e 100644 --- a/src/native/managed/cdac/tests/DumpTests/StackReferenceDumpTests.cs +++ b/src/native/managed/cdac/tests/DumpTests/StackReferenceDumpTests.cs @@ -29,7 +29,7 @@ public void WalkStackReferences_ReturnsWithoutThrowing(TestConfiguration config) ThreadData crashingThread = DumpTestHelpers.FindFailFastThread(Target); - IReadOnlyList refs = stackWalk.WalkStackReferences(crashingThread); + IReadOnlyList refs = stackWalk.WalkStackReferences(crashingThread, false); Assert.NotNull(refs); } @@ -43,7 +43,7 @@ public void WalkStackReferences_RefsHaveValidSourceInfo(TestConfiguration config ThreadData crashingThread = DumpTestHelpers.FindFailFastThread(Target); - IReadOnlyList refs = stackWalk.WalkStackReferences(crashingThread); + IReadOnlyList refs = stackWalk.WalkStackReferences(crashingThread, false); foreach (StackReferenceData r in refs) { Assert.True(r.Source != TargetPointer.Null, "Stack reference should have a non-null Source (IP or Frame address)"); @@ -64,7 +64,7 @@ public void GCRoots_WalkStackReferences_FindsRefs(TestConfiguration config) ThreadData crashingThread = DumpTestHelpers.FindThreadWithMethod(Target, "Main"); - IReadOnlyList refs = stackWalk.WalkStackReferences(crashingThread); + IReadOnlyList refs = stackWalk.WalkStackReferences(crashingThread, false); Assert.NotNull(refs); Assert.True(refs.Count > 0, "Expected GCRoots Main thread to have at least one stack reference (objects kept alive via GC.KeepAlive)"); @@ -81,7 +81,7 @@ public void GCRoots_RefsPointToValidObjects(TestConfiguration config) ThreadData crashingThread = DumpTestHelpers.FindThreadWithMethod(Target, "Main"); - IReadOnlyList refs = stackWalk.WalkStackReferences(crashingThread); + IReadOnlyList refs = stackWalk.WalkStackReferences(crashingThread, false); int validObjectCount = 0; foreach (StackReferenceData r in refs) @@ -143,7 +143,7 @@ public void NestedException_InFlightExceptionsReportedAsRoots(TestConfiguration // WalkStackReferences must surface every in-flight exception object as a stack reference, // reported with the Other source type (the ExInfo node is not a capital-F Frame). HashSet reported = new(); - foreach (StackReferenceData r in stackWalk.WalkStackReferences(crashingThread)) + foreach (StackReferenceData r in stackWalk.WalkStackReferences(crashingThread, false)) { if (r.Object == TargetPointer.Null) continue; @@ -174,7 +174,7 @@ public void GCProtect_GCFrameRootsAreReported(TestConfiguration config) // holding a GCPROTECT frame over the requesting Assembly reference. WalkStackReferences reports // each GCFrame-protected object with the GCFrame node address as its Source; the test walks the // thread's GCFrame chain and asserts a reported root's Source matches a node in that chain. - IReadOnlyList refs = stackWalk.WalkStackReferences(crashingThread); + IReadOnlyList refs = stackWalk.WalkStackReferences(crashingThread, false); // Enumerate the thread's GCFrame chain node addresses; each reported GCFrame root carries the // GCFrame node address as its Source (UpdateScanContext(frame: pGCFrame)). @@ -232,7 +232,7 @@ public void StackRefs_FindsMarkerString(TestConfiguration config) ThreadData crashingThread = DumpTestHelpers.FindThreadWithMethod(Target, "MethodWithStackRefs"); - IReadOnlyList refs = stackWalk.WalkStackReferences(crashingThread); + IReadOnlyList refs = stackWalk.WalkStackReferences(crashingThread, false); Assert.True(refs.Count > 0, "Expected at least one stack reference from MethodWithStackRefs"); bool foundMarker = false; @@ -272,7 +272,7 @@ public void StackRefs_FindsArrayReference(TestConfiguration config) ThreadData crashingThread = DumpTestHelpers.FindThreadWithMethod(Target, "MethodWithStackRefs"); - IReadOnlyList refs = stackWalk.WalkStackReferences(crashingThread); + IReadOnlyList refs = stackWalk.WalkStackReferences(crashingThread, false); Assert.True(refs.Count > 0, "Expected at least one stack reference from MethodWithStackRefs"); // Look for the int[] { 1, 2, 3, 4, 5 } array using the Object contract. @@ -322,7 +322,7 @@ public void PInvoke_WalkStackReferences_ReturnsWithoutThrowing(TestConfiguration ThreadData crashingThread = DumpTestHelpers.FindThreadWithMethod(Target, "Main"); - IReadOnlyList refs = stackWalk.WalkStackReferences(crashingThread); + IReadOnlyList refs = stackWalk.WalkStackReferences(crashingThread, false); Assert.NotNull(refs); } } diff --git a/src/native/managed/cdac/tests/TestInfrastructure/TestPlaceholderTarget.cs b/src/native/managed/cdac/tests/TestInfrastructure/TestPlaceholderTarget.cs index 12561ffd8676a7..4b68eae2b1eb9b 100644 --- a/src/native/managed/cdac/tests/TestInfrastructure/TestPlaceholderTarget.cs +++ b/src/native/managed/cdac/tests/TestInfrastructure/TestPlaceholderTarget.cs @@ -657,4 +657,6 @@ public override bool TryGetContract([NotNullWhen(true)] out TContract public override void Flush(FlushScope scope) { } } + public override void ActivateCache() => throw new NotImplementedException(); + public override void DeactivateCache() => throw new NotImplementedException(); } From d7085776c65270821d48ed56f80b696673b730a4 Mon Sep 17 00:00:00 2001 From: rcj1 Date: Thu, 18 Jun 2026 11:18:27 -0700 Subject: [PATCH 06/20] code review --- .../Target.cs | 6 +++ .../Dbi/DacDbiImpl.cs | 1 + .../DacDbi/DacDbiRefWalkDumpTests.cs | 37 +++++++++++++++---- 3 files changed, 37 insertions(+), 7 deletions(-) diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Target.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Target.cs index b1824ab8d41263..a604da8bec57e4 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Target.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Target.cs @@ -341,7 +341,13 @@ public virtual void Flush(FlushScope scope) Contracts.Flush(scope); } + /// + /// Activate a special-purpose cache for the target process + /// public abstract void ActivateCache(); + /// + /// Deactivate a special-purpose cache for the target process + /// public abstract void DeactivateCache(); } 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 8cbac0e96db732..dac035e5a3c007 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 @@ -3755,6 +3755,7 @@ public int CreateRefWalk(nuint* pHandle, Interop.BOOL walkStacks, CorGCReference { int hr = HResults.S_OK; RefWalk? walk = null; + *pHandle = 0; try { if (pHandle is null) diff --git a/src/native/managed/cdac/tests/DumpTests/DacDbi/DacDbiRefWalkDumpTests.cs b/src/native/managed/cdac/tests/DumpTests/DacDbi/DacDbiRefWalkDumpTests.cs index ce40b7438e574b..c05efc91fe18f5 100644 --- a/src/native/managed/cdac/tests/DumpTests/DacDbi/DacDbiRefWalkDumpTests.cs +++ b/src/native/managed/cdac/tests/DumpTests/DacDbi/DacDbiRefWalkDumpTests.cs @@ -62,27 +62,50 @@ private static unsafe List WalkAllRefs(DacDbiImpl dbi, bool walk return refs; } + /// + /// Cross product of with each handle type the + /// GCRoots debuggee allocates, along with its walk mask and the + /// the ref-walk is expected to report for that handle. + /// + public static IEnumerable HandleTypeConfigurations() + { + (HandleType HandleType, CorGCReferenceType Mask, CorGCReferenceType ExpectedType)[] cases = + [ + (HandleType.Strong, CorGCReferenceType.CorHandleStrong, CorGCReferenceType.CorHandleStrong), + (HandleType.Pinned, CorGCReferenceType.CorHandleStrongPinning, CorGCReferenceType.CorHandleStrongPinning), + (HandleType.WeakShort, CorGCReferenceType.CorHandleWeakShort, CorGCReferenceType.CorHandleWeakShort), + (HandleType.WeakLong, CorGCReferenceType.CorHandleWeakLong, CorGCReferenceType.CorHandleWeakLong), + (HandleType.Dependent, CorGCReferenceType.CorHandleStrongDependent, CorGCReferenceType.CorHandleStrongDependent), + ]; + + foreach (object[] config in TestConfigurations) + { + foreach ((HandleType handleType, CorGCReferenceType mask, CorGCReferenceType expectedType) in cases) + yield return [config[0], handleType, mask, expectedType]; + } + } + [ConditionalTheory] - [MemberData(nameof(TestConfigurations))] - public unsafe void WalkRefs_StrongHandles_MatchHandleTable(TestConfiguration config) + [MemberData(nameof(HandleTypeConfigurations))] + public unsafe void WalkRefs_Handles_MatchHandleTable(TestConfiguration config, HandleType handleType, CorGCReferenceType mask, CorGCReferenceType expectedType) { InitializeDumpTest(config); DacDbiImpl dbi = CreateDacDbi(); IGC gc = Target.Contracts.GC; - List refs = WalkAllRefs(dbi, walkStacks: false, handleWalkMask: CorGCReferenceType.CorHandleStrong); + List refs = WalkAllRefs(dbi, walkStacks: false, handleWalkMask: mask); - // Every reference must be a strong handle reported by its (low-bit-clear) handle address. + // Every reference must be a handle of the requested type reported by its (low-bit-clear) handle address. HashSet walkedHandles = new(); foreach (DacGcReference r in refs) { - Assert.Equal(CorGCReferenceType.CorHandleStrong, r.dwType); + Assert.Equal(expectedType, r.dwType); Assert.Equal(0ul, r.pObject & 1); walkedHandles.Add(r.pObject); } - HashSet expectedHandles = gc.GetHandles([HandleType.Strong]).Select(h => h.Handle.Value).ToHashSet(); - Assert.True(expectedHandles.Count > 0, "Expected at least one strong handle in GCRoots."); + HashSet expectedHandles = gc.GetHandles([handleType]).Select(h => h.Handle.Value).ToHashSet(); + Assert.True(expectedHandles.Count > 0, $"Expected at least one {handleType} handle in GCRoots."); Assert.Equal(expectedHandles, walkedHandles); } From a9308ab1d53223ecf9946b31ce5477522187e973 Mon Sep 17 00:00:00 2001 From: Rachel Jarvi Date: Thu, 18 Jun 2026 12:07:49 -0700 Subject: [PATCH 07/20] Update TestTarget.cs --- src/native/managed/cdac/tests/DataGenerator/TestTarget.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/native/managed/cdac/tests/DataGenerator/TestTarget.cs b/src/native/managed/cdac/tests/DataGenerator/TestTarget.cs index b823054d3b0220..062752e8b36b4c 100644 --- a/src/native/managed/cdac/tests/DataGenerator/TestTarget.cs +++ b/src/native/managed/cdac/tests/DataGenerator/TestTarget.cs @@ -256,6 +256,8 @@ public override bool TryReadGlobalPointer(string name, [NotNullWhen(true)] out T public override TargetPointer ReadPointerFromSpan(ReadOnlySpan bytes) => throw new NotImplementedException(); public override bool IsAlignedToPointerSize(TargetPointer pointer) => throw new NotImplementedException(); public override bool TryGetThreadContext(ulong threadId, uint contextFlags, Span buffer) => throw new NotImplementedException(); + public override void ActivateCache() => throw new NotImplementedException(); + public override void DeactivateCache() => throw new NotImplementedException(); // --- Stub ContractRegistry ------------------------------------- From 925d7b5733c3923b8a55e1ceeb6d2c3982549ac9 Mon Sep 17 00:00:00 2001 From: Rachel Jarvi Date: Thu, 18 Jun 2026 12:59:03 -0700 Subject: [PATCH 08/20] Apply suggestions from code review Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../LinearReadCache.cs | 4 +--- .../Contracts/StackWalk/GC/GcScanContext.cs | 2 -- .../cdac/tests/TestInfrastructure/TestPlaceholderTarget.cs | 4 ++-- 3 files changed, 3 insertions(+), 7 deletions(-) diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/LinearReadCache.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/LinearReadCache.cs index e6c75a29979f50..1cff0062746aa7 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/LinearReadCache.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/LinearReadCache.cs @@ -1,12 +1,10 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -// Linear page cache used by the per-object heap walk. using System; -using System.Buffers.Binary; using Microsoft.Diagnostics.DataContractReader; -namespace Microsoft.Diagnostics.DataContractReader.Abstractions; +namespace Microsoft.Diagnostics.DataContractReader.Abstractions; public sealed class LinearReadCache { // Typical page size diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanContext.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanContext.cs index 9c47dc61a588d9..3c08e36723700d 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanContext.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanContext.cs @@ -3,9 +3,7 @@ using System; using System.Collections.Generic; -using Microsoft.Diagnostics.DataContractReader.Abstractions; using Microsoft.Diagnostics.DataContractReader.Contracts.Extensions; - namespace Microsoft.Diagnostics.DataContractReader.Contracts.StackWalkHelpers; internal class GcScanContext diff --git a/src/native/managed/cdac/tests/TestInfrastructure/TestPlaceholderTarget.cs b/src/native/managed/cdac/tests/TestInfrastructure/TestPlaceholderTarget.cs index 4b68eae2b1eb9b..9248066271247f 100644 --- a/src/native/managed/cdac/tests/TestInfrastructure/TestPlaceholderTarget.cs +++ b/src/native/managed/cdac/tests/TestInfrastructure/TestPlaceholderTarget.cs @@ -657,6 +657,6 @@ public override bool TryGetContract([NotNullWhen(true)] out TContract public override void Flush(FlushScope scope) { } } - public override void ActivateCache() => throw new NotImplementedException(); - public override void DeactivateCache() => throw new NotImplementedException(); + public override void ActivateCache() { } + public override void DeactivateCache() { } } From 729ff40d8cc7bf8ef331a6285bb477ff2fca14c5 Mon Sep 17 00:00:00 2001 From: Rachel Jarvi Date: Thu, 18 Jun 2026 13:07:43 -0700 Subject: [PATCH 09/20] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../Dbi/DacDbiImpl.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 862b9a796edc29..277e5ed74503b6 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 @@ -4139,11 +4139,12 @@ public int CreateRefWalk(nuint* pHandle, Interop.BOOL walkStacks, CorGCReference { int hr = HResults.S_OK; RefWalk? walk = null; - *pHandle = 0; try { if (pHandle is null) throw new NullReferenceException(nameof(pHandle)); + + *pHandle = 0; walk = new RefWalk(_target, walkStacks != Interop.BOOL.FALSE, handleWalkMask); *pHandle = (nuint)((IEnum)walk).GetHandle(); } From eb7df0879a313046f6d7280695e6905ba2248654 Mon Sep 17 00:00:00 2001 From: rcj1 Date: Thu, 18 Jun 2026 13:09:45 -0700 Subject: [PATCH 10/20] code revierw --- .../Contracts/StackWalk/GC/GcScanContext.cs | 7 +++---- .../Dbi/Helpers/HeapWalk.cs | 1 - 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanContext.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanContext.cs index 3c08e36723700d..3d835266ffacca 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanContext.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanContext.cs @@ -10,7 +10,6 @@ internal class GcScanContext { private readonly Target _target; - private readonly LinearReadCache _cache; private readonly IGC _gc; public bool ResolveInteriorPointers { get; } public List StackRefs { get; } = []; @@ -27,7 +26,6 @@ public GcScanContext(Target target, bool resolveInteriorPointers) { _target = target; ResolveInteriorPointers = resolveInteriorPointers; - _cache = new LinearReadCache(target); _gc = target.Contracts.GC; } @@ -118,6 +116,7 @@ public void GCEnumCallback(TargetPointer pObject, GcScanFlags flags, GcScanSlotL private TargetPointer GetInteriorPointer(TargetPointer obj) { + TargetPointer outerObj = TargetPointer.Null; foreach ((GCHeapSegmentInfo seg, GCHeapData _) in _gc.EnumerateAllSegments()) { if (obj.Value < seg.Start.Value || obj.Value >= seg.End.Value) @@ -144,11 +143,11 @@ private TargetPointer GetInteriorPointer(TargetPointer obj) { return TargetPointer.Null; } - obj = currentObj; + outerObj = currentObj; currentObj = _gc.GetPotentialNextObjectAddress(currentObj, size, seg); } } - return obj; + return outerObj; } public void GCReportCallback(TargetPointer ppObj, GcScanFlags flags) diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/Helpers/HeapWalk.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/Helpers/HeapWalk.cs index 5cdbf9e9f878c6..a12f4e6a2ef892 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/Helpers/HeapWalk.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/Helpers/HeapWalk.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; -using System.Buffers.Binary; using System.Linq; using Microsoft.Diagnostics.DataContractReader.Contracts; using Microsoft.Diagnostics.DataContractReader.Contracts.Extensions; From 1e87302000b2d6440021b86830974d7692b7cf69 Mon Sep 17 00:00:00 2001 From: rcj1 Date: Thu, 18 Jun 2026 14:56:39 -0700 Subject: [PATCH 11/20] move linear read cache and create scope --- .../LinearReadCache.cs | 93 ---------------- .../Target.cs | 10 +- .../Contracts/StackWalk/GC/GcScanContext.cs | 9 +- .../Dbi/Helpers/HeapWalk.cs | 12 +- .../ContractDescriptorTarget.cs | 104 +++++++++++++++++- .../cdac/tests/DataGenerator/TestTarget.cs | 3 +- .../TestPlaceholderTarget.cs | 9 +- 7 files changed, 121 insertions(+), 119 deletions(-) delete mode 100644 src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/LinearReadCache.cs diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/LinearReadCache.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/LinearReadCache.cs deleted file mode 100644 index 1cff0062746aa7..00000000000000 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/LinearReadCache.cs +++ /dev/null @@ -1,93 +0,0 @@ -// 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 Microsoft.Diagnostics.DataContractReader; - -namespace Microsoft.Diagnostics.DataContractReader.Abstractions; -public sealed class LinearReadCache -{ - // Typical page size - private const uint PageSize = 0x1000; - - private readonly Target _target; - private readonly byte[] _page = new byte[PageSize]; - private ulong _currPageStart; - private uint _currPageSize; - - public LinearReadCache(Target target) - { - _target = target; - } - - public bool TryReadPointer(ulong addr, out TargetPointer value) - { - Span buffer = stackalloc byte[sizeof(ulong)]; - buffer = buffer.Slice(0, _target.PointerSize); - if (!TryRead(addr, buffer)) - { - value = TargetPointer.Null; - return false; - } - value = _target.ReadPointerFromSpan(buffer); - return true; - } - - public bool TryRead(ulong addr, Span dest) - { - // If the request misses the currently-cached page, try to load the page - // containing it. If that fails (e.g. the page is unmapped), or the request - // straddles the end of the cached page, fall back to a direct read. - if (addr < _currPageStart || addr - _currPageStart >= _currPageSize) - { - if (!MoveToPage(addr)) - return DirectRead(addr, dest); - } - - ulong offset = addr - _currPageStart; - if (offset + (ulong)dest.Length > _currPageSize) - return DirectRead(addr, dest); - - _page.AsSpan((int)offset, dest.Length).CopyTo(dest); - return true; - } - - public void Flush() - { - // Clear the current page cache - _currPageStart = 0; - _currPageSize = 0; - Array.Clear(_page, 0, _page.Length); - } - - private bool MoveToPage(ulong addr) - { - ulong pageStart = addr - (addr % PageSize); - try - { - _target.ReadBuffer(pageStart, _page.AsSpan(0, (int)PageSize)); - _currPageStart = pageStart; - _currPageSize = PageSize; - return true; - } - catch - { - _currPageStart = 0; - _currPageSize = 0; - return false; - } - } - - private bool DirectRead(ulong addr, Span dest) - { - try - { - _target.ReadBuffer(addr, dest); - return true; - } - catch - { - return false; - } - } -} diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Target.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Target.cs index a604da8bec57e4..a9982d8c0868e0 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Target.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Target.cs @@ -342,12 +342,8 @@ public virtual void Flush(FlushScope scope) } /// - /// Activate a special-purpose cache for the target process + /// Begins a scope where reads via this Target are routed through a special-purpose cache. + /// Reads outside the returned scope bypass the cache. Scopes must not be nested on the same Target. /// - public abstract void ActivateCache(); - - /// - /// Deactivate a special-purpose cache for the target process - /// - public abstract void DeactivateCache(); + public abstract IDisposable BeginReadCache(); } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanContext.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanContext.cs index 3d835266ffacca..d9f034592a8a2c 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanContext.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanContext.cs @@ -123,18 +123,16 @@ private TargetPointer GetInteriorPointer(TargetPointer obj) continue; TargetPointer currentObj = _gc.GetPotentialNextObjectAddress(seg.Start, 0, seg); - while (currentObj.Value < obj.Value) + while (currentObj.Value <= obj.Value) { ulong size; try { - _target.ActivateCache(); - size = _target.Contracts.Object.GetSize(currentObj); - _target.DeactivateCache(); + using (_target.BeginReadCache()) + size = _target.Contracts.Object.GetSize(currentObj); } catch { - _target.DeactivateCache(); return TargetPointer.Null; } @@ -146,6 +144,7 @@ private TargetPointer GetInteriorPointer(TargetPointer obj) outerObj = currentObj; currentObj = _gc.GetPotentialNextObjectAddress(currentObj, size, seg); } + return outerObj; } return outerObj; } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/Helpers/HeapWalk.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/Helpers/HeapWalk.cs index a12f4e6a2ef892..43c25b85c1ee12 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/Helpers/HeapWalk.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/Helpers/HeapWalk.cs @@ -43,26 +43,22 @@ private IEnumerable Walk() ulong size; try { - _target.ActivateCache(); - mt = _object.GetMethodTableAddress(currentObj); - _target.DeactivateCache(); + using (_target.BeginReadCache()) + mt = _object.GetMethodTableAddress(currentObj); } catch { - _target.DeactivateCache(); pendingFailure = true; break; } try { - _target.ActivateCache(); - size = _object.GetSize(currentObj); - _target.DeactivateCache(); + using (_target.BeginReadCache()) + size = _object.GetSize(currentObj); } catch { - _target.DeactivateCache(); pendingFailure = true; break; } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader/ContractDescriptorTarget.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader/ContractDescriptorTarget.cs index f3e75f039a2eb8..f945363ae85060 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader/ContractDescriptorTarget.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader/ContractDescriptorTarget.cs @@ -30,6 +30,7 @@ public sealed unsafe class ContractDescriptorTarget : Target private const int StackAllocByteThreshold = 1024; private bool _pageCacheActive; private readonly LinearReadCache _linearReadCache; + private readonly LinearReadCacheScope _linearReadCacheScope; private readonly struct Configuration { @@ -139,6 +140,7 @@ private ContractDescriptorTarget(Descriptor mainDescriptor, DataTargetDelegates Contracts = new CachingContractRegistry(this, this.TryGetContractVersion, contractRegistrations); ProcessedData = new DataCache(this); _linearReadCache = new LinearReadCache(this); + _linearReadCacheScope = new LinearReadCacheScope(this); _config = mainDescriptor.Config; _dataTargetDelegates = dataTargetDelegates; @@ -1012,6 +1014,104 @@ public int AllocVirtual(ulong size, out ulong allocatedAddress) } } - public override void ActivateCache() => _pageCacheActive = true; - public override void DeactivateCache() => _pageCacheActive = false; + private sealed class LinearReadCache + { + // Typical page size + private const uint PageSize = 0x1000; + + private readonly Target _target; + private readonly byte[] _page = new byte[PageSize]; + private ulong _currPageStart; + private uint _currPageSize; + + public LinearReadCache(Target target) + { + _target = target; + } + + public bool TryReadPointer(ulong addr, out TargetPointer value) + { + Span buffer = stackalloc byte[sizeof(ulong)]; + buffer = buffer.Slice(0, _target.PointerSize); + if (!TryRead(addr, buffer)) + { + value = TargetPointer.Null; + return false; + } + value = _target.ReadPointerFromSpan(buffer); + return true; + } + + public bool TryRead(ulong addr, Span dest) + { + // If the request misses the currently-cached page, try to load the page + // containing it. If that fails (e.g. the page is unmapped), or the request + // straddles the end of the cached page, fall back to a direct read. + if (addr < _currPageStart || addr - _currPageStart >= _currPageSize) + { + if (!MoveToPage(addr)) + return DirectRead(addr, dest); + } + + ulong offset = addr - _currPageStart; + if (offset + (ulong)dest.Length > _currPageSize) + return DirectRead(addr, dest); + + _page.AsSpan((int)offset, dest.Length).CopyTo(dest); + return true; + } + + public void Flush() + { + // Clear the current page cache + _currPageStart = 0; + _currPageSize = 0; + Array.Clear(_page, 0, _page.Length); + } + + private bool MoveToPage(ulong addr) + { + ulong pageStart = addr - (addr % PageSize); + try + { + _target.ReadBuffer(pageStart, _page.AsSpan(0, (int)PageSize)); + _currPageStart = pageStart; + _currPageSize = PageSize; + return true; + } + catch + { + _currPageStart = 0; + _currPageSize = 0; + return false; + } + } + + private bool DirectRead(ulong addr, Span dest) + { + try + { + _target.ReadBuffer(addr, dest); + return true; + } + catch + { + return false; + } + } + } + + public override IDisposable BeginReadCache() + { + Debug.Assert(!_pageCacheActive, "Linear read cache scopes must not be nested on the same Target."); + _pageCacheActive = true; + + // The scope object is reused: nesting is disallowed, so at most one scope is ever active. + return _linearReadCacheScope; + } + + private sealed class LinearReadCacheScope(ContractDescriptorTarget target) : IDisposable + { + public void Dispose() => target._pageCacheActive = false; + } } diff --git a/src/native/managed/cdac/tests/DataGenerator/TestTarget.cs b/src/native/managed/cdac/tests/DataGenerator/TestTarget.cs index 062752e8b36b4c..506ef6b0a7d05b 100644 --- a/src/native/managed/cdac/tests/DataGenerator/TestTarget.cs +++ b/src/native/managed/cdac/tests/DataGenerator/TestTarget.cs @@ -256,8 +256,7 @@ public override bool TryReadGlobalPointer(string name, [NotNullWhen(true)] out T public override TargetPointer ReadPointerFromSpan(ReadOnlySpan bytes) => throw new NotImplementedException(); public override bool IsAlignedToPointerSize(TargetPointer pointer) => throw new NotImplementedException(); public override bool TryGetThreadContext(ulong threadId, uint contextFlags, Span buffer) => throw new NotImplementedException(); - public override void ActivateCache() => throw new NotImplementedException(); - public override void DeactivateCache() => throw new NotImplementedException(); + public override IDisposable BeginReadCache() => throw new NotImplementedException(); // --- Stub ContractRegistry ------------------------------------- diff --git a/src/native/managed/cdac/tests/TestInfrastructure/TestPlaceholderTarget.cs b/src/native/managed/cdac/tests/TestInfrastructure/TestPlaceholderTarget.cs index 9248066271247f..760304cdeb4a5c 100644 --- a/src/native/managed/cdac/tests/TestInfrastructure/TestPlaceholderTarget.cs +++ b/src/native/managed/cdac/tests/TestInfrastructure/TestPlaceholderTarget.cs @@ -657,6 +657,11 @@ public override bool TryGetContract([NotNullWhen(true)] out TContract public override void Flush(FlushScope scope) { } } - public override void ActivateCache() { } - public override void DeactivateCache() { } + public override IDisposable BeginReadCache() => NoOpScope.Instance; + + private sealed class NoOpScope : IDisposable + { + public static readonly NoOpScope Instance = new(); + public void Dispose() { } + } } From cd21fa2711682b9d1e0dbab7f0b0bd8b4c678978 Mon Sep 17 00:00:00 2001 From: rcj1 Date: Thu, 18 Jun 2026 15:08:41 -0700 Subject: [PATCH 12/20] fix buidl break --- .../ContractDescriptorTarget.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader/ContractDescriptorTarget.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader/ContractDescriptorTarget.cs index f945363ae85060..55440ceba276aa 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader/ContractDescriptorTarget.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader/ContractDescriptorTarget.cs @@ -12,7 +12,6 @@ using Microsoft.Diagnostics.DataContractReader.Data; using Microsoft.Diagnostics.DataContractReader.Contracts; using System.Collections.Frozen; -using Microsoft.Diagnostics.DataContractReader.Abstractions; namespace Microsoft.Diagnostics.DataContractReader; From 5e1f797bd1ac04047f30269c54527dde0ff915bd Mon Sep 17 00:00:00 2001 From: rcj1 Date: Fri, 19 Jun 2026 11:19:03 -0700 Subject: [PATCH 13/20] cpde review --- .../ITargetReadCache.cs | 11 ++ .../Target.cs | 29 +++- .../Contracts/Object_1.cs | 3 + .../Contracts/StackWalk/GC/GcScanContext.cs | 4 +- .../LinearReadCache.cs | 83 ++++++++++++ .../Dbi/DacDbiImpl.cs | 8 +- .../Dbi/Helpers/HeapWalk.cs | 7 +- .../DebugExtensions.cs | 6 + .../ContractDescriptorTarget.cs | 126 +++--------------- .../cdac/tests/DataGenerator/TestTarget.cs | 1 - .../TestPlaceholderTarget.cs | 8 -- 11 files changed, 161 insertions(+), 125 deletions(-) create mode 100644 src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/ITargetReadCache.cs create mode 100644 src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/LinearReadCache.cs diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/ITargetReadCache.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/ITargetReadCache.cs new file mode 100644 index 00000000000000..2502caf5cf0948 --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/ITargetReadCache.cs @@ -0,0 +1,11 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; + +namespace Microsoft.Diagnostics.DataContractReader; +public interface ITargetReadCache : IDisposable +{ + bool TryRead(ulong address, Span destination); + void Invalidate(); +} diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Target.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Target.cs index a9982d8c0868e0..66302b293169e7 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Target.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Target.cs @@ -342,8 +342,31 @@ public virtual void Flush(FlushScope scope) } /// - /// Begins a scope where reads via this Target are routed through a special-purpose cache. - /// Reads outside the returned scope bypass the cache. Scopes must not be nested on the same Target. + /// Begin a scope during which reads from this target may be served by . + /// Returns a handle that ends the scope when disposed. Callers should typically wrap the + /// scope in a using block. /// - public abstract IDisposable BeginReadCache(); + /// The caching strategy to install for the duration of the scope. + /// + /// Only one cache scope may be active per target at a time; opening a second scope while one + /// is still active throws . + /// + public virtual IDisposable BeginCacheScope(ITargetReadCache cache) + { + ArgumentNullException.ThrowIfNull(cache); + return new DefaultCacheScope(cache); + } + + private sealed class DefaultCacheScope(ITargetReadCache cache) : IDisposable + { + private bool _disposed; + public void Dispose() + { + if (_disposed) + return; + _disposed = true; + cache.Invalidate(); + cache.Dispose(); + } + } } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/Object_1.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/Object_1.cs index ff81dd148477f7..641792e3801948 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/Object_1.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/Object_1.cs @@ -191,6 +191,9 @@ public DelegateInfo GetDelegateInfo(TargetPointer address) public ulong GetSize(TargetPointer address) { + // NOTE NOTE NOTE NOTE NOTE + // This method is called on every managed object in the debuggee during a heap walk. + // Do NOT allocate managed objects here in a way that scales with the number of heap objects. TargetPointer mt = GetMethodTableAddress(address); if (mt == TargetPointer.Null) throw new ArgumentException("Address represents a free object"); diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanContext.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanContext.cs index d9f034592a8a2c..3e8d5bb34150a6 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanContext.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanContext.cs @@ -122,14 +122,14 @@ private TargetPointer GetInteriorPointer(TargetPointer obj) if (obj.Value < seg.Start.Value || obj.Value >= seg.End.Value) continue; + using IDisposable _ = _target.BeginCacheScope(new LinearReadCache(_target)); TargetPointer currentObj = _gc.GetPotentialNextObjectAddress(seg.Start, 0, seg); while (currentObj.Value <= obj.Value) { ulong size; try { - using (_target.BeginReadCache()) - size = _target.Contracts.Object.GetSize(currentObj); + size = _target.Contracts.Object.GetSize(currentObj); } catch { diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/LinearReadCache.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/LinearReadCache.cs new file mode 100644 index 00000000000000..c389e51c5ae693 --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/LinearReadCache.cs @@ -0,0 +1,83 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +namespace Microsoft.Diagnostics.DataContractReader; +public sealed class LinearReadCache : ITargetReadCache, IDisposable +{ + // Typical page size + private const uint PageSize = 0x1000; + + private readonly Target _target; + private readonly byte[] _page = new byte[PageSize]; + private ulong _currPageStart; + private uint _currPageSize; + + public LinearReadCache(Target target) + { + _target = target; + } + + public bool TryRead(ulong addr, Span dest) + { + // If the request misses the currently-cached page, try to load the page + // containing it. If that fails (e.g. the page is unmapped), or the request + // straddles the end of the cached page, fall back to a direct read. + if (addr < _currPageStart || addr - _currPageStart >= _currPageSize) + { + if (!MoveToPage(addr)) + return DirectRead(addr, dest); + } + + ulong offset = addr - _currPageStart; + if (offset + (ulong)dest.Length > _currPageSize) + return DirectRead(addr, dest); + + _page.AsSpan((int)offset, dest.Length).CopyTo(dest); + return true; + } + + public void Invalidate() + { + // Clear the current page cache + _currPageStart = 0; + _currPageSize = 0; + Array.Clear(_page, 0, _page.Length); + } + + public void Dispose() + { + // Nothing to dispose + } + + private bool MoveToPage(ulong addr) + { + ulong pageStart = addr - (addr % PageSize); + try + { + _target.ReadBuffer(pageStart, _page.AsSpan(0, (int)PageSize)); + _currPageStart = pageStart; + _currPageSize = PageSize; + return true; + } + catch + { + _currPageStart = 0; + _currPageSize = 0; + return false; + } + } + + private bool DirectRead(ulong addr, Span dest) + { + try + { + _target.ReadBuffer(addr, dest); + return true; + } + catch + { + return false; + } + } +} 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 277e5ed74503b6..f2300e45282dd6 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 @@ -3942,7 +3942,10 @@ public int WalkHeap(nuint handle, uint count, COR_HEAPOBJECT* objects, uint* fet { hrLocal = _legacy.WalkHeap(walk.LegacyHandle, count, objectsLocalPtr, &fetchedLocal); } - Debug.ValidateHResult(hr, hrLocal); + // The number of reported stack refs is not guaranteed to match between cDAC and legacy DAC. + // If this is the case, the cDAC may report S_FALSE while the legacy DAC reports S_OK, or vice versa. + // Allow divergent success codes, but still validate the rest of the results. + Debug.ValidateHResult(hr, hrLocal, HResultValidationMode.AllowDivergentSuccess); if (hr >= HResults.S_OK) { Debug.Assert(*fetched == fetchedLocal, @@ -4243,8 +4246,7 @@ public int WalkRefs(nuint handle, uint count, [In, MarshalUsing(CountElementName DacGcReference[] legacyRefs = new DacGcReference[(int)count]; uint legacyFetched = 0; int hrLocal = _legacy.WalkRefs(walk.LegacyHandle, count, legacyRefs, &legacyFetched); - Debug.ValidateHResult(hr, hrLocal); - + // Debug.ValidateHResult(hr, hrLocal); uint cdacHandlePrefix = CountHandlePrefix(refs, i); uint legacyHandlePrefix = CountHandlePrefix(legacyRefs, legacyFetched); Debug.Assert( diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/Helpers/HeapWalk.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/Helpers/HeapWalk.cs index 43c25b85c1ee12..a38e83cde37ffd 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/Helpers/HeapWalk.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/Helpers/HeapWalk.cs @@ -33,6 +33,7 @@ private IEnumerable Walk() foreach ((GCHeapSegmentInfo seg, GCHeapData _) in _gc.EnumerateAllSegments()) { + using IDisposable _ = _target.BeginCacheScope(new LinearReadCache(_target)); if (seg.Start.Value >= seg.End.Value) continue; @@ -43,8 +44,7 @@ private IEnumerable Walk() ulong size; try { - using (_target.BeginReadCache()) - mt = _object.GetMethodTableAddress(currentObj); + mt = _object.GetMethodTableAddress(currentObj); } catch { @@ -54,8 +54,7 @@ private IEnumerable Walk() try { - using (_target.BeginReadCache()) - size = _object.GetSize(currentObj); + size = _object.GetSize(currentObj); } catch { diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/DebugExtensions.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/DebugExtensions.cs index 3b1600e5863b84..1ea57467a4bdeb 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/DebugExtensions.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/DebugExtensions.cs @@ -22,6 +22,11 @@ internal enum HResultValidationMode /// AllowDivergentFailures, + /// + /// Allows divergent success HRESULTs, but failing HRESULTs must match exactly. + /// + AllowDivergentSuccess, + /// /// Like , but also allows the cDAC to succeed when the native DAC fails. /// The native DAC's MetaSig constructor traverses MethodDesc -> Module -> MDImport -> signature blob via @@ -95,6 +100,7 @@ internal static void ValidateHResult( { HResultValidationMode.Exact => cdacHr == dacHr, HResultValidationMode.AllowDivergentFailures => cdacHr == dacHr || (cdacHr < 0 && dacHr < 0), + HResultValidationMode.AllowDivergentSuccess => cdacHr == dacHr || (cdacHr >= 0 && dacHr >= 0), HResultValidationMode.AllowCdacSuccess => cdacHr == dacHr || (cdacHr < 0 && dacHr < 0) || (cdacHr >= 0 && dacHr < 0), _ => cdacHr == dacHr, }; diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader/ContractDescriptorTarget.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader/ContractDescriptorTarget.cs index 55440ceba276aa..e02f67cfa5cb8f 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader/ContractDescriptorTarget.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader/ContractDescriptorTarget.cs @@ -27,9 +27,7 @@ namespace Microsoft.Diagnostics.DataContractReader; public sealed unsafe class ContractDescriptorTarget : Target { private const int StackAllocByteThreshold = 1024; - private bool _pageCacheActive; - private readonly LinearReadCache _linearReadCache; - private readonly LinearReadCacheScope _linearReadCacheScope; + private ITargetReadCache? _activeCache; private readonly struct Configuration { @@ -138,8 +136,6 @@ private ContractDescriptorTarget(Descriptor mainDescriptor, DataTargetDelegates { Contracts = new CachingContractRegistry(this, this.TryGetContractVersion, contractRegistrations); ProcessedData = new DataCache(this); - _linearReadCache = new LinearReadCache(this); - _linearReadCacheScope = new LinearReadCacheScope(this); _config = mainDescriptor.Config; _dataTargetDelegates = dataTargetDelegates; @@ -151,8 +147,7 @@ private ContractDescriptorTarget(Descriptor mainDescriptor, DataTargetDelegates public override void Flush(FlushScope scope) { base.Flush(scope); - _linearReadCache.Flush(); - + _activeCache?.Invalidate(); BuildDescriptors(); } @@ -470,10 +465,10 @@ private bool TryRead(ulong address, bool isLittleEndian, DataTargetDelegates { value = default; Span buffer = stackalloc byte[sizeof(T)]; - if (_pageCacheActive) + if (_activeCache is not null) { // Use the linear read cache - if (!_linearReadCache.TryRead(address, buffer)) + if (!_activeCache.TryRead(address, buffer)) return false; } else @@ -1013,104 +1008,27 @@ public int AllocVirtual(ulong size, out ulong allocatedAddress) } } - private sealed class LinearReadCache - { - // Typical page size - private const uint PageSize = 0x1000; - - private readonly Target _target; - private readonly byte[] _page = new byte[PageSize]; - private ulong _currPageStart; - private uint _currPageSize; - - public LinearReadCache(Target target) - { - _target = target; - } - - public bool TryReadPointer(ulong addr, out TargetPointer value) - { - Span buffer = stackalloc byte[sizeof(ulong)]; - buffer = buffer.Slice(0, _target.PointerSize); - if (!TryRead(addr, buffer)) - { - value = TargetPointer.Null; - return false; - } - value = _target.ReadPointerFromSpan(buffer); - return true; - } - - public bool TryRead(ulong addr, Span dest) - { - // If the request misses the currently-cached page, try to load the page - // containing it. If that fails (e.g. the page is unmapped), or the request - // straddles the end of the cached page, fall back to a direct read. - if (addr < _currPageStart || addr - _currPageStart >= _currPageSize) - { - if (!MoveToPage(addr)) - return DirectRead(addr, dest); - } - - ulong offset = addr - _currPageStart; - if (offset + (ulong)dest.Length > _currPageSize) - return DirectRead(addr, dest); - - _page.AsSpan((int)offset, dest.Length).CopyTo(dest); - return true; - } - - public void Flush() - { - // Clear the current page cache - _currPageStart = 0; - _currPageSize = 0; - Array.Clear(_page, 0, _page.Length); - } - - private bool MoveToPage(ulong addr) - { - ulong pageStart = addr - (addr % PageSize); - try - { - _target.ReadBuffer(pageStart, _page.AsSpan(0, (int)PageSize)); - _currPageStart = pageStart; - _currPageSize = PageSize; - return true; - } - catch - { - _currPageStart = 0; - _currPageSize = 0; - return false; - } - } - - private bool DirectRead(ulong addr, Span dest) - { - try - { - _target.ReadBuffer(addr, dest); - return true; - } - catch - { - return false; - } - } - } - - public override IDisposable BeginReadCache() + public override IDisposable BeginCacheScope(ITargetReadCache cache) { - Debug.Assert(!_pageCacheActive, "Linear read cache scopes must not be nested on the same Target."); - _pageCacheActive = true; - - // The scope object is reused: nesting is disallowed, so at most one scope is ever active. - return _linearReadCacheScope; + ArgumentNullException.ThrowIfNull(cache); + if (_activeCache is not null) + throw new InvalidOperationException("Nested cache scopes are not supported."); + _activeCache = cache; + return new CacheScope(this, cache); } - private sealed class LinearReadCacheScope(ContractDescriptorTarget target) : IDisposable + private sealed class CacheScope(ContractDescriptorTarget target, ITargetReadCache cache) : IDisposable { - public void Dispose() => target._pageCacheActive = false; + private bool _disposed; + public void Dispose() + { + if (_disposed) + return; + _disposed = true; + Debug.Assert(ReferenceEquals(target._activeCache, cache), "Cache scope disposed out of order"); + target._activeCache = null; + cache.Invalidate(); + cache.Dispose(); + } } } diff --git a/src/native/managed/cdac/tests/DataGenerator/TestTarget.cs b/src/native/managed/cdac/tests/DataGenerator/TestTarget.cs index 506ef6b0a7d05b..b823054d3b0220 100644 --- a/src/native/managed/cdac/tests/DataGenerator/TestTarget.cs +++ b/src/native/managed/cdac/tests/DataGenerator/TestTarget.cs @@ -256,7 +256,6 @@ public override bool TryReadGlobalPointer(string name, [NotNullWhen(true)] out T public override TargetPointer ReadPointerFromSpan(ReadOnlySpan bytes) => throw new NotImplementedException(); public override bool IsAlignedToPointerSize(TargetPointer pointer) => throw new NotImplementedException(); public override bool TryGetThreadContext(ulong threadId, uint contextFlags, Span buffer) => throw new NotImplementedException(); - public override IDisposable BeginReadCache() => throw new NotImplementedException(); // --- Stub ContractRegistry ------------------------------------- diff --git a/src/native/managed/cdac/tests/TestInfrastructure/TestPlaceholderTarget.cs b/src/native/managed/cdac/tests/TestInfrastructure/TestPlaceholderTarget.cs index 760304cdeb4a5c..da922c47d93dca 100644 --- a/src/native/managed/cdac/tests/TestInfrastructure/TestPlaceholderTarget.cs +++ b/src/native/managed/cdac/tests/TestInfrastructure/TestPlaceholderTarget.cs @@ -656,12 +656,4 @@ public override bool TryGetContract([NotNullWhen(true)] out TContract public override void Flush(FlushScope scope) { } } - - public override IDisposable BeginReadCache() => NoOpScope.Instance; - - private sealed class NoOpScope : IDisposable - { - public static readonly NoOpScope Instance = new(); - public void Dispose() { } - } } From 2d5d7bf9f12cb04d50d628e5dca914f4b21443cc Mon Sep 17 00:00:00 2001 From: rcj1 Date: Fri, 19 Jun 2026 16:20:37 -0700 Subject: [PATCH 14/20] code review --- .../ITargetReadCache.cs | 5 ++- .../Contracts/StackWalk/GC/GcScanContext.cs | 3 +- .../LinearReadCache.cs | 32 +++----------- .../Dbi/Helpers/HeapWalk.cs | 3 +- .../ContractDescriptorTarget.cs | 43 +++++++++++-------- 5 files changed, 38 insertions(+), 48 deletions(-) diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/ITargetReadCache.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/ITargetReadCache.cs index 2502caf5cf0948..0c2d925367c6af 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/ITargetReadCache.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/ITargetReadCache.cs @@ -4,8 +4,11 @@ using System; namespace Microsoft.Diagnostics.DataContractReader; + +public delegate void RawReadDelegate(ulong address, Span destination); + public interface ITargetReadCache : IDisposable { - bool TryRead(ulong address, Span destination); + bool TryRead(ulong address, Span destination, RawReadDelegate readDelegate); void Invalidate(); } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanContext.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanContext.cs index 3e8d5bb34150a6..87ab03411b3939 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanContext.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanContext.cs @@ -117,12 +117,13 @@ public void GCEnumCallback(TargetPointer pObject, GcScanFlags flags, GcScanSlotL private TargetPointer GetInteriorPointer(TargetPointer obj) { TargetPointer outerObj = TargetPointer.Null; + LinearReadCache cache = new(); foreach ((GCHeapSegmentInfo seg, GCHeapData _) in _gc.EnumerateAllSegments()) { if (obj.Value < seg.Start.Value || obj.Value >= seg.End.Value) continue; - using IDisposable _ = _target.BeginCacheScope(new LinearReadCache(_target)); + using IDisposable _ = _target.BeginCacheScope(cache); TargetPointer currentObj = _gc.GetPotentialNextObjectAddress(seg.Start, 0, seg); while (currentObj.Value <= obj.Value) { diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/LinearReadCache.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/LinearReadCache.cs index c389e51c5ae693..a0c6d9ca6b9096 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/LinearReadCache.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/LinearReadCache.cs @@ -7,31 +7,24 @@ public sealed class LinearReadCache : ITargetReadCache, IDisposable { // Typical page size private const uint PageSize = 0x1000; - - private readonly Target _target; private readonly byte[] _page = new byte[PageSize]; private ulong _currPageStart; private uint _currPageSize; - public LinearReadCache(Target target) - { - _target = target; - } - - public bool TryRead(ulong addr, Span dest) + public bool TryRead(ulong addr, Span dest, RawReadDelegate readDelegate) { // If the request misses the currently-cached page, try to load the page // containing it. If that fails (e.g. the page is unmapped), or the request // straddles the end of the cached page, fall back to a direct read. if (addr < _currPageStart || addr - _currPageStart >= _currPageSize) { - if (!MoveToPage(addr)) - return DirectRead(addr, dest); + if (!MoveToPage(addr, readDelegate)) + return false; } ulong offset = addr - _currPageStart; if (offset + (ulong)dest.Length > _currPageSize) - return DirectRead(addr, dest); + return false; _page.AsSpan((int)offset, dest.Length).CopyTo(dest); return true; @@ -50,12 +43,12 @@ public void Dispose() // Nothing to dispose } - private bool MoveToPage(ulong addr) + private bool MoveToPage(ulong addr, RawReadDelegate readDelegate) { ulong pageStart = addr - (addr % PageSize); try { - _target.ReadBuffer(pageStart, _page.AsSpan(0, (int)PageSize)); + readDelegate(pageStart, _page.AsSpan(0, (int)PageSize)); _currPageStart = pageStart; _currPageSize = PageSize; return true; @@ -67,17 +60,4 @@ private bool MoveToPage(ulong addr) return false; } } - - private bool DirectRead(ulong addr, Span dest) - { - try - { - _target.ReadBuffer(addr, dest); - return true; - } - catch - { - return false; - } - } } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/Helpers/HeapWalk.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/Helpers/HeapWalk.cs index a38e83cde37ffd..b2ea1396ab52b7 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/Helpers/HeapWalk.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/Helpers/HeapWalk.cs @@ -30,10 +30,11 @@ public HeapWalk(Target target) private IEnumerable Walk() { bool pendingFailure = false; + LinearReadCache cache = new(); foreach ((GCHeapSegmentInfo seg, GCHeapData _) in _gc.EnumerateAllSegments()) { - using IDisposable _ = _target.BeginCacheScope(new LinearReadCache(_target)); + using IDisposable _ = _target.BeginCacheScope(cache); if (seg.Start.Value >= seg.End.Value) continue; diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader/ContractDescriptorTarget.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader/ContractDescriptorTarget.cs index e02f67cfa5cb8f..c9e83f3913f456 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader/ContractDescriptorTarget.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader/ContractDescriptorTarget.cs @@ -425,7 +425,7 @@ public override bool TryGetThreadContext(ulong threadId, uint contextFlags, Span /// Thrown when the read operation fails public override T Read(ulong address) { - if (!TryRead(address, _config.IsLittleEndian, _dataTargetDelegates, out T value)) + if (!TryRead(address, _config.IsLittleEndian, out T value)) throw new VirtualReadException($"Failed to read {typeof(T)} at 0x{address:x8}."); return value; @@ -439,7 +439,7 @@ public override T Read(ulong address) /// Value read from the target public override T ReadLittleEndian(ulong address) { - if (!TryRead(address, true, _dataTargetDelegates, out T value)) + if (!TryRead(address, true, out T value)) throw new VirtualReadException($"Failed to read {typeof(T)} at 0x{address:x8}."); return value; @@ -454,26 +454,26 @@ public override T ReadLittleEndian(ulong address) public override bool TryRead(ulong address, out T value) { value = default; - if (!TryRead(address, _config.IsLittleEndian, _dataTargetDelegates, out T readValue)) + if (!TryRead(address, _config.IsLittleEndian, out T readValue)) return false; value = readValue; return true; } - private bool TryRead(ulong address, bool isLittleEndian, DataTargetDelegates dataTargetDelegates, out T value) where T : unmanaged, IBinaryInteger, IMinMaxValue + private bool TryRead(ulong address, bool isLittleEndian, out T value) where T : unmanaged, IBinaryInteger, IMinMaxValue { value = default; Span buffer = stackalloc byte[sizeof(T)]; if (_activeCache is not null) { - // Use the linear read cache - if (!_activeCache.TryRead(address, buffer)) + // Use the read cache + if (!_activeCache.TryRead(address, buffer, (addr, buf) => _dataTargetDelegates.ReadFromTarget(addr, buf))) return false; } else { - if (dataTargetDelegates.ReadFromTarget(address, buffer) < 0) + if (_dataTargetDelegates.ReadFromTarget(address, buffer) < 0) return false; } @@ -559,6 +559,11 @@ public override void ReadBuffer(ulong address, Span buffer) private bool TryReadBuffer(ulong address, Span buffer) { + if (_activeCache is not null) + { + // Use the read cache + return _activeCache.TryRead(address, buffer, (addr, buf) => _dataTargetDelegates.ReadFromTarget(addr, buf)); + } return _dataTargetDelegates.ReadFromTarget(address, buffer) >= 0; } @@ -597,14 +602,14 @@ private static bool IsSigned() where T : struct, INumberBase, IMinMaxValue /// Pointer read from the target public override TargetPointer ReadPointer(ulong address) { - if (!TryReadPointer(address, _config, _dataTargetDelegates, out TargetPointer pointer)) + if (!TryReadPointer(address, _config, out TargetPointer pointer)) throw new VirtualReadException($"Failed to read pointer at 0x{address:x8}."); return pointer; } public override bool TryReadPointer(ulong address, out TargetPointer value) - => TryReadPointer(address, _config, _dataTargetDelegates, out value); + => TryReadPointer(address, _config, out value); public override TargetPointer ReadPointerFromSpan(ReadOnlySpan bytes) { @@ -729,7 +734,7 @@ public override string ReadUtf16String(ulong address) /// Value read from the target public override TargetNUInt ReadNUInt(ulong address) { - if (!TryReadNUInt(address, _config, _dataTargetDelegates, out ulong value)) + if (!TryReadNUInt(address, _config, out ulong value)) throw new VirtualReadException($"Failed to read nuint at 0x{address:x8}."); return new TargetNUInt(value); @@ -737,16 +742,16 @@ public override TargetNUInt ReadNUInt(ulong address) public override TargetNInt ReadNInt(ulong address) { - if (!TryReadNInt(address, _config, _dataTargetDelegates, out long value)) + if (!TryReadNInt(address, _config, out long value)) throw new VirtualReadException($"Failed to read nint at 0x{address:x8}."); return new TargetNInt(value); } - private bool TryReadPointer(ulong address, Configuration config, DataTargetDelegates dataTargetDelegates, out TargetPointer pointer) + private bool TryReadPointer(ulong address, Configuration config, out TargetPointer pointer) { pointer = TargetPointer.Null; - if (!TryReadNUInt(address, config, dataTargetDelegates, out ulong value)) + if (!TryReadNUInt(address, config, out ulong value)) return false; pointer = new TargetPointer(value); @@ -783,17 +788,17 @@ private static bool TryReadNUIntStatic(ulong address, Configuration config, Data } - private bool TryReadNUInt(ulong address, Configuration config, DataTargetDelegates dataTargetDelegates, out ulong value) + private bool TryReadNUInt(ulong address, Configuration config, out ulong value) { value = 0; if (config.PointerSize == sizeof(uint) - && TryRead(address, config.IsLittleEndian, dataTargetDelegates, out uint value32)) + && TryRead(address, config.IsLittleEndian, out uint value32)) { value = value32; return true; } else if (config.PointerSize == sizeof(ulong) - && TryRead(address, config.IsLittleEndian, dataTargetDelegates, out ulong value64)) + && TryRead(address, config.IsLittleEndian, out ulong value64)) { value = value64; return true; @@ -802,17 +807,17 @@ private bool TryReadNUInt(ulong address, Configuration config, DataTargetDelegat return false; } - private bool TryReadNInt(ulong address, Configuration config, DataTargetDelegates dataTargetDelegates, out long value) + private bool TryReadNInt(ulong address, Configuration config, out long value) { value = 0; if (config.PointerSize == sizeof(uint) - && TryRead(address, config.IsLittleEndian, dataTargetDelegates, out int value32)) + && TryRead(address, config.IsLittleEndian, out int value32)) { value = value32; return true; } else if (config.PointerSize == sizeof(ulong) - && TryRead(address, config.IsLittleEndian, dataTargetDelegates, out long value64)) + && TryRead(address, config.IsLittleEndian, out long value64)) { value = value64; return true; From 35f6f851046b7efffbcecbe04aa5b7264ce6f995 Mon Sep 17 00:00:00 2001 From: rcj1 Date: Fri, 19 Jun 2026 16:37:52 -0700 Subject: [PATCH 15/20] code review --- .../LinearReadCache.cs | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/LinearReadCache.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/LinearReadCache.cs index a0c6d9ca6b9096..f23a18a5b3abee 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/LinearReadCache.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/LinearReadCache.cs @@ -19,12 +19,12 @@ public bool TryRead(ulong addr, Span dest, RawReadDelegate readDelegate) if (addr < _currPageStart || addr - _currPageStart >= _currPageSize) { if (!MoveToPage(addr, readDelegate)) - return false; + return DirectRead(addr, dest, readDelegate); } ulong offset = addr - _currPageStart; if (offset + (ulong)dest.Length > _currPageSize) - return false; + return DirectRead(addr, dest, readDelegate); _page.AsSpan((int)offset, dest.Length).CopyTo(dest); return true; @@ -60,4 +60,17 @@ private bool MoveToPage(ulong addr, RawReadDelegate readDelegate) return false; } } + + private static bool DirectRead(ulong addr, Span dest, RawReadDelegate readDelegate) + { + try + { + readDelegate(addr, dest); + return true; + } + catch + { + return false; + } + } } From bc8ad5b66272eeec25963beea45fcf28cc28f759 Mon Sep 17 00:00:00 2001 From: rcj1 Date: Mon, 22 Jun 2026 11:11:10 -0700 Subject: [PATCH 16/20] revert to separate linearreadcache --- .../ITargetReadCache.cs | 14 -- .../Target.cs | 29 ---- .../Contracts/Object_1.cs | 7 +- .../Contracts/StackWalk/GC/GcScanContext.cs | 54 +++++++- .../LinearReadCache.cs | 67 ++++++--- .../Dbi/Helpers/HeapWalk.cs | 65 ++++++--- .../ContractDescriptorTarget.cs | 127 ++++-------------- .../TestPlaceholderTarget.cs | 1 + 8 files changed, 169 insertions(+), 195 deletions(-) delete mode 100644 src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/ITargetReadCache.cs diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/ITargetReadCache.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/ITargetReadCache.cs deleted file mode 100644 index 0c2d925367c6af..00000000000000 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/ITargetReadCache.cs +++ /dev/null @@ -1,14 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; - -namespace Microsoft.Diagnostics.DataContractReader; - -public delegate void RawReadDelegate(ulong address, Span destination); - -public interface ITargetReadCache : IDisposable -{ - bool TryRead(ulong address, Span destination, RawReadDelegate readDelegate); - void Invalidate(); -} diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Target.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Target.cs index 66302b293169e7..b091152bcc7a66 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Target.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Target.cs @@ -340,33 +340,4 @@ public virtual void Flush(FlushScope scope) ProcessedData.Clear(); Contracts.Flush(scope); } - - /// - /// Begin a scope during which reads from this target may be served by . - /// Returns a handle that ends the scope when disposed. Callers should typically wrap the - /// scope in a using block. - /// - /// The caching strategy to install for the duration of the scope. - /// - /// Only one cache scope may be active per target at a time; opening a second scope while one - /// is still active throws . - /// - public virtual IDisposable BeginCacheScope(ITargetReadCache cache) - { - ArgumentNullException.ThrowIfNull(cache); - return new DefaultCacheScope(cache); - } - - private sealed class DefaultCacheScope(ITargetReadCache cache) : IDisposable - { - private bool _disposed; - public void Dispose() - { - if (_disposed) - return; - _disposed = true; - cache.Invalidate(); - cache.Dispose(); - } - } } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/Object_1.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/Object_1.cs index 641792e3801948..6be137d7e28c2b 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/Object_1.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/Object_1.cs @@ -191,9 +191,6 @@ public DelegateInfo GetDelegateInfo(TargetPointer address) public ulong GetSize(TargetPointer address) { - // NOTE NOTE NOTE NOTE NOTE - // This method is called on every managed object in the debuggee during a heap walk. - // Do NOT allocate managed objects here in a way that scales with the number of heap objects. TargetPointer mt = GetMethodTableAddress(address); if (mt == TargetPointer.Null) throw new ArgumentException("Address represents a free object"); @@ -206,8 +203,8 @@ public ulong GetSize(TargetPointer address) { // Variable-size object (array or string): add the component data size. // Both Array and String share the m_NumComponents/m_StringLength field layout. - uint numComponents = _target.Read(address.Value + (ulong)_target.GetTypeInfo(DataType.Array).Fields["m_NumComponents"].Offset); - size += (ulong)numComponents * componentSize; + Data.Array arr = _target.ProcessedData.GetOrAdd(address); + size += (ulong)arr.NumComponents * componentSize; } return size; } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanContext.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanContext.cs index 87ab03411b3939..508a6b494801b6 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanContext.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanContext.cs @@ -11,6 +11,13 @@ internal class GcScanContext private readonly Target _target; private readonly IGC _gc; + private readonly IRuntimeTypeSystem _rts; + + private readonly LinearReadCache _cache; + private readonly uint _numComponentsOffsetArray; + private readonly uint _numComponentsOffsetString; + private readonly ulong _methodTableOffset; + private readonly byte _objectToMethodTableUnmask; public bool ResolveInteriorPointers { get; } public List StackRefs { get; } = []; public TargetPointer StackPointer { get; private set; } @@ -27,6 +34,12 @@ public GcScanContext(Target target, bool resolveInteriorPointers) _target = target; ResolveInteriorPointers = resolveInteriorPointers; _gc = target.Contracts.GC; + _rts = target.Contracts.RuntimeTypeSystem; + _cache = new LinearReadCache(target); + _numComponentsOffsetArray = (uint)target.GetTypeInfo(DataType.Array).Fields[Constants.FieldNames.Array.NumComponents].Offset; + _numComponentsOffsetString = (uint)target.GetTypeInfo(DataType.String).Fields["m_StringLength"].Offset; + _methodTableOffset = (ulong)target.GetTypeInfo(DataType.Object).Fields["m_pMethTab"].Offset; + _objectToMethodTableUnmask = target.ReadGlobal(Constants.Globals.ObjectToMethodTableUnmask); } public void UpdateScanContext(TargetPointer sp, TargetPointer ip, TargetPointer frame, StackRefData.SourceTypes? sourceTypeOverride = null) @@ -117,22 +130,22 @@ public void GCEnumCallback(TargetPointer pObject, GcScanFlags flags, GcScanSlotL private TargetPointer GetInteriorPointer(TargetPointer obj) { TargetPointer outerObj = TargetPointer.Null; - LinearReadCache cache = new(); foreach ((GCHeapSegmentInfo seg, GCHeapData _) in _gc.EnumerateAllSegments()) { if (obj.Value < seg.Start.Value || obj.Value >= seg.End.Value) continue; - using IDisposable _ = _target.BeginCacheScope(cache); TargetPointer currentObj = _gc.GetPotentialNextObjectAddress(seg.Start, 0, seg); while (currentObj.Value <= obj.Value) { ulong size; - try + if (!_cache.TryReadPointer(currentObj.Value + _methodTableOffset, out TargetPointer mt)) { - size = _target.Contracts.Object.GetSize(currentObj); + return TargetPointer.Null; } - catch + mt = mt.Value & (ulong)~_objectToMethodTableUnmask; + + if (!TryGetObjectSize(currentObj, mt, out size) || size == 0) { return TargetPointer.Null; } @@ -150,6 +163,37 @@ private TargetPointer GetInteriorPointer(TargetPointer obj) return outerObj; } + private bool TryGetObjectSize(TargetPointer objAddr, TargetPointer mt, out ulong size) + { + size = 0; + try + { + TypeHandle handle = _rts.GetTypeHandle(mt); + ulong baseSize = _rts.GetBaseSize(handle); + uint componentSize = _rts.GetComponentSize(handle); + uint numComponentsOffset = 0; + if (componentSize != 0) + { + if (_rts.IsArray(handle, out _) || _rts.IsFreeObjectMethodTable(handle)) + numComponentsOffset = _numComponentsOffsetArray; + else if (_rts.IsString(handle)) + numComponentsOffset = _numComponentsOffsetString; + else + return false; // unrecognized component type + if (!_cache.TryReadUInt32(objAddr.Value + numComponentsOffset, out uint numComponents)) + return false; + baseSize += (ulong)componentSize * numComponents; + } + size = baseSize; + return true; + } + catch + { + // The MT may be corrupt — surface as a read failure. + return false; + } + } + public void GCReportCallback(TargetPointer ppObj, GcScanFlags flags) { // Read the object pointer from the stack slot. diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/LinearReadCache.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/LinearReadCache.cs index f23a18a5b3abee..69a67b3872d53f 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/LinearReadCache.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/LinearReadCache.cs @@ -2,53 +2,76 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.Buffers.Binary; + namespace Microsoft.Diagnostics.DataContractReader; -public sealed class LinearReadCache : ITargetReadCache, IDisposable +// Linear page cache used by the per-object heap walk. +public sealed class LinearReadCache { // Typical page size private const uint PageSize = 0x1000; + private readonly Target _target; private readonly byte[] _page = new byte[PageSize]; private ulong _currPageStart; private uint _currPageSize; - public bool TryRead(ulong addr, Span dest, RawReadDelegate readDelegate) + public LinearReadCache(Target target) + { + _target = target; + } + + public bool TryReadPointer(ulong addr, out TargetPointer value) + { + Span buffer = stackalloc byte[sizeof(ulong)]; + buffer = buffer.Slice(0, _target.PointerSize); + if (!TryRead(addr, buffer)) + { + value = TargetPointer.Null; + return false; + } + value = _target.ReadPointerFromSpan(buffer); + return true; + } + + public bool TryReadUInt32(ulong addr, out uint value) + { + Span buffer = stackalloc byte[sizeof(uint)]; + if (!TryRead(addr, buffer)) + { + value = 0; + return false; + } + value = _target.IsLittleEndian + ? BinaryPrimitives.ReadUInt32LittleEndian(buffer) + : BinaryPrimitives.ReadUInt32BigEndian(buffer); + return true; + } + + private bool TryRead(ulong addr, Span dest) { // If the request misses the currently-cached page, try to load the page // containing it. If that fails (e.g. the page is unmapped), or the request // straddles the end of the cached page, fall back to a direct read. if (addr < _currPageStart || addr - _currPageStart >= _currPageSize) { - if (!MoveToPage(addr, readDelegate)) - return DirectRead(addr, dest, readDelegate); + if (!MoveToPage(addr)) + return DirectRead(addr, dest); } ulong offset = addr - _currPageStart; if (offset + (ulong)dest.Length > _currPageSize) - return DirectRead(addr, dest, readDelegate); + return DirectRead(addr, dest); _page.AsSpan((int)offset, dest.Length).CopyTo(dest); return true; } - public void Invalidate() - { - // Clear the current page cache - _currPageStart = 0; - _currPageSize = 0; - Array.Clear(_page, 0, _page.Length); - } - - public void Dispose() - { - // Nothing to dispose - } - - private bool MoveToPage(ulong addr, RawReadDelegate readDelegate) + private bool MoveToPage(ulong addr) { ulong pageStart = addr - (addr % PageSize); try { - readDelegate(pageStart, _page.AsSpan(0, (int)PageSize)); + _target.ReadBuffer(pageStart, _page.AsSpan(0, (int)PageSize)); _currPageStart = pageStart; _currPageSize = PageSize; return true; @@ -61,11 +84,11 @@ private bool MoveToPage(ulong addr, RawReadDelegate readDelegate) } } - private static bool DirectRead(ulong addr, Span dest, RawReadDelegate readDelegate) + private bool DirectRead(ulong addr, Span dest) { try { - readDelegate(addr, dest); + _target.ReadBuffer(addr, dest); return true; } catch diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/Helpers/HeapWalk.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/Helpers/HeapWalk.cs index b2ea1396ab52b7..4f040912e6b29b 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/Helpers/HeapWalk.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/Helpers/HeapWalk.cs @@ -13,8 +13,14 @@ internal sealed class HeapWalk : IEnum { private readonly IGC _gc; private readonly IObject _object; + private readonly IRuntimeTypeSystem _rts; private readonly TargetPointer _freeObjectMT; - private readonly Target _target; + private readonly LinearReadCache _cache; + private readonly uint _numComponentsOffsetArray; + private readonly uint _numComponentsOffsetString; + private readonly uint _methodTableOffset; + private readonly ulong _methodTableMask; + public IEnumerator Enumerator { get; } public nuint LegacyHandle { get; set; } = 0; @@ -22,42 +28,38 @@ public HeapWalk(Target target) { _gc = target.Contracts.GC; _object = target.Contracts.Object; + _rts = target.Contracts.RuntimeTypeSystem; _freeObjectMT = target.Contracts.RuntimeTypeSystem.GetWellKnownMethodTable(WellKnownMethodTable.Free); - _target = target; + _cache = new LinearReadCache(target); + // use these fields directly instead of through RuntimeTypeSystem so that we can use our cache that we really only need for heap walking + _numComponentsOffsetArray = (uint)target.GetTypeInfo(DataType.Array).Fields[Constants.FieldNames.Array.NumComponents].Offset; + _numComponentsOffsetString = (uint)target.GetTypeInfo(DataType.String).Fields["m_StringLength"].Offset; + _methodTableOffset = (uint)target.GetTypeInfo(DataType.Object).Fields["m_pMethTab"].Offset; + _methodTableMask = (ulong)~target.ReadGlobal(Constants.Globals.ObjectToMethodTableUnmask); + Enumerator = Walk().GetEnumerator(); } private IEnumerable Walk() { bool pendingFailure = false; - LinearReadCache cache = new(); foreach ((GCHeapSegmentInfo seg, GCHeapData _) in _gc.EnumerateAllSegments()) { - using IDisposable _ = _target.BeginCacheScope(cache); if (seg.Start.Value >= seg.End.Value) continue; TargetPointer currentObj = _gc.GetPotentialNextObjectAddress(seg.Start, 0, seg); while (currentObj.Value < seg.End.Value) { - TargetPointer mt; - ulong size; - try - { - mt = _object.GetMethodTableAddress(currentObj); - } - catch + if (!_cache.TryReadPointer(currentObj.Value + _methodTableOffset, out TargetPointer mt)) { pendingFailure = true; break; } + mt = new TargetPointer(mt.Value & _methodTableMask); - try - { - size = _object.GetSize(currentObj); - } - catch + if (!TryGetObjectSize(currentObj, mt, out ulong size) || size == 0) { pendingFailure = true; break; @@ -106,4 +108,35 @@ private IEnumerable Walk() if (pendingFailure) yield return default; } + + private bool TryGetObjectSize(TargetPointer objAddr, TargetPointer mt, out ulong size) + { + size = 0; + try + { + TypeHandle handle = _rts.GetTypeHandle(mt); + ulong baseSize = _rts.GetBaseSize(handle); + uint componentSize = _rts.GetComponentSize(handle); + uint numComponentsOffset = 0; + if (componentSize != 0) + { + if (_rts.IsArray(handle, out _) || _rts.IsFreeObjectMethodTable(handle)) + numComponentsOffset = _numComponentsOffsetArray; + else if (_rts.IsString(handle)) + numComponentsOffset = _numComponentsOffsetString; + else + return false; // unrecognized component type + if (!_cache.TryReadUInt32(objAddr.Value + numComponentsOffset, out uint numComponents)) + return false; + baseSize += (ulong)componentSize * numComponents; + } + size = baseSize; + return true; + } + catch + { + // The MT may be corrupt — surface as a read failure. + return false; + } + } } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader/ContractDescriptorTarget.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader/ContractDescriptorTarget.cs index c9e83f3913f456..633484f17e2578 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader/ContractDescriptorTarget.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader/ContractDescriptorTarget.cs @@ -27,7 +27,6 @@ namespace Microsoft.Diagnostics.DataContractReader; public sealed unsafe class ContractDescriptorTarget : Target { private const int StackAllocByteThreshold = 1024; - private ITargetReadCache? _activeCache; private readonly struct Configuration { @@ -147,7 +146,7 @@ private ContractDescriptorTarget(Descriptor mainDescriptor, DataTargetDelegates public override void Flush(FlushScope scope) { base.Flush(scope); - _activeCache?.Invalidate(); + BuildDescriptors(); } @@ -342,7 +341,7 @@ private static bool TryReadContractDescriptor( return false; // Flags - uint32_t - if (!TryReadStatic(address, isLittleEndian, dataTargetDelegates, out uint flags)) + if (!TryRead(address, isLittleEndian, dataTargetDelegates, out uint flags)) return false; address += sizeof(uint); @@ -353,19 +352,19 @@ private static bool TryReadContractDescriptor( Configuration config = new Configuration { IsLittleEndian = isLittleEndian, PointerSize = pointerSize }; // Descriptor size - uint32_t - if (!TryReadStatic(address, config.IsLittleEndian, dataTargetDelegates, out uint descriptorSize)) + if (!TryRead(address, config.IsLittleEndian, dataTargetDelegates, out uint descriptorSize)) return false; address += sizeof(uint); // Descriptor - char* - if (!TryReadPointerStatic(address, config, dataTargetDelegates, out TargetPointer descriptorAddr)) + if (!TryReadPointer(address, config, dataTargetDelegates, out TargetPointer descriptorAddr)) return false; address += (uint)pointerSize; // Pointer data count - uint32_t - if (!TryReadStatic(address, config.IsLittleEndian, dataTargetDelegates, out uint pointerDataCount)) + if (!TryRead(address, config.IsLittleEndian, dataTargetDelegates, out uint pointerDataCount)) return false; address += sizeof(uint); @@ -374,7 +373,7 @@ private static bool TryReadContractDescriptor( address += sizeof(uint); // Pointer data - uintptr_t* - if (!TryReadPointerStatic(address, config, dataTargetDelegates, out TargetPointer pointerDataAddr)) + if (!TryReadPointer(address, config, dataTargetDelegates, out TargetPointer pointerDataAddr)) return false; // Read descriptor @@ -392,7 +391,7 @@ private static bool TryReadContractDescriptor( TargetPointer[] pointerData = new TargetPointer[pointerDataCount]; for (int i = 0; i < pointerDataCount; i++) { - if (!TryReadPointerStatic(pointerDataAddr.Value + (uint)(i * pointerSize), config, dataTargetDelegates, out pointerData[i])) + if (!TryReadPointer(pointerDataAddr.Value + (uint)(i * pointerSize), config, dataTargetDelegates, out pointerData[i])) return false; } @@ -425,7 +424,7 @@ public override bool TryGetThreadContext(ulong threadId, uint contextFlags, Span /// Thrown when the read operation fails public override T Read(ulong address) { - if (!TryRead(address, _config.IsLittleEndian, out T value)) + if (!TryRead(address, _config.IsLittleEndian, _dataTargetDelegates, out T value)) throw new VirtualReadException($"Failed to read {typeof(T)} at 0x{address:x8}."); return value; @@ -439,7 +438,7 @@ public override T Read(ulong address) /// Value read from the target public override T ReadLittleEndian(ulong address) { - if (!TryRead(address, true, out T value)) + if (!TryRead(address, true, _dataTargetDelegates, out T value)) throw new VirtualReadException($"Failed to read {typeof(T)} at 0x{address:x8}."); return value; @@ -454,35 +453,14 @@ public override T ReadLittleEndian(ulong address) public override bool TryRead(ulong address, out T value) { value = default; - if (!TryRead(address, _config.IsLittleEndian, out T readValue)) + if (!TryRead(address, _config.IsLittleEndian, _dataTargetDelegates, out T readValue)) return false; value = readValue; return true; } - private bool TryRead(ulong address, bool isLittleEndian, out T value) where T : unmanaged, IBinaryInteger, IMinMaxValue - { - value = default; - Span buffer = stackalloc byte[sizeof(T)]; - if (_activeCache is not null) - { - // Use the read cache - if (!_activeCache.TryRead(address, buffer, (addr, buf) => _dataTargetDelegates.ReadFromTarget(addr, buf))) - return false; - } - else - { - if (_dataTargetDelegates.ReadFromTarget(address, buffer) < 0) - return false; - } - - return isLittleEndian - ? T.TryReadLittleEndian(buffer, !IsSigned(), out value) - : T.TryReadBigEndian(buffer, !IsSigned(), out value); - } - - private static bool TryReadStatic(ulong address, bool isLittleEndian, DataTargetDelegates dataTargetDelegates, out T value) where T : unmanaged, IBinaryInteger, IMinMaxValue + private static bool TryRead(ulong address, bool isLittleEndian, DataTargetDelegates dataTargetDelegates, out T value) where T : unmanaged, IBinaryInteger, IMinMaxValue { value = default; Span buffer = stackalloc byte[sizeof(T)]; @@ -559,11 +537,6 @@ public override void ReadBuffer(ulong address, Span buffer) private bool TryReadBuffer(ulong address, Span buffer) { - if (_activeCache is not null) - { - // Use the read cache - return _activeCache.TryRead(address, buffer, (addr, buf) => _dataTargetDelegates.ReadFromTarget(addr, buf)); - } return _dataTargetDelegates.ReadFromTarget(address, buffer) >= 0; } @@ -602,14 +575,14 @@ private static bool IsSigned() where T : struct, INumberBase, IMinMaxValue /// Pointer read from the target public override TargetPointer ReadPointer(ulong address) { - if (!TryReadPointer(address, _config, out TargetPointer pointer)) + if (!TryReadPointer(address, _config, _dataTargetDelegates, out TargetPointer pointer)) throw new VirtualReadException($"Failed to read pointer at 0x{address:x8}."); return pointer; } public override bool TryReadPointer(ulong address, out TargetPointer value) - => TryReadPointer(address, _config, out value); + => TryReadPointer(address, _config, _dataTargetDelegates, out value); public override TargetPointer ReadPointerFromSpan(ReadOnlySpan bytes) { @@ -734,7 +707,7 @@ public override string ReadUtf16String(ulong address) /// Value read from the target public override TargetNUInt ReadNUInt(ulong address) { - if (!TryReadNUInt(address, _config, out ulong value)) + if (!TryReadNUInt(address, _config, _dataTargetDelegates, out ulong value)) throw new VirtualReadException($"Failed to read nuint at 0x{address:x8}."); return new TargetNUInt(value); @@ -742,63 +715,33 @@ public override TargetNUInt ReadNUInt(ulong address) public override TargetNInt ReadNInt(ulong address) { - if (!TryReadNInt(address, _config, out long value)) + if (!TryReadNInt(address, _config, _dataTargetDelegates, out long value)) throw new VirtualReadException($"Failed to read nint at 0x{address:x8}."); return new TargetNInt(value); } - private bool TryReadPointer(ulong address, Configuration config, out TargetPointer pointer) - { - pointer = TargetPointer.Null; - if (!TryReadNUInt(address, config, out ulong value)) - return false; - - pointer = new TargetPointer(value); - return true; - } - - private static bool TryReadPointerStatic(ulong address, Configuration config, DataTargetDelegates dataTargetDelegates, out TargetPointer pointer) + private static bool TryReadPointer(ulong address, Configuration config, DataTargetDelegates dataTargetDelegates, out TargetPointer pointer) { pointer = TargetPointer.Null; - if (!TryReadNUIntStatic(address, config, dataTargetDelegates, out ulong value)) + if (!TryReadNUInt(address, config, dataTargetDelegates, out ulong value)) return false; pointer = new TargetPointer(value); return true; } - private static bool TryReadNUIntStatic(ulong address, Configuration config, DataTargetDelegates dataTargetDelegates, out ulong value) - { - value = 0; - if (config.PointerSize == sizeof(uint) - && TryReadStatic(address, config.IsLittleEndian, dataTargetDelegates, out uint value32)) - { - value = value32; - return true; - } - else if (config.PointerSize == sizeof(ulong) - && TryReadStatic(address, config.IsLittleEndian, dataTargetDelegates, out ulong value64)) - { - value = value64; - return true; - } - - return false; - } - - - private bool TryReadNUInt(ulong address, Configuration config, out ulong value) + private static bool TryReadNUInt(ulong address, Configuration config, DataTargetDelegates dataTargetDelegates, out ulong value) { value = 0; if (config.PointerSize == sizeof(uint) - && TryRead(address, config.IsLittleEndian, out uint value32)) + && TryRead(address, config.IsLittleEndian, dataTargetDelegates, out uint value32)) { value = value32; return true; } else if (config.PointerSize == sizeof(ulong) - && TryRead(address, config.IsLittleEndian, out ulong value64)) + && TryRead(address, config.IsLittleEndian, dataTargetDelegates, out ulong value64)) { value = value64; return true; @@ -807,17 +750,17 @@ private bool TryReadNUInt(ulong address, Configuration config, out ulong value) return false; } - private bool TryReadNInt(ulong address, Configuration config, out long value) + private static bool TryReadNInt(ulong address, Configuration config, DataTargetDelegates dataTargetDelegates, out long value) { value = 0; if (config.PointerSize == sizeof(uint) - && TryRead(address, config.IsLittleEndian, out int value32)) + && TryRead(address, config.IsLittleEndian, dataTargetDelegates, out int value32)) { value = value32; return true; } else if (config.PointerSize == sizeof(ulong) - && TryRead(address, config.IsLittleEndian, out long value64)) + && TryRead(address, config.IsLittleEndian, dataTargetDelegates, out long value64)) { value = value64; return true; @@ -1012,28 +955,4 @@ public int AllocVirtual(ulong size, out ulong allocatedAddress) return allocVirtual(size, out allocatedAddress); } } - - public override IDisposable BeginCacheScope(ITargetReadCache cache) - { - ArgumentNullException.ThrowIfNull(cache); - if (_activeCache is not null) - throw new InvalidOperationException("Nested cache scopes are not supported."); - _activeCache = cache; - return new CacheScope(this, cache); - } - - private sealed class CacheScope(ContractDescriptorTarget target, ITargetReadCache cache) : IDisposable - { - private bool _disposed; - public void Dispose() - { - if (_disposed) - return; - _disposed = true; - Debug.Assert(ReferenceEquals(target._activeCache, cache), "Cache scope disposed out of order"); - target._activeCache = null; - cache.Invalidate(); - cache.Dispose(); - } - } } diff --git a/src/native/managed/cdac/tests/TestInfrastructure/TestPlaceholderTarget.cs b/src/native/managed/cdac/tests/TestInfrastructure/TestPlaceholderTarget.cs index da922c47d93dca..12561ffd8676a7 100644 --- a/src/native/managed/cdac/tests/TestInfrastructure/TestPlaceholderTarget.cs +++ b/src/native/managed/cdac/tests/TestInfrastructure/TestPlaceholderTarget.cs @@ -656,4 +656,5 @@ public override bool TryGetContract([NotNullWhen(true)] out TContract public override void Flush(FlushScope scope) { } } + } From 5263cf41f095b936451aa14c78aaf51ab59b8c3d Mon Sep 17 00:00:00 2001 From: Rachel Jarvi Date: Mon, 22 Jun 2026 11:14:18 -0700 Subject: [PATCH 17/20] Update GcScanContext.cs --- .../Contracts/StackWalk/GC/GcScanContext.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanContext.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanContext.cs index 508a6b494801b6..992a08ea771d4b 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanContext.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanContext.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using Microsoft.Diagnostics.DataContractReader.Contracts.Extensions; + namespace Microsoft.Diagnostics.DataContractReader.Contracts.StackWalkHelpers; internal class GcScanContext From 086b19aa2adbda139841d90eebc30d3d51d56d99 Mon Sep 17 00:00:00 2001 From: Rachel Jarvi Date: Mon, 22 Jun 2026 11:15:37 -0700 Subject: [PATCH 18/20] Update HeapWalk.cs --- .../Dbi/Helpers/HeapWalk.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/Helpers/HeapWalk.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/Helpers/HeapWalk.cs index 4f040912e6b29b..6b91f478bc62a5 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/Helpers/HeapWalk.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/Helpers/HeapWalk.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; -using System.Linq; using Microsoft.Diagnostics.DataContractReader.Contracts; using Microsoft.Diagnostics.DataContractReader.Contracts.Extensions; @@ -29,7 +28,7 @@ public HeapWalk(Target target) _gc = target.Contracts.GC; _object = target.Contracts.Object; _rts = target.Contracts.RuntimeTypeSystem; - _freeObjectMT = target.Contracts.RuntimeTypeSystem.GetWellKnownMethodTable(WellKnownMethodTable.Free); + _freeObjectMT = _rts.GetWellKnownMethodTable(WellKnownMethodTable.Free); _cache = new LinearReadCache(target); // use these fields directly instead of through RuntimeTypeSystem so that we can use our cache that we really only need for heap walking _numComponentsOffsetArray = (uint)target.GetTypeInfo(DataType.Array).Fields[Constants.FieldNames.Array.NumComponents].Offset; From 0b1ea62db63bf74c9516de66c4e3f0d14d632975 Mon Sep 17 00:00:00 2001 From: rcj1 Date: Mon, 22 Jun 2026 16:09:29 -0700 Subject: [PATCH 19/20] add comments --- .../Contracts/StackWalk/GC/GcScanContext.cs | 2 ++ .../Dbi/Helpers/HeapWalk.cs | 2 ++ 2 files changed, 4 insertions(+) diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanContext.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanContext.cs index 992a08ea771d4b..300bb164445dc8 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanContext.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanContext.cs @@ -140,12 +140,14 @@ private TargetPointer GetInteriorPointer(TargetPointer obj) while (currentObj.Value <= obj.Value) { ulong size; + // Replicate IObject.GetMethodTableAddress in fast path with linear read cache if (!_cache.TryReadPointer(currentObj.Value + _methodTableOffset, out TargetPointer mt)) { return TargetPointer.Null; } mt = mt.Value & (ulong)~_objectToMethodTableUnmask; + // Replicate IObject.GetSize in fast path with linear read cache if (!TryGetObjectSize(currentObj, mt, out size) || size == 0) { return TargetPointer.Null; diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/Helpers/HeapWalk.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/Helpers/HeapWalk.cs index 6b91f478bc62a5..2e42a79f024b5d 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/Helpers/HeapWalk.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/Helpers/HeapWalk.cs @@ -51,6 +51,7 @@ private IEnumerable Walk() TargetPointer currentObj = _gc.GetPotentialNextObjectAddress(seg.Start, 0, seg); while (currentObj.Value < seg.End.Value) { + // Replicate IObject.GetMethodTableAddress in fast path with linear read cache if (!_cache.TryReadPointer(currentObj.Value + _methodTableOffset, out TargetPointer mt)) { pendingFailure = true; @@ -58,6 +59,7 @@ private IEnumerable Walk() } mt = new TargetPointer(mt.Value & _methodTableMask); + // Replicate IObject.GetSize in fast path with linear read cache if (!TryGetObjectSize(currentObj, mt, out ulong size) || size == 0) { pendingFailure = true; From ec07fc074a0c90c245b85babb4e634ec40fe1472 Mon Sep 17 00:00:00 2001 From: rcj1 Date: Tue, 23 Jun 2026 09:42:17 -0700 Subject: [PATCH 20/20] ccr --- src/coreclr/debug/daccess/dacdbiimpl.cpp | 17 +---------------- src/coreclr/debug/daccess/dacdbiimpl.h | 5 ----- .../Contracts/StackWalk/GC/GcScanContext.cs | 4 ++-- .../Dbi/DacDbiImpl.cs | 10 +++++----- .../Dbi/Helpers/HeapWalk.cs | 2 -- 5 files changed, 8 insertions(+), 30 deletions(-) diff --git a/src/coreclr/debug/daccess/dacdbiimpl.cpp b/src/coreclr/debug/daccess/dacdbiimpl.cpp index de6961295ca564..3c8e48f22517cb 100644 --- a/src/coreclr/debug/daccess/dacdbiimpl.cpp +++ b/src/coreclr/debug/daccess/dacdbiimpl.cpp @@ -7412,7 +7412,7 @@ HRESULT STDMETHODCALLTYPE DacDbiInterfaceImpl::GetGenericArgTokenIndex(VMPTR_Met DacRefWalker::DacRefWalker(ClrDataAccess *dac, BOOL walkStacks, UINT32 handleMask, BOOL resolvePointers) : mDac(dac), mWalkStacks(walkStacks), mHandleMask(handleMask), mStackWalker(NULL), - mResolvePointers(resolvePointers), mHandleWalker(NULL), mFQStart(PTR_NULL), mFQEnd(PTR_NULL), mFQCurr(PTR_NULL) + mResolvePointers(resolvePointers), mHandleWalker(NULL) { } @@ -7507,21 +7507,6 @@ HRESULT DacRefWalker::Next(ULONG celt, DacGcReference roots[], ULONG *pceltFetch } } - if (total < celt) - { - while (total < celt && mFQCurr < mFQEnd) - { - DacGcReference &ref = roots[total++]; - - ref.vmDomain = VMPTR_AppDomain::NullPtr(); - ref.objHnd.SetDacTargetPtr(mFQCurr.GetAddr()); - ref.dwType = (DWORD)CorReferenceFinalizer; - ref.i64ExtraData = 0; - - mFQCurr++; - } - } - while (total < celt && mStackWalker) { ULONG fetched = 0; diff --git a/src/coreclr/debug/daccess/dacdbiimpl.h b/src/coreclr/debug/daccess/dacdbiimpl.h index 3b607eb621db63..a1e947dc357a95 100644 --- a/src/coreclr/debug/daccess/dacdbiimpl.h +++ b/src/coreclr/debug/daccess/dacdbiimpl.h @@ -980,11 +980,6 @@ class DacRefWalker // Handles DacHandleWalker *mHandleWalker; - - // FQ - PTR_PTR_Object mFQStart; - PTR_PTR_Object mFQEnd; - PTR_PTR_Object mFQCurr; }; #endif // _DACDBI_IMPL_H_ diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanContext.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanContext.cs index 300bb164445dc8..e26549b29caee9 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanContext.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanContext.cs @@ -137,9 +137,9 @@ private TargetPointer GetInteriorPointer(TargetPointer obj) continue; TargetPointer currentObj = _gc.GetPotentialNextObjectAddress(seg.Start, 0, seg); + ulong size = 0; while (currentObj.Value <= obj.Value) { - ulong size; // Replicate IObject.GetMethodTableAddress in fast path with linear read cache if (!_cache.TryReadPointer(currentObj.Value + _methodTableOffset, out TargetPointer mt)) { @@ -161,7 +161,7 @@ private TargetPointer GetInteriorPointer(TargetPointer obj) outerObj = currentObj; currentObj = _gc.GetPotentialNextObjectAddress(currentObj, size, seg); } - return outerObj; + return outerObj + size > obj ? outerObj : TargetPointer.Null; } return outerObj; } 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 f2300e45282dd6..0bea797176476c 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 @@ -3942,10 +3942,7 @@ public int WalkHeap(nuint handle, uint count, COR_HEAPOBJECT* objects, uint* fet { hrLocal = _legacy.WalkHeap(walk.LegacyHandle, count, objectsLocalPtr, &fetchedLocal); } - // The number of reported stack refs is not guaranteed to match between cDAC and legacy DAC. - // If this is the case, the cDAC may report S_FALSE while the legacy DAC reports S_OK, or vice versa. - // Allow divergent success codes, but still validate the rest of the results. - Debug.ValidateHResult(hr, hrLocal, HResultValidationMode.AllowDivergentSuccess); + Debug.ValidateHResult(hr, hrLocal); if (hr >= HResults.S_OK) { Debug.Assert(*fetched == fetchedLocal, @@ -4246,7 +4243,10 @@ public int WalkRefs(nuint handle, uint count, [In, MarshalUsing(CountElementName DacGcReference[] legacyRefs = new DacGcReference[(int)count]; uint legacyFetched = 0; int hrLocal = _legacy.WalkRefs(walk.LegacyHandle, count, legacyRefs, &legacyFetched); - // Debug.ValidateHResult(hr, hrLocal); + // The number of reported stack refs is not guaranteed to match between cDAC and legacy DAC. + // If this is the case, the cDAC may report S_FALSE while the legacy DAC reports S_OK, or vice versa. + // Allow divergent success codes, but still validate the rest of the results. + Debug.ValidateHResult(hr, hrLocal, HResultValidationMode.AllowDivergentSuccess); uint cdacHandlePrefix = CountHandlePrefix(refs, i); uint legacyHandlePrefix = CountHandlePrefix(legacyRefs, legacyFetched); Debug.Assert( diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/Helpers/HeapWalk.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/Helpers/HeapWalk.cs index 2e42a79f024b5d..4910f02297cc15 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/Helpers/HeapWalk.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/Helpers/HeapWalk.cs @@ -11,7 +11,6 @@ namespace Microsoft.Diagnostics.DataContractReader.Legacy; internal sealed class HeapWalk : IEnum { private readonly IGC _gc; - private readonly IObject _object; private readonly IRuntimeTypeSystem _rts; private readonly TargetPointer _freeObjectMT; private readonly LinearReadCache _cache; @@ -26,7 +25,6 @@ internal sealed class HeapWalk : IEnum public HeapWalk(Target target) { _gc = target.Contracts.GC; - _object = target.Contracts.Object; _rts = target.Contracts.RuntimeTypeSystem; _freeObjectMT = _rts.GetWellKnownMethodTable(WellKnownMethodTable.Free); _cache = new LinearReadCache(target);