Skip to content

TraceEvent SymbolReader does not resolve source lines from an embedded portable PDB #2433

Description

@JeremyKuhne

Summary

Microsoft.Diagnostics.Tracing.TraceEvent's SymbolReader cannot resolve managed source lines for a module that ships its portable PDB embedded in the PE image (<DebugType>embedded</DebugType>). When no standalone .pdb exists next to the DLL, on the SymbolPath, or on a symbol server, TraceCodeAddress.GetSourceLine(SymbolReader) falls back to no source, even though the PDB bytes are present inside the on-disk module that the trace points at (the trace itself records the module's path and signature, not its bytes).

This is distinct from #1487 / #1699, which added embedded source-file support (reading source text out of a portable PDB that is already loaded). The embedded PDB-loading half of #1487 — materializing the portable PDB from the PE debug directory in the first place — was never implemented.

Repro

  1. Build a small managed app with <DebugType>embedded</DebugType> (no standalone .pdb produced).
  2. Capture a .nettrace (EventPipe) or .etl CPU trace of it.
  3. Open via TraceLog, resolve a managed frame with TraceCodeAddress.GetSourceLine(symbolReader).

Expected: the frame resolves to file:line.

Actual: no source line, because SymbolReader.FindSymbolFilePath / TraceCodeAddresses.OpenPdbForModuleFile only probe for a standalone .pdb (next to the DLL, on the SymbolPath, or on a symbol server) and never inspect the module's debug directory for an EmbeddedPortablePdb entry. A code search of microsoft/perfview for EmbeddedPortablePdb returns zero hits; PEFile's debug-directory scan (GetPdbSignature / IMAGE_DEBUG_TYPE) only recognizes the CODEVIEW (RSDS) entry, not the EmbeddedPortablePdb entry (type 17).

Proposed change

Teach the trace symbol-resolution path to read an embedded portable PDB directly from the on-disk module before falling back to the standalone-file probe. The natural hook is TraceCodeAddresses.OpenPdbForModuleFile (a private method in TraceLog.cs), which has the module's FilePath and already gates on-disk reads with the existing TraceModuleUnchanged checksum check; SymbolReader.OpenSymbolFile only ever sees a PDB path, so it is not the right entry point on its own:

  • When resolving a managed module (and after TraceModuleUnchanged confirms the on-disk DLL matches the trace), inspect its PE debug directory for an EmbeddedPortablePdb entry. This requires extending PEFile's debug-directory scan to recognize the EmbeddedPortablePdb entry (type 17) — TraceEvent today only reads the separate CODEVIEW (RSDS) entry for PdbName / PdbSignature / PdbAge, so the embedded entry is not yet reachable.
  • If present, inflate the MPDB-framed (0x4244504D) deflate blob in memory and hand the resulting portable-PDB bytes to the existing PortableSymbolModule(SymbolReader, Stream) constructor, skipping the file-system / symbol-server probe. System.Reflection.Metadata (already referenced) exposes PEReader.ReadEmbeddedPortablePdbDebugDirectoryData, so the MPDB/deflate handling need not be hand-rolled.
  • Because the PDB is read from the same on-disk DLL the trace points at, no separate GUID/age matching against a different copy is needed — which avoids the mismatch class of bugs that affects any "extract the PDB from a separately-built copy of the DLL" workaround.
  • The portable-PDB reader is cross-platform, so this would enable embedded-PDB source resolution on non-Windows as well, unlike the native .pdb path.

The embedded-source support added in #1699 already demonstrates TraceEvent will read embedded data out of a portable PDB once it has one in hand; this issue asks for the step that gets that PDB in hand.

Workaround (for reference)

Consumers can re-materialize the embedded PDB to a temp standalone .pdb and append its directory to SymbolReader.SymbolPath, but this requires the same build of the DLL that produced the trace (GUID/age must match), which is fragile — e.g. BenchmarkDotNet builds an isolated copy with its own deterministic GUID and deletes it during artifact cleanup. An in-reader solution that reads the embedded PDB straight from the on-disk DLL removes the separate extract-and-match step whenever that DLL is still present. Note it does not rescue the case where the producing DLL has already been deleted (e.g. BenchmarkDotNet's artifact cleanup) — the embedded PDB lives in that same file — but it does eliminate the GUID/age mismatch risk for the common case where the module is on disk but ships no standalone PDB.

Context

  • Follows up #1487 (which asked "can it also find embedded pdb's?" — answered "would need to test / investigation needed" and then closed via Support embedded source files #1699 covering only embedded source).
  • Happy to prototype the in-memory embedded-PDB path and open a PR if the approach sounds reasonable.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions