Skip to content

[cDAC] Add pluggable cache scopes to Target#129604

Draft
max-charlamb wants to merge 1 commit into
dotnet:mainfrom
max-charlamb:cdac/target-cache-scopes
Draft

[cDAC] Add pluggable cache scopes to Target#129604
max-charlamb wants to merge 1 commit into
dotnet:mainfrom
max-charlamb:cdac/target-cache-scopes

Conversation

@max-charlamb

Copy link
Copy Markdown
Member

Introduces a small abstraction for caching reads from a cDAC Target during focused operations such as a heap walk. Replaces the ad-hoc LinearReadCache that was previously a private helper inside HeapWalk.cs.

API

ITargetReadCache (new, in Microsoft.Diagnostics.DataContractReader.Abstractions):

public delegate void RawReadDelegate(ulong address, Span<byte> destination);

public interface ITargetReadCache : IDisposable
{
    void ReadBuffer(ulong address, Span<byte> destination, RawReadDelegate fallback);
    void Invalidate();
}

Target.BeginCacheScope (new, virtual):

public virtual IDisposable BeginCacheScope(ITargetReadCache cache);

Opens a scope during which Target reads route through the supplied cache. Default implementation is a no-op suitable for test stubs - it still invalidates and disposes the cache on scope end so the lifecycle is consistent.

LinearReadCache (new, public): page-buffered ITargetReadCache. Reads inside a single page are served from a cached buffer; misses, larger-than-page reads, and page-straddling reads fall through. Configurable page size, default 0x1000.

ContractDescriptorTarget

  • Implements BeginCacheScope. Nested scopes throw InvalidOperationException.
  • TryReadBuffer routes through the active cache when present.
  • Typed reads (TryRead<T>, ReadPointer/TryReadPointer, ReadNUInt/ReadNInt) consolidated to funnel through TryReadBuffer so they benefit from caching automatically.
  • The bootstrap TryReadContractDescriptor helpers continue to read directly (no target instance yet).

Caller migration

HeapWalk (legacy DBI): drops its private LinearReadCache, opens a single cache scope spanning the entire walk, and uses Target's typed-read APIs:

using IDisposable _ = _target.BeginCacheScope(new LinearReadCache());

while (currentObj.Value < seg.End.Value)
{
    if (!_target.TryReadPointer(currentObj.Value + _methodTableOffset, out TargetPointer mt))
        ...
    if (!_target.TryRead(objAddr.Value + numComponentsOffset, out uint numComponents))
        ...
}

The cache stays warm across iterations - subsequent iterations typically hit the same page (object header + a trailing component-count field).

Tests

  • LinearReadCacheTests: page hit, cross-page miss, oversized read, separate page reload, Invalidate, page-fault fallback path, constructor validation.
  • CacheScopeTests: reads inside vs outside scope, typed reads route through cache, nested scope throws, Dispose is idempotent, allows new scope after dispose, null cache throws.

Full cdac unit suite: Passed: 2595, Failed: 0, Skipped: 16.

Design notes

See cdac-cache-scope-proposal.md (session artifact) for the full design discussion. Key choices:

  • Single active scope (no nesting). cDAC is single-threaded; nesting can be added later if a use case appears.
  • Invalidate and Dispose are separate so callers can flush mid-scope (e.g. after a target write) without ending the scope.
  • Fallback is a delegate, not the Target. Keeps caches decoupled from Target; the delegate is cached once per ContractDescriptorTarget so opening a scope does not allocate.
  • Default BeginCacheScope is virtual, not abstract. Test target stubs (TestPlaceholderTarget, TestTarget) need no changes; concrete reader targets opt into real caching.

Note

This PR was authored with assistance from GitHub Copilot.

Introduces a small abstraction for caching reads from a cDAC Target during
focused operations such as a heap walk.

- ITargetReadCache (new, in Abstractions): single ReadBuffer(addr, dest, fallback)
  + Invalidate, IDisposable. Implementations decide whether to satisfy a read
  from cached state or fall through via the supplied RawReadDelegate.
- Target.BeginCacheScope(ITargetReadCache cache) : IDisposable (virtual): opens
  a scope during which Target reads route through the cache. Default impl on
  Target keeps the no-cache behavior used by test stubs and still invalidates
  + disposes the cache on scope end so the contract is consistent.
