diff --git a/PerfView.sln b/PerfView.sln index 3da566c16..2007aade6 100644 --- a/PerfView.sln +++ b/PerfView.sln @@ -91,6 +91,14 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TraceParserGen.Tests", "src EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TraceEvent.Benchmarks", "src\TraceEvent\TraceEvent.Benchmarks\TraceEvent.Benchmarks.csproj", "{F2FBDEF0-4C45-44B2-8B92-9C5763BD2E69}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{827E0CD3-B72D-47B6-A68D-7590B98EB39B}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "TraceEvent", "TraceEvent", "{F2AE6042-2485-6774-F42B-98E120E28306}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "TraceEvent.Tests", "TraceEvent.Tests", "{282B72FF-D1CF-1C67-705A-D384E1729A5D}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EmbeddedPdbTestApp", "src\TraceEvent\TraceEvent.Tests\EmbeddedPdbTestApp\EmbeddedPdbTestApp.csproj", "{1CE6D3C2-5E27-4296-89FD-5177387F0B15}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -445,10 +453,27 @@ Global {F2FBDEF0-4C45-44B2-8B92-9C5763BD2E69}.Release|x64.Build.0 = Release|Any CPU {F2FBDEF0-4C45-44B2-8B92-9C5763BD2E69}.Release|x86.ActiveCfg = Release|Any CPU {F2FBDEF0-4C45-44B2-8B92-9C5763BD2E69}.Release|x86.Build.0 = Release|Any CPU + {1CE6D3C2-5E27-4296-89FD-5177387F0B15}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1CE6D3C2-5E27-4296-89FD-5177387F0B15}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1CE6D3C2-5E27-4296-89FD-5177387F0B15}.Debug|x64.ActiveCfg = Debug|Any CPU + {1CE6D3C2-5E27-4296-89FD-5177387F0B15}.Debug|x64.Build.0 = Debug|Any CPU + {1CE6D3C2-5E27-4296-89FD-5177387F0B15}.Debug|x86.ActiveCfg = Debug|Any CPU + {1CE6D3C2-5E27-4296-89FD-5177387F0B15}.Debug|x86.Build.0 = Debug|Any CPU + {1CE6D3C2-5E27-4296-89FD-5177387F0B15}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1CE6D3C2-5E27-4296-89FD-5177387F0B15}.Release|Any CPU.Build.0 = Release|Any CPU + {1CE6D3C2-5E27-4296-89FD-5177387F0B15}.Release|x64.ActiveCfg = Release|Any CPU + {1CE6D3C2-5E27-4296-89FD-5177387F0B15}.Release|x64.Build.0 = Release|Any CPU + {1CE6D3C2-5E27-4296-89FD-5177387F0B15}.Release|x86.ActiveCfg = Release|Any CPU + {1CE6D3C2-5E27-4296-89FD-5177387F0B15}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {F2AE6042-2485-6774-F42B-98E120E28306} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + {282B72FF-D1CF-1C67-705A-D384E1729A5D} = {F2AE6042-2485-6774-F42B-98E120E28306} + {1CE6D3C2-5E27-4296-89FD-5177387F0B15} = {282B72FF-D1CF-1C67-705A-D384E1729A5D} + EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {9F85A2A3-E0DF-4826-9BBA-4DFFA0F17150} EndGlobalSection diff --git a/src/TraceEvent/Symbols/PortableSymbolModule.cs b/src/TraceEvent/Symbols/PortableSymbolModule.cs index 09e54e0f9..30f8a8184 100644 --- a/src/TraceEvent/Symbols/PortableSymbolModule.cs +++ b/src/TraceEvent/Symbols/PortableSymbolModule.cs @@ -19,6 +19,19 @@ public PortableSymbolModule(SymbolReader reader, Stream stream, string pdbFileNa _metaData = _provider.GetMetadataReader(); } + /// + /// Creates a PortableSymbolModule from a MetadataReaderProvider that has already been + /// opened (e.g. an embedded portable PDB read out of a PE image via + /// PEReader.ReadEmbeddedPortablePdbDebugDirectoryData). The provider owns its own backing + /// memory, so it remains valid after the PEReader it came from has been disposed. This + /// PortableSymbolModule takes ownership of 'provider' and disposes it when disposed. + /// + internal PortableSymbolModule(SymbolReader reader, MetadataReaderProvider provider, string pdbFileName = "") : base(reader, pdbFileName) + { + _provider = provider; + _metaData = _provider.GetMetadataReader(); + } + public void Dispose() => _provider.Dispose(); public override Guid PdbGuid diff --git a/src/TraceEvent/Symbols/SymbolReader.cs b/src/TraceEvent/Symbols/SymbolReader.cs index b7ea16039..bd7ea5796 100644 --- a/src/TraceEvent/Symbols/SymbolReader.cs +++ b/src/TraceEvent/Symbols/SymbolReader.cs @@ -4,6 +4,8 @@ using System.IO; using System.Linq; using System.Net.Http; +using System.Reflection.Metadata; +using System.Reflection.PortableExecutable; using System.Runtime.InteropServices; using System.Security.Cryptography; using System.Text; @@ -711,6 +713,69 @@ public NativeSymbolModule OpenNativeSymbolFile(string pdbFileName) return OpenSymbolFile(pdbFileName) as NativeSymbolModule; } + /// + /// Attempts to open a portable PDB that is embedded inside the managed module image at + /// (modules built with <DebugType>embedded</DebugType>). + /// Returns a (a PortableSymbolModule) whose data comes + /// straight from the module's embedded debug directory, or null if + /// does not exist, is not a PE file, or has no embedded portable PDB. + /// + /// Because the PDB bytes are read from the same on-disk module, no separate GUID/age matching + /// against a standalone PDB file is needed. This is the embedded-PDB analog of + /// , which opens a standalone PDB file. + /// + /// The path to the managed module (.dll/.exe) that may contain an embedded portable PDB. + /// The symbol module for the embedded PDB, or null if none is present. + public ManagedSymbolModule OpenEmbeddedPortablePdb(string dllFilePath) + { + // Suffix the key so it cannot collide with the standalone-PDB cache entries (which are + // keyed by PDB file path), even when an embedded and a standalone PDB share the same module. + string cacheKey = dllFilePath + "|EmbeddedPdb"; + if (m_symbolModuleCache.TryGet(cacheKey, out ManagedSymbolModule ret)) + { + return ret; + } + + try + { + if (!File.Exists(dllFilePath)) + { + m_log.WriteLine("OpenEmbeddedPortablePdb: {0} does not exist.", dllFilePath); + return null; + } + + using (FileStream peStream = File.Open(dllFilePath, FileMode.Open, FileAccess.Read, FileShare.Read)) + using (PEReader peReader = new PEReader(peStream)) + { + foreach (DebugDirectoryEntry entry in peReader.ReadDebugDirectory()) + { + if (entry.Type == DebugDirectoryEntryType.EmbeddedPortablePdb) + { + // The returned provider owns its own (decompressed) backing memory, so it + // remains valid after the PEReader and FileStream are disposed below. + MetadataReaderProvider provider = peReader.ReadEmbeddedPortablePdbDebugDirectoryData(entry); + ret = new PortableSymbolModule(this, provider, dllFilePath); + m_log.WriteLine("OpenEmbeddedPortablePdb: Found embedded portable PDB in {0}", dllFilePath); + break; + } + } + + if (ret == null) + { + m_log.WriteLine("OpenEmbeddedPortablePdb: {0} has no embedded portable PDB.", dllFilePath); + } + } + } + catch (Exception e) + { + m_log.WriteLine("OpenEmbeddedPortablePdb: Failure reading {0}: {1}", dllFilePath, e.Message); + ret = null; + } + + m_symbolModuleCache.Add(cacheKey, ret); + return ret; + } + internal R2RPerfMapSymbolModule OpenR2RPerfMapSymbolFile(string filePath, uint loadedLayoutTextOffset) { return new R2RPerfMapSymbolModule(filePath, loadedLayoutTextOffset); diff --git a/src/TraceEvent/TraceEvent.Tests/EmbeddedPdbTestApp/EmbeddedPdbTestApp.csproj b/src/TraceEvent/TraceEvent.Tests/EmbeddedPdbTestApp/EmbeddedPdbTestApp.csproj new file mode 100644 index 000000000..6a7f498f5 --- /dev/null +++ b/src/TraceEvent/TraceEvent.Tests/EmbeddedPdbTestApp/EmbeddedPdbTestApp.csproj @@ -0,0 +1,44 @@ + + + + + + netstandard2.0 + EmbeddedPdbTestApp + EmbeddedPdbTestApp + + embedded + true + true + + false + false + false + true + + + + + true + true + true + ..\..\..\MSFT.snk + + + diff --git a/src/TraceEvent/TraceEvent.Tests/EmbeddedPdbTestApp/EmbeddedTarget.cs b/src/TraceEvent/TraceEvent.Tests/EmbeddedPdbTestApp/EmbeddedTarget.cs new file mode 100644 index 000000000..48d929691 --- /dev/null +++ b/src/TraceEvent/TraceEvent.Tests/EmbeddedPdbTestApp/EmbeddedTarget.cs @@ -0,0 +1,20 @@ +namespace EmbeddedPdbTestApp +{ + /// + /// Minimal target type for the embedded-portable-PDB SymbolReader test. The test resolves the + /// metadata token of via reflection (do not rely on a fixed token, since the + /// netstandard2.0 build synthesizes attribute-type constructors ahead of it in the MethodDef table). + /// + /// IMPORTANT: SymbolReaderTests asserts the source line of the first sequence point of + /// (IL offset 0, the first statement). If you move the body below, + /// update the expected line number in the test. + /// + public static class EmbeddedTarget + { + public static int Add(int a, int b) + { + int sum = a + b; // first sequence point (IL offset 0) is on this line + return sum; + } + } +} diff --git a/src/TraceEvent/TraceEvent.Tests/Symbols/SymbolReaderTests.cs b/src/TraceEvent/TraceEvent.Tests/Symbols/SymbolReaderTests.cs index bb0eec08b..21e4e9eb4 100644 --- a/src/TraceEvent/TraceEvent.Tests/Symbols/SymbolReaderTests.cs +++ b/src/TraceEvent/TraceEvent.Tests/Symbols/SymbolReaderTests.cs @@ -1,4 +1,8 @@ +using FastSerialization; using Microsoft.Diagnostics.Symbols; +using Microsoft.Diagnostics.Tracing; +using Microsoft.Diagnostics.Tracing.Etlx; +using Microsoft.Diagnostics.Tracing.EventPipe; using PerfView.TestUtilities; using System; using System.Collections.Generic; @@ -135,6 +139,145 @@ public void PortablePdbHasValidSourceInfo() } } + [Fact] + public void EmbeddedPortablePdbResolvesSourceLine() + { + string dllPath = EmbeddedPdbTestAppPath; + + ManagedSymbolModule pdbFile = _symbolReader.OpenEmbeddedPortablePdb(dllPath); + using (pdbFile as IDisposable) + { + Assert.NotNull(pdbFile); + Assert.IsType(pdbFile); + + // Resolve the metadata token for EmbeddedTarget.Add from the referenced fixture assembly + // rather than hard-coding it: on netstandard2.0 the compiler synthesizes attribute types + // (e.g. EmbeddedAttribute, RefSafetyRulesAttribute) whose constructors occupy the first + // MethodDef rows, so Add is not token 0x06000001. + uint addToken = (uint)typeof(EmbeddedPdbTestApp.EmbeddedTarget) + .GetMethod(nameof(EmbeddedPdbTestApp.EmbeddedTarget.Add)).MetadataToken; + + // Resolve the source location of the method's first sequence point (the one covering + // IL offset 0). + SourceLocation sourceLocation = pdbFile.SourceLocationForManagedCode(addToken, ilOffset: 0); + Assert.NotNull(sourceLocation); + + SourceFile sourceFile = sourceLocation.SourceFile; + Assert.NotNull(sourceFile); + Assert.EndsWith("EmbeddedTarget.cs", sourceFile.BuildTimeFilePath, StringComparison.OrdinalIgnoreCase); + + // The first sequence point of Add() (IL offset 0) maps to its first statement, + // "int sum = a + b;", on this line of EmbeddedTarget.cs. If the fixture source moves, + // update this expectation. + Assert.Equal(15, sourceLocation.LineNumber); + } + } + + [Fact] + public void EmbeddedPortablePdbReadableAfterPEReaderDisposed() + { + // OpenEmbeddedPortablePdb disposes the PEReader/FileStream it used before returning, so a + // successful source-line lookup here proves the MetadataReaderProvider owns its own backing + // memory and survives that disposal. (Note: the SymbolReader still owns the returned module; + // disposing the SymbolReader itself would clear its cache and dispose this module.) + uint addToken = (uint)typeof(EmbeddedPdbTestApp.EmbeddedTarget) + .GetMethod(nameof(EmbeddedPdbTestApp.EmbeddedTarget.Add)).MetadataToken; + + ManagedSymbolModule pdbFile = _symbolReader.OpenEmbeddedPortablePdb(EmbeddedPdbTestAppPath); + using (pdbFile as IDisposable) + { + Assert.NotNull(pdbFile); + SourceLocation sourceLocation = pdbFile.SourceLocationForManagedCode(addToken, ilOffset: 0); + Assert.NotNull(sourceLocation); + Assert.NotNull(sourceLocation.SourceFile); + } + } + + [Fact] + public void OpenEmbeddedPortablePdbCachesResult() + { + // A second open of the same module must return the cached instance (the embedded-PDB cache + // key cannot collide with a standalone-PDB path). Do not dispose between calls: the + // SymbolReader owns the cached module's lifetime. + ManagedSymbolModule first = _symbolReader.OpenEmbeddedPortablePdb(EmbeddedPdbTestAppPath); + ManagedSymbolModule second = _symbolReader.OpenEmbeddedPortablePdb(EmbeddedPdbTestAppPath); + + Assert.NotNull(first); + Assert.Same(first, second); + } + + [Fact] + public void OpenEmbeddedPortablePdbReturnsNullWhenNotEmbedded() + { + // The test assembly itself is built with a standalone (portable) PDB, not an embedded one, + // so there is no EmbeddedPortablePdb debug-directory entry to read. + string dllWithoutEmbeddedPdb = typeof(SymbolReaderTests).Assembly.Location; + Assert.True(File.Exists(dllWithoutEmbeddedPdb)); + + ManagedSymbolModule pdbFile = _symbolReader.OpenEmbeddedPortablePdb(dllWithoutEmbeddedPdb); + Assert.Null(pdbFile); + } + + [Fact] + public void OpenEmbeddedPortablePdbReturnsNullForMissingFile() + { + string missing = Path.Combine(Path.GetTempPath(), "DoesNotExist_" + Guid.NewGuid().ToString("N") + ".dll"); + ManagedSymbolModule pdbFile = _symbolReader.OpenEmbeddedPortablePdb(missing); + Assert.Null(pdbFile); + } + + [Fact] + public void TraceLogOpenPdbForModuleFileResolvesEmbeddedPdb() + { + // Exercises the trace symbol-resolution path end-to-end: TraceCodeAddresses.OpenPdbForModuleFile, + // given a managed module that (a) carries no PDB identity in the trace, (b) whose on-disk copy + // matches the trace (checksum + timestamp), and (c) has no standalone PDB next to it, must fall + // back to the module's embedded portable PDB. This is the only consumer of + // SymbolReader.OpenEmbeddedPortablePdb that the SymbolReaderTests above do not reach directly. + string dllPath = EmbeddedPdbTestAppPath; + + // Read the real PE checksum/timestamp from the fixture DLL so the TraceModuleUnchanged gate + // inside OpenPdbForModuleFile passes regardless of how/when the fixture was (re)built. + int imageChecksum; + int timeDateStamp; + using (var peFile = new PEFile.PEFile(dllPath)) + { + imageChecksum = (int)peFile.Header.CheckSum; + timeDateStamp = peFile.Header.TimeDateStampSec; + } + + uint addToken = (uint)typeof(EmbeddedPdbTestApp.EmbeddedTarget) + .GetMethod(nameof(EmbeddedPdbTestApp.EmbeddedTarget.Add)).MetadataToken; + + using (TraceLog traceLog = CreateEmptyInMemoryTraceLog()) + { + // Build a managed TraceModuleFile that points at the fixture DLL on disk. The internal + // constructor lower-cases the path, so restore the real-cased path afterwards because + // File.Exists (inside TraceModuleUnchanged) is case-sensitive on non-Windows. + TraceModuleFile moduleFile = new TraceModuleFile(dllPath, 0x10000, (ModuleFileIndex)0); + moduleFile.fileName = dllPath; + moduleFile.imageChecksum = imageChecksum; + moduleFile.timeDateStamp = timeDateStamp; + // Leave symbolInfo null so PdbSignature == Guid.Empty: with no recorded PDB identity, + // OpenPdbForModuleFile is forced down the on-disk-match -> embedded-PDB branch. + + ManagedSymbolModule pdbFile = traceLog.CodeAddresses.OpenPdbForModuleFile(_symbolReader, moduleFile); + using (pdbFile as IDisposable) + { + Assert.NotNull(pdbFile); + Assert.IsType(pdbFile); + + // OpenPdbForModuleFile stamps ExePath with the module it resolved symbols for. + Assert.Equal(dllPath, pdbFile.ExePath); + + SourceLocation sourceLocation = pdbFile.SourceLocationForManagedCode(addToken, ilOffset: 0); + Assert.NotNull(sourceLocation); + Assert.EndsWith("EmbeddedTarget.cs", sourceLocation.SourceFile.BuildTimeFilePath, StringComparison.OrdinalIgnoreCase); + Assert.Equal(15, sourceLocation.LineNumber); + } + } + } + [Fact] public void SourceLinkUrlsAreEscaped() { @@ -1402,6 +1545,45 @@ protected void PrepareTestData() protected string UnzippedSymbolReaderTestInputDir => Path.Combine(UnZippedDataDir, SymbolReaderTestInput); + /// + /// Path to the EmbeddedPdbTestApp fixture DLL (built with <DebugType>embedded</DebugType>) + /// that the TraceEvent.Tests project copies next to the test assembly. + /// + protected static string EmbeddedPdbTestAppPath + { + get + { + string path = Path.Combine(AppContext.BaseDirectory, "EmbeddedPdbTestApp.dll"); + Assert.True(File.Exists(path), "EmbeddedPdbTestApp.dll was not found next to the test assembly: " + path); + return path; + } + } + + /// + /// Builds a minimal, valid, empty entirely in memory (no ETW capture, so + /// it works cross-platform). Callers can then hand-populate s and + /// drive symbol-resolution APIs directly. + /// + private static TraceLog CreateEmptyInMemoryTraceLog() + { + // Generate a minimal in-memory nettrace stream with just enough metadata to be valid. + var writer = new EventPipeWriterV6(); + writer.WriteHeaders(); + writer.WriteMetadataBlock(new EventMetadata(1, "Microsoft-Windows-DotNETRuntime", "EventSource", 0)); + writer.WriteThreadBlock(w => w.WriteThreadEntry(1, 0, 0)); + writer.WriteEventBlock(w => w.WriteEventBlob(1, 1, 1, new byte[0])); + writer.WriteEndBlock(); + + using MemoryStream nettraceStream = new MemoryStream(writer.ToArray()); + TraceEventDispatcher eventSource = new EventPipeEventSource(nettraceStream); + + // Convert to in-memory ETLX and open it as a TraceLog (the caller owns disposal). + MemoryStream etlxStream = new MemoryStream(); + TraceLog.CreateFromEventPipeEventSources(eventSource, new IOStreamStreamWriter(etlxStream, SerializationSettings.Default, leaveOpen: true), null); + etlxStream.Position = 0; + return new TraceLog(etlxStream); + } + /// /// A handler for the in that /// can be used by unit tests to intercept requests to symbol server (for PDB diff --git a/src/TraceEvent/TraceEvent.Tests/TraceEvent.Tests.csproj b/src/TraceEvent/TraceEvent.Tests/TraceEvent.Tests.csproj index 0ffb5c455..589f6b8e9 100644 --- a/src/TraceEvent/TraceEvent.Tests/TraceEvent.Tests.csproj +++ b/src/TraceEvent/TraceEvent.Tests/TraceEvent.Tests.csproj @@ -32,6 +32,19 @@ + + + + + + + + + + diff --git a/src/TraceEvent/TraceLog.cs b/src/TraceEvent/TraceLog.cs index f397c03bb..dfbb35c97 100644 --- a/src/TraceEvent/TraceLog.cs +++ b/src/TraceEvent/TraceLog.cs @@ -9111,7 +9111,7 @@ private void LookupSymbolsForModule(SymbolReader reader, TraceModuleFile moduleF /// /// Look up the SymbolModule (open PDB) for a given moduleFile. Will generate NGEN pdbs as needed. /// - private unsafe ManagedSymbolModule OpenPdbForModuleFile(SymbolReader symReader, TraceModuleFile moduleFile) + internal unsafe ManagedSymbolModule OpenPdbForModuleFile(SymbolReader symReader, TraceModuleFile moduleFile) { string pdbFileName = null; // If we have a signature, use it @@ -9131,6 +9131,21 @@ private unsafe ManagedSymbolModule OpenPdbForModuleFile(SymbolReader symReader, if (TraceModuleUnchanged(moduleFile, symReader.m_log)) { pdbFileName = symReader.FindSymbolFilePathForModule(moduleFile.FilePath); + + // No standalone PDB found, but the on-disk module matches the trace. See if the + // module carries an embedded portable PDB (embedded) and use + // it directly. Because the bytes come from the matched-on-disk module, no GUID/age + // re-validation is required, so we return the module immediately. + if (pdbFileName == null) + { + ManagedSymbolModule embeddedSymbolModule = symReader.OpenEmbeddedPortablePdb(moduleFile.FilePath); + if (embeddedSymbolModule != null) + { + embeddedSymbolModule.ExePath = moduleFile.FilePath; + symReader.m_log.WriteLine("Opened embedded portable PDB for {0}", moduleFile.FilePath); + return embeddedSymbolModule; + } + } } }