microsoft / microsoft/perfview

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

Open
#2,433 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
C#
Stars
4.7k
Forks
775
Avg merge
5d 11h
Merged PRs (30d)
9

Description

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 #1699 covering only embedded source).
  • Happy to prototype the in-memory embedded-PDB path and open a PR if the approach sounds reasonable.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start in TraceLog.cs at TraceCodeAddresses.OpenPdbForModuleFile and inspect PEFile's debug-directory scan, then review the existing PortableSymbolModule(SymbolReader, Stream) path and System.Reflection.Metadata's embedded-PDB reader. Done means the repro resolves managed frames to file and line from an embedded portable PDB while retaining the existing standalone-PDB fallback.

Written by the indexing model from the issue text.

Assessment

Tech stack
csharp
Domain
devtools, performance
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
58/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.