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/12] 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/12] 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/12] 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/12] 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/12] 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/12] 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/12] 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/12] 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/12] 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/12] 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/12] 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/12] 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;