microsoft / microsoft/perfview
TraceEvent `SymbolReader` does not resolve source lines from an embedded portable PDB
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
- Build a small managed app with
<DebugType>embedded</DebugType>(no standalone.pdbproduced). - Capture a
.nettrace(EventPipe) or.etlCPU trace of it. - Open via
TraceLog, resolve a managed frame withTraceCodeAddress.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
TraceModuleUnchangedconfirms the on-disk DLL matches the trace), inspect its PE debug directory for anEmbeddedPortablePdbentry. This requires extendingPEFile's debug-directory scan to recognize theEmbeddedPortablePdbentry (type 17) — TraceEvent today only reads the separateCODEVIEW(RSDS) entry forPdbName/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 existingPortableSymbolModule(SymbolReader, Stream)constructor, skipping the file-system / symbol-server probe.System.Reflection.Metadata(already referenced) exposesPEReader.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
.pdbpath.
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
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- 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