- LinearReadCache (new, public): page-buffered ITargetReadCache. Reads inside
  a single page are served from a cached buffer; misses, larger-than-page
  reads, and page-straddling reads fall through.
- ContractDescriptorTarget: implements BeginCacheScope; rejects nested scopes;
  routes TryReadBuffer through the active cache; consolidates typed reads
  (TryRead<T>, ReadPointer/TryReadPointer, ReadNUInt/ReadNInt) to funnel
  through TryReadBuffer so they benefit from caching automatically. The
  bootstrap-time TryReadContractDescriptor helpers continue to read directly.
- HeapWalk (Legacy/Dbi): drops its private LinearReadCache, opens a single
  cache scope spanning the entire walk, and uses Target's typed-read APIs.

Tests: LinearReadCacheTests (page hit/miss, cross-page, oversized, invalidate,
page-fault fallback, constructor validation); CacheScopeTests (reads inside
vs outside scope, typed reads route through cache, nested-scope throws,
dispose is idempotent, allows new scope after dispose, null-cache throws).

Full cdac unit suite passes: 2595 passed / 0 failed / 16 skipped.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @steveisok, @tommcdon, @dotnet/dotnet-diag
See info in area-owners.md if you want to be subscribed.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds a pluggable read-caching mechanism to the cDAC Target abstraction via cache scopes, introduces a reusable page-based cache implementation, migrates the legacy heap-walk code path to use the new scope, and adds unit tests for both the cache and the scoping behavior.

Changes:

  • Add ITargetReadCache + RawReadDelegate abstractions and a new Target.BeginCacheScope(ITargetReadCache) virtual API.
  • Implement cache scoping + cache-routed reads in ContractDescriptorTarget, and add a public LinearReadCache implementation.
  • Update legacy HeapWalk to open a single cache scope for the duration of the walk; add unit tests for the cache and scope behaviors.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/ITargetReadCache.cs Adds the cache interface + fallback delegate abstraction.
src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Target.cs Adds the new BeginCacheScope virtual API and default scope implementation.
src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader/LinearReadCache.cs Introduces a page-buffered cache implementation.
src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader/ContractDescriptorTarget.cs Implements cache scoping and funnels typed reads through cached buffer reads.
src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/Helpers/HeapWalk.cs Uses a cache scope for the full heap walk and switches to Target typed read APIs.
src/native/managed/cdac/tests/UnitTests/LinearReadCacheTests.cs Adds unit tests for LinearReadCache behavior.
src/native/managed/cdac/tests/UnitTests/ContractDescriptor/CacheScopeTests.cs Adds unit tests for scope lifecycle, routing, and nesting behavior.

Comment on lines +357 to +361
public virtual IDisposable BeginCacheScope(ITargetReadCache cache)
{
ArgumentNullException.ThrowIfNull(cache);
return new DefaultCacheScope(cache);
}
Comment on lines +351 to +355
/// Only one cache scope may be active per target at a time; opening a second scope while one
/// is still active throws <see cref="InvalidOperationException"/>. The default implementation
/// does not route reads through the cache and is suitable for targets (such as test stubs)
/// that do not need caching; it still invalidates and disposes the cache when the scope ends
/// so that <see cref="ITargetReadCache"/> implementations see a consistent lifecycle.
Comment on lines +28 to +31
/// Read <c>destination.Length</c> bytes starting at <paramref name="address"/>. On a cache
/// miss the implementation must call <paramref name="fallback"/> to fetch the bytes from the
/// underlying target. Exceptions thrown by <paramref name="fallback"/> must be allowed to
/// propagate to the caller.
Comment on lines +34 to +39
public LinearReadCache(uint pageSize)
{
ArgumentOutOfRangeException.ThrowIfZero(pageSize);
_pageSize = pageSize;
_page = new byte[pageSize];
}
Comment on lines +62 to +70
catch
{
// The page is unreadable (e.g. unmapped). Drop any cached state and let the
// direct read surface the failure.
_currPageStart = 0;
_currPageSize = 0;
fallback(address, destination);
return;
}
Comment on lines +147 to +151
// Cached delegate over ReadBufferRaw so opening a cache scope does not allocate.
private readonly RawReadDelegate _rawReadBuffer;
private ITargetReadCache? _activeCache;

public override IDisposable BeginCacheScope(ITargetReadCache cache)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants