Skip to content

[cDAC] Cache resolved read-only metadata address per module in EcmaMetadata_1#128774

Open
max-charlamb wants to merge 1 commit into
dotnet:mainfrom
max-charlamb:cdac/cache-readonly-metadata
Open

[cDAC] Cache resolved read-only metadata address per module in EcmaMetadata_1#128774
max-charlamb wants to merge 1 commit into
dotnet:mainfrom
max-charlamb:cdac/cache-readonly-metadata

Conversation

@max-charlamb
Copy link
Copy Markdown
Member

@max-charlamb max-charlamb commented May 29, 2026

Fixes part of #128717.

GetReadOnlyMetadataAddress walked the PE headers on every call and asked PEReader to also PrefetchMetadata, which eagerly read the entire metadata blob into memory and then discarded it (we only ever read PEHeaders.MetadataStartOffset / MetadataSize from the PEReader before disposing it).

On dumps where the metadata pages are not all captured, the prefetch threw VirtualReadException before the method could cache its result, so every subsequent call repeated the full PE walk and threw again. The exceptions were swallowed by callers' fallbacks (e.g. SOSDacImpl.GetMethodTableName -> DacStreams.StringFromEEAddress), so functionality was unaffected but performance suffered badly.

This PR:

  • Drops PrefetchMetadata (we never use the prefetched bytes; callers read the metadata span themselves via target.ReadBuffer).
  • Adds a small per-handle cache that Flush() clears alongside _metadata.
  • Promotes ModuleHandle to a record struct so dictionary lookups use compiler-generated IEquatable<T>/GetHashCode instead of reflection-based ValueType.Equals (~6x faster in a microbenchmark).

Measurements

Measured on SOS.WebApp3.Heap.dmp via a local benchmark harness (GetMethodTableData + GetMethodTableName + GetMethodTableFieldData + GetObjectData over the full GC heap, 3 iterations):

Metric Before After Delta
Cold-iter reads 137,522 61,306 -55%
Warm-iter reads 99,198 19,518 -80%
Total reads 385,928 150,351 -61%
Failed reads 1,992 1,992 unchanged
Name resolution ok/err 686/0 686/0 unchanged

GetReadOnlyMetadataAddress walked the PE headers on every call and
asked PEReader to also PrefetchMetadata, which slurped the entire
metadata blob into memory and then discarded it (we only ever read
PEHeaders.MetadataStartOffset/MetadataSize from the PEReader before
disposing it). On dumps where the metadata pages are not all captured,
the prefetch threw VirtualReadException before the method could cache
its result, so every subsequent call repeated the full PE walk and
threw again. The exceptions were swallowed by callers' fallbacks (e.g.
SOSDacImpl.GetMethodTableName -> DacStreams.StringFromEEAddress), so
functionality was unaffected but performance suffered badly.

Drop PrefetchMetadata (we never use the prefetched bytes; callers read
the metadata span themselves via target.ReadBuffer) and add a small
per-handle cache that Flush() clears alongside _metadata.

Also promote ModuleHandle to a record struct so it implements
IEquatable<ModuleHandle> with a custom GetHashCode. The previous
plain-struct version forced Dictionary<ModuleHandle, ...> lookups
through ValueType.Equals/GetHashCode (reflection-based), which is
roughly 6x slower than the compiler-generated equality.

Measured on SOS.WebApp3.Heap.dmp via a local benchmark harness
(GetMethodTableData + GetMethodTableName + GetMethodTableFieldData +
GetObjectData over the full GC heap, 3 iterations):

  warm-iter reads:   99,198 -> 19,518   (-80%)
  warm-iter time:    0.19 s -> 0.14 s   (-29% to -42%)
  total reads:       385,928 -> 150,351 (-61%)
  failed reads:      1,992 (unchanged)
  name ok/err:       686/0  (unchanged)

Contributes to dotnet#128717.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings May 29, 2026 15:29
@max-charlamb max-charlamb changed the title Cache resolved read-only metadata address per module in EcmaMetadata_1 [cDAC] Cache resolved read-only metadata address per module in EcmaMetadata_1 May 29, 2026
@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.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Caches the resolved read-only metadata TargetSpan per ModuleHandle in EcmaMetadata_1 and drops the unused PrefetchMetadata flag from PEReader, eliminating repeated PE walks and per-call eager metadata reads that were throwing VirtualReadException on dumps with partial metadata pages. ModuleHandle is promoted to a record struct to give dictionary lookups compiler-generated value equality instead of reflection-based ValueType.Equals.

Changes:

  • Add _readOnlyMetadataAddress cache in EcmaMetadata_1, cleared in Flush() alongside _metadata.
  • Stop requesting PEStreamOptions.PrefetchMetadata since callers read metadata bytes themselves via target.ReadBuffer.
  • Convert ModuleHandle from a hand-written readonly struct to a readonly record struct for fast value equality/hashing.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.

File Description
src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/EcmaMetadata_1.cs Adds per-module cache for the read-only metadata address and removes PrefetchMetadata.
src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/ILoader.cs Converts ModuleHandle to a readonly record struct for compiler-generated equality.

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.

3 participants