From 9e0a32e1a78947638786989b6a0b7d3619dcf99e Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Fri, 15 Jul 2022 15:48:31 +0200 Subject: [PATCH 1/2] update and sync TraceEvent version --- .../BenchmarkDotNet.Diagnostics.Windows.csproj | 2 +- src/BenchmarkDotNet/BenchmarkDotNet.csproj | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/BenchmarkDotNet.Diagnostics.Windows/BenchmarkDotNet.Diagnostics.Windows.csproj b/src/BenchmarkDotNet.Diagnostics.Windows/BenchmarkDotNet.Diagnostics.Windows.csproj index d830a8ebcf..6503c7fa7b 100644 --- a/src/BenchmarkDotNet.Diagnostics.Windows/BenchmarkDotNet.Diagnostics.Windows.csproj +++ b/src/BenchmarkDotNet.Diagnostics.Windows/BenchmarkDotNet.Diagnostics.Windows.csproj @@ -12,6 +12,6 @@ - + diff --git a/src/BenchmarkDotNet/BenchmarkDotNet.csproj b/src/BenchmarkDotNet/BenchmarkDotNet.csproj index efd2054eb4..c99c0a43ff 100644 --- a/src/BenchmarkDotNet/BenchmarkDotNet.csproj +++ b/src/BenchmarkDotNet/BenchmarkDotNet.csproj @@ -24,7 +24,7 @@ - + From 4f5cae80a504d23879220d9c6ce78b7b4adefe89 Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Fri, 15 Jul 2022 17:48:58 +0200 Subject: [PATCH 2/2] port the Disassembler from ClrMD 1.x to 2.x --- .gitignore | 2 +- .../BenchmarkDotNet.Samples.csproj | 2 +- ...Disassembler.cs => ClrMdV1Disassembler.cs} | 3 +- .../Program.cs | 2 +- .../BenchmarkDotNet.Disassembler.x86.csproj | 2 +- src/BenchmarkDotNet/BenchmarkDotNet.csproj | 5 +- .../Disassemblers/ClrMdV2Disassembler.cs | 265 ++++++++++++++++++ .../Exporters/DisassemblyPrettifier.cs | 4 +- .../Disassemblers/LinuxDisassembler.cs | 2 +- .../Disassemblers/SourceCodeProvider.cs | 183 ++++++++++++ .../BenchmarkDotNet.IntegrationTests.csproj | 2 +- .../BenchmarkDotNet.Tests.csproj | 2 +- 12 files changed, 460 insertions(+), 14 deletions(-) rename src/BenchmarkDotNet.Disassembler.x64/{ClrMdDisassembler.cs => ClrMdV1Disassembler.cs} (98%) create mode 100644 src/BenchmarkDotNet/Disassemblers/ClrMdV2Disassembler.cs create mode 100644 src/BenchmarkDotNet/Disassemblers/SourceCodeProvider.cs diff --git a/.gitignore b/.gitignore index c2591a37cc..96012efb78 100644 --- a/.gitignore +++ b/.gitignore @@ -48,7 +48,7 @@ tests/output/* artifacts/* BDN.Generated BenchmarkDotNet.Samples/Properties/launchSettings.json -src/BenchmarkDotNet/Disassemblers/net461/* +src/BenchmarkDotNet/Disassemblers/net462/* src/BenchmarkDotNet/Disassemblers/BenchmarkDotNet.Disassembler.*.nupkg # Visual Studio 2015 cache/options directory diff --git a/samples/BenchmarkDotNet.Samples/BenchmarkDotNet.Samples.csproj b/samples/BenchmarkDotNet.Samples/BenchmarkDotNet.Samples.csproj index e3cd86696b..36f094bbbe 100644 --- a/samples/BenchmarkDotNet.Samples/BenchmarkDotNet.Samples.csproj +++ b/samples/BenchmarkDotNet.Samples/BenchmarkDotNet.Samples.csproj @@ -18,7 +18,7 @@ - + diff --git a/src/BenchmarkDotNet.Disassembler.x64/ClrMdDisassembler.cs b/src/BenchmarkDotNet.Disassembler.x64/ClrMdV1Disassembler.cs similarity index 98% rename from src/BenchmarkDotNet.Disassembler.x64/ClrMdDisassembler.cs rename to src/BenchmarkDotNet.Disassembler.x64/ClrMdV1Disassembler.cs index ed16c22c6e..281ffd1c69 100644 --- a/src/BenchmarkDotNet.Disassembler.x64/ClrMdDisassembler.cs +++ b/src/BenchmarkDotNet.Disassembler.x64/ClrMdV1Disassembler.cs @@ -7,7 +7,8 @@ namespace BenchmarkDotNet.Disassemblers { - internal static class ClrMdDisassembler + // This Disassembler uses ClrMd v1x. Please keep it in sync with ClrMdV2Disassembler (if possible). + internal static class ClrMdV1Disassembler { internal static DisassemblyResult AttachAndDisassemble(Settings settings) { diff --git a/src/BenchmarkDotNet.Disassembler.x64/Program.cs b/src/BenchmarkDotNet.Disassembler.x64/Program.cs index aeb649ff25..701c4764bc 100644 --- a/src/BenchmarkDotNet.Disassembler.x64/Program.cs +++ b/src/BenchmarkDotNet.Disassembler.x64/Program.cs @@ -24,7 +24,7 @@ public static void Main(string[] args) try { - var methodsToExport = ClrMdDisassembler.AttachAndDisassemble(options); + var methodsToExport = ClrMdV1Disassembler.AttachAndDisassemble(options); SaveToFile(methodsToExport, options.ResultsPath); } diff --git a/src/BenchmarkDotNet.Disassembler.x86/BenchmarkDotNet.Disassembler.x86.csproj b/src/BenchmarkDotNet.Disassembler.x86/BenchmarkDotNet.Disassembler.x86.csproj index 050237c101..983c78346f 100644 --- a/src/BenchmarkDotNet.Disassembler.x86/BenchmarkDotNet.Disassembler.x86.csproj +++ b/src/BenchmarkDotNet.Disassembler.x86/BenchmarkDotNet.Disassembler.x86.csproj @@ -15,7 +15,7 @@ - + diff --git a/src/BenchmarkDotNet/BenchmarkDotNet.csproj b/src/BenchmarkDotNet/BenchmarkDotNet.csproj index c99c0a43ff..f7b38bf4aa 100644 --- a/src/BenchmarkDotNet/BenchmarkDotNet.csproj +++ b/src/BenchmarkDotNet/BenchmarkDotNet.csproj @@ -16,14 +16,13 @@ - + - @@ -47,7 +46,5 @@ - - diff --git a/src/BenchmarkDotNet/Disassemblers/ClrMdV2Disassembler.cs b/src/BenchmarkDotNet/Disassemblers/ClrMdV2Disassembler.cs new file mode 100644 index 0000000000..908fd43050 --- /dev/null +++ b/src/BenchmarkDotNet/Disassemblers/ClrMdV2Disassembler.cs @@ -0,0 +1,265 @@ +using Iced.Intel; +using Microsoft.Diagnostics.Runtime; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace BenchmarkDotNet.Disassemblers +{ + // This Disassembler uses ClrMd v2x. Please keep it in sync with ClrMdV1Disassembler (if possible). + internal static class ClrMdV2Disassembler + { + internal static DisassemblyResult AttachAndDisassemble(Settings settings) + { + using (var dataTarget = DataTarget.AttachToProcess( + settings.ProcessId, + suspend: false)) + { + var runtime = dataTarget.ClrVersions.Single().CreateRuntime(); + + ConfigureSymbols(dataTarget); + + var state = new State(runtime); + + var typeWithBenchmark = state.Runtime.EnumerateModules().Select(module => module.GetTypeByName(settings.TypeName)).First(type => type != null); + + state.Todo.Enqueue( + new MethodInfo( + // the Disassembler Entry Method is always parameterless, so check by name is enough + typeWithBenchmark.Methods.Single(method => method.IsPublic && method.Name == settings.MethodName), + 0)); + + var disassembledMethods = Disassemble(settings, state); + + // we don't want to export the disassembler entry point method which is just an artificial method added to get generic types working + var filteredMethods = disassembledMethods.Length == 1 + ? disassembledMethods // if there is only one method we want to return it (most probably benchmark got inlined) + : disassembledMethods.Where(method => !method.Name.Contains(DisassemblerConstants.DisassemblerEntryMethodName)).ToArray(); + + return new DisassemblyResult + { + Methods = filteredMethods, + SerializedAddressToNameMapping = state.AddressToNameMapping.Select(x => new DisassemblyResult.MutablePair { Key = x.Key, Value = x.Value }).ToArray(), + PointerSize = (uint)IntPtr.Size + }; + } + } + + private static void ConfigureSymbols(DataTarget dataTarget) + { + // code copied from https://github.com/Microsoft/clrmd/issues/34#issuecomment-161926535 + dataTarget.SetSymbolPath("http://msdl.microsoft.com/download/symbols"); + } + + private static DisassembledMethod[] Disassemble(Settings settings, State state) + { + var result = new List(); + + while (state.Todo.Count != 0) + { + var methodInfo = state.Todo.Dequeue(); + + if (!state.HandledMethods.Add(methodInfo.Method)) // add it now to avoid StackOverflow for recursive methods + continue; // already handled + + if (settings.MaxDepth >= methodInfo.Depth) + result.Add(DisassembleMethod(methodInfo, state, settings)); + } + + return result.ToArray(); + } + + private static DisassembledMethod DisassembleMethod(MethodInfo methodInfo, State state, Settings settings) + { + var method = methodInfo.Method; + + if (method.ILOffsetMap.Length == 0 && (method.HotColdInfo.HotStart == 0 || method.HotColdInfo.HotSize == 0)) + { + if (method.IsPInvoke) + return CreateEmpty(method, "PInvoke method"); + if (method.IL is null || method.IL.Length == 0) + return CreateEmpty(method, "Extern method"); + if (method.CompilationType == MethodCompilationType.None) + return CreateEmpty(method, "Method was not JITted yet."); + + return CreateEmpty(method, $"No valid {nameof(method.ILOffsetMap)} and {nameof(method.HotColdInfo)}"); + } + + var codes = new List(); + if (settings.PrintSource && method.ILOffsetMap.Length > 0) + { + // we use HashSet to prevent from duplicates + var uniqueSourceCodeLines = new HashSet(new SharpComparer()); + // for getting C# code we always use the original ILOffsetMap + foreach (var map in method.ILOffsetMap.Where(map => map.StartAddress < map.EndAddress && map.ILOffset >= 0).OrderBy(map => map.StartAddress)) + foreach (var sharp in SourceCodeProvider.GetSource(method, map)) + uniqueSourceCodeLines.Add(sharp); + + codes.AddRange(uniqueSourceCodeLines); + } + + // for getting ASM we try to use data from HotColdInfo if available (better for decoding) + foreach (var map in GetCompleteNativeMap(method)) + codes.AddRange(Decode(map.StartAddress, (uint)(map.EndAddress - map.StartAddress), state, methodInfo.Depth, method)); + + Map[] maps = settings.PrintSource + ? codes.GroupBy(code => code.InstructionPointer).OrderBy(group => group.Key).Select(group => new Map() { SourceCodes = group.ToArray() }).ToArray() + : new[] { new Map() { SourceCodes = codes.ToArray() } }; + + return new DisassembledMethod + { + Maps = maps, + Name = method.Signature, + NativeCode = method.NativeCode + }; + } + + private static IEnumerable Decode(ulong startAddress, uint size, State state, int depth, ClrMethod currentMethod) + { + byte[] code = new byte[size]; + int bytesRead = state.Runtime.DataTarget.DataReader.Read(startAddress, code); + if (bytesRead == 0 || bytesRead != size) + yield break; + + var reader = new ByteArrayCodeReader(code, 0, bytesRead); + var decoder = Decoder.Create(state.Runtime.DataTarget.DataReader.PointerSize * 8, reader); + decoder.IP = startAddress; + + while (reader.CanReadByte) + { + decoder.Decode(out var instruction); + + TryTranslateAddressToName(instruction, state, depth, currentMethod); + + yield return new Asm + { + InstructionPointer = instruction.IP, + Instruction = instruction + }; + } + } + + private static void TryTranslateAddressToName(Instruction instruction, State state, int depth, ClrMethod currentMethod) + { + var runtime = state.Runtime; + + if (!TryGetReferencedAddress(instruction, (uint)runtime.DataTarget.DataReader.PointerSize, out ulong address)) + return; + + if (state.AddressToNameMapping.ContainsKey(address)) + return; + + var jitHelperFunctionName = runtime.GetJitHelperFunctionName(address); + if (!string.IsNullOrEmpty(jitHelperFunctionName)) + { + state.AddressToNameMapping.Add(address, jitHelperFunctionName); + return; + } + + var methodTableName = runtime.DacLibrary.SOSDacInterface.GetMethodTableName(address); + if (!string.IsNullOrEmpty(methodTableName)) + { + state.AddressToNameMapping.Add(address, $"MT_{methodTableName}"); + return; + } + + var methodDescriptor = runtime.GetMethodByHandle(address); + if (!(methodDescriptor is null)) + { + state.AddressToNameMapping.Add(address, $"MD_{methodDescriptor.Signature}"); + return; + } + + var method = runtime.GetMethodByInstructionPointer(address); + if (method is null && (address & ((uint)runtime.DataTarget.DataReader.PointerSize - 1)) == 0) + { + if (runtime.DataTarget.DataReader.ReadPointer(address, out ulong newAddress) && newAddress > ushort.MaxValue) + method = runtime.GetMethodByInstructionPointer(newAddress); + } + + if (method is null) + return; + + if (method.NativeCode == currentMethod.NativeCode && method.Signature == currentMethod.Signature) + return; // in case of a call which is just a jump within the method or a recursive call + + if (!state.HandledMethods.Contains(method)) + state.Todo.Enqueue(new MethodInfo(method, depth + 1)); + + var methodName = method.Signature; + if (!methodName.Any(c => c == '.')) // the method name does not contain namespace and type name + methodName = $"{method.Type.Name}.{method.Signature}"; + state.AddressToNameMapping.Add(address, methodName); + } + + internal static bool TryGetReferencedAddress(Instruction instruction, uint pointerSize, out ulong referencedAddress) + { + for (int i = 0; i < instruction.OpCount; i++) + { + switch (instruction.GetOpKind(i)) + { + case OpKind.NearBranch16: + case OpKind.NearBranch32: + case OpKind.NearBranch64: + referencedAddress = instruction.NearBranchTarget; + return referencedAddress > ushort.MaxValue; + case OpKind.Immediate16: + case OpKind.Immediate8to16: + case OpKind.Immediate8to32: + case OpKind.Immediate8to64: + case OpKind.Immediate32to64: + case OpKind.Immediate32 when pointerSize == 4: + case OpKind.Immediate64: + referencedAddress = instruction.GetImmediate(i); + return referencedAddress > ushort.MaxValue; + case OpKind.Memory when instruction.IsIPRelativeMemoryOperand: + referencedAddress = instruction.IPRelativeMemoryAddress; + return referencedAddress > ushort.MaxValue; + case OpKind.Memory: + referencedAddress = instruction.MemoryDisplacement64; + return referencedAddress > ushort.MaxValue; + } + } + + referencedAddress = default; + return false; + } + + private static ILToNativeMap[] GetCompleteNativeMap(ClrMethod method) + { + // it's better to use one single map rather than few small ones + // it's simply easier to get next instruction when decoding ;) + var hotColdInfo = method.HotColdInfo; + if (hotColdInfo.HotSize > 0 && hotColdInfo.HotStart > 0) + { + return hotColdInfo.ColdSize <= 0 + ? new[] { new ILToNativeMap() { StartAddress = hotColdInfo.HotStart, EndAddress = hotColdInfo.HotStart + hotColdInfo.HotSize, ILOffset = -1 } } + : new[] + { + new ILToNativeMap() { StartAddress = hotColdInfo.HotStart, EndAddress = hotColdInfo.HotStart + hotColdInfo.HotSize, ILOffset = -1 }, + new ILToNativeMap() { StartAddress = hotColdInfo.ColdStart, EndAddress = hotColdInfo.ColdStart + hotColdInfo.ColdSize, ILOffset = -1 } + }; + } + + return method.ILOffsetMap + .Where(map => map.StartAddress < map.EndAddress) // some maps have 0 length? + .OrderBy(map => map.StartAddress) // we need to print in the machine code order, not IL! #536 + .ToArray(); + } + + private static DisassembledMethod CreateEmpty(ClrMethod method, string reason) + => DisassembledMethod.Empty(method.Signature, method.NativeCode, reason); + + private class SharpComparer : IEqualityComparer + { + public bool Equals(Sharp x, Sharp y) + { + // sometimes some C# code lines are duplicated because the same line is the best match for multiple ILToNativeMaps + // we don't want to confuse the users, so this must also be removed + return x.FilePath == y.FilePath && x.LineNumber == y.LineNumber; + } + + public int GetHashCode(Sharp obj) => obj.FilePath.GetHashCode() ^ obj.LineNumber; + } + } +} diff --git a/src/BenchmarkDotNet/Disassemblers/Exporters/DisassemblyPrettifier.cs b/src/BenchmarkDotNet/Disassemblers/Exporters/DisassemblyPrettifier.cs index 15b62ea449..f9aaf47ef9 100644 --- a/src/BenchmarkDotNet/Disassemblers/Exporters/DisassemblyPrettifier.cs +++ b/src/BenchmarkDotNet/Disassemblers/Exporters/DisassemblyPrettifier.cs @@ -39,7 +39,7 @@ internal static IReadOnlyList Prettify(DisassembledMethod method, Disas // first of all, we search of referenced addresses (jump|calls) var referencedAddresses = new HashSet(); foreach (var asm in asmInstructions) - if (ClrMdDisassembler.TryGetReferencedAddress(asm.Instruction, disassemblyResult.PointerSize, out ulong referencedAddress)) + if (ClrMdV2Disassembler.TryGetReferencedAddress(asm.Instruction, disassemblyResult.PointerSize, out ulong referencedAddress)) referencedAddresses.Add(referencedAddress); // for every IP that is referenced, we emit a uinque label @@ -72,7 +72,7 @@ internal static IReadOnlyList Prettify(DisassembledMethod method, Disas prettified.Add(new Label(label)); } - if (ClrMdDisassembler.TryGetReferencedAddress(asm.Instruction, disassemblyResult.PointerSize, out ulong referencedAddress)) + if (ClrMdV2Disassembler.TryGetReferencedAddress(asm.Instruction, disassemblyResult.PointerSize, out ulong referencedAddress)) { // jump or a call within same method if (addressesToLabels.TryGetValue(referencedAddress, out string translated)) diff --git a/src/BenchmarkDotNet/Disassemblers/LinuxDisassembler.cs b/src/BenchmarkDotNet/Disassemblers/LinuxDisassembler.cs index ee6cdc6cb9..4072baebdf 100644 --- a/src/BenchmarkDotNet/Disassemblers/LinuxDisassembler.cs +++ b/src/BenchmarkDotNet/Disassemblers/LinuxDisassembler.cs @@ -9,7 +9,7 @@ internal class LinuxDisassembler internal LinuxDisassembler(DisassemblyDiagnoserConfig config) => this.config = config; internal DisassemblyResult Disassemble(DiagnoserActionParameters parameters) - => ClrMdDisassembler.AttachAndDisassemble(BuildDisassemblerSettings(parameters)); + => ClrMdV2Disassembler.AttachAndDisassemble(BuildDisassemblerSettings(parameters)); private Settings BuildDisassemblerSettings(DiagnoserActionParameters parameters) => new Settings( diff --git a/src/BenchmarkDotNet/Disassemblers/SourceCodeProvider.cs b/src/BenchmarkDotNet/Disassemblers/SourceCodeProvider.cs new file mode 100644 index 0000000000..55e31cf8b8 --- /dev/null +++ b/src/BenchmarkDotNet/Disassemblers/SourceCodeProvider.cs @@ -0,0 +1,183 @@ +using Microsoft.Diagnostics.Runtime; +using Microsoft.Diagnostics.Symbols; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; + +namespace BenchmarkDotNet.Disassemblers +{ + internal static class SourceCodeProvider + { + private static readonly Dictionary SourceFileCache = new Dictionary(); + private static readonly Dictionary SourceFilePathsCache = new Dictionary(); + + internal static IEnumerable GetSource(ClrMethod method, ILToNativeMap map) + { + var sourceLocation = method.GetSourceLocation(map.ILOffset); + if (sourceLocation == null) + yield break; + + for (int line = sourceLocation.LineNumber; line <= sourceLocation.LineNumberEnd; ++line) + { + var sourceLine = ReadSourceLine(sourceLocation.SourceFile, line); + if (sourceLine == null) + continue; + + var text = sourceLine + Environment.NewLine + + GetSmartPointer(sourceLine, + start: line == sourceLocation.LineNumber ? sourceLocation.ColumnNumber - 1 : default(int?), + end: line == sourceLocation.LineNumberEnd ? sourceLocation.ColumnNumberEnd - 1 : default(int?)); + + yield return new Sharp + { + Text = text, + InstructionPointer = map.StartAddress, + FilePath = GetFilePath(sourceLocation.SourceFile), + LineNumber = line + }; + } + } + + private static string GetFilePath(SourceFile sourceFile) + => SourceFilePathsCache.TryGetValue(sourceFile, out string filePath) ? filePath : sourceFile.Url; + + private static string ReadSourceLine(SourceFile file, int line) + { + if (!SourceFileCache.TryGetValue(file, out string[] contents)) + { + // GetSourceFile method returns path when file is stored on the same machine + // otherwise it downloads it from the Symbol Server and returns the source code ;) + string wholeFileOrJustPath = file.GetSourceFile(); + + if (string.IsNullOrEmpty(wholeFileOrJustPath)) + return null; + + if (File.Exists(wholeFileOrJustPath)) + { + contents = File.ReadAllLines(wholeFileOrJustPath); + SourceFilePathsCache.Add(file, wholeFileOrJustPath); + } + else + { + contents = wholeFileOrJustPath.Split(new string[] { Environment.NewLine }, StringSplitOptions.None); + } + + SourceFileCache.Add(file, contents); + } + + return line - 1 < contents.Length + ? contents[line - 1] + : null; // "nop" can have no corresponding c# code ;) + } + + private static string GetSmartPointer(string sourceLine, int? start, int? end) + { + Debug.Assert(start is null || start < sourceLine.Length); + Debug.Assert(end is null || end <= sourceLine.Length); + + var prefix = new char[end ?? sourceLine.Length]; + var index = 0; + + // write offset using whitespaces + while (index < (start ?? prefix.Length)) + { + prefix[index] = + sourceLine.Length > index && + sourceLine[index] == '\t' + ? '\t' + : ' '; + index++; + } + + // write smart pointer + while (index < prefix.Length) + { + prefix[index] = '^'; + index++; + } + + return new string(prefix); + } + } + + internal static class ClrSourceExtensions + { + // TODO Not sure we want this to be a shared dictionary, especially without + // any synchronization. Probably want to put this hanging off the Context + // somewhere, or inside SymbolCache. + private static readonly Dictionary s_pdbReaders = new Dictionary(); + private static readonly SymbolReader symbolReader = new SymbolReader(TextWriter.Null) { SymbolPath = SymbolPath.MicrosoftSymbolServerPath }; + + internal static SourceLocation GetSourceLocation(this ClrMethod method, int ilOffset) + { + var reader = GetReaderForMethod(method); + if (reader == null) + return null; + + return reader.SourceLocationForManagedCode((uint)method.MetadataToken, ilOffset); + } + + internal static SourceLocation GetSourceLocation(this ClrStackFrame frame) + { + var reader = GetReaderForMethod(frame.Method); + if (reader == null) + return null; + + return reader.SourceLocationForManagedCode((uint)frame.Method.MetadataToken, FindIlOffset(frame)); + } + + private static int FindIlOffset(ClrStackFrame frame) + { + ulong ip = frame.InstructionPointer; + int last = -1; + foreach (ILToNativeMap item in frame.Method.ILOffsetMap) + { + if (item.StartAddress > ip) + return last; + + if (ip <= item.EndAddress) + return item.ILOffset; + + last = item.ILOffset; + } + + return last; + } + + private static ManagedSymbolModule GetReaderForMethod(ClrMethod method) + { + ClrModule module = method?.Type?.Module; + PdbInfo info = module?.Pdb; + + ManagedSymbolModule reader = null; + if (info != null) + { + if (!s_pdbReaders.TryGetValue(info, out reader)) + { + string pdbPath = info.Path; + if (pdbPath != null) + { + try + { + reader = symbolReader.OpenSymbolFile(pdbPath); + } + catch (IOException) + { + // This will typically happen when trying to load information + // from public symbols, or symbol files generated by some weird + // compiler. We can ignore this, but there's no need to load + // this PDB anymore, so we will put null in the dictionary and + // be done with it. + reader = null; + } + } + + s_pdbReaders[info] = reader; + } + } + + return reader; + } + } +} diff --git a/tests/BenchmarkDotNet.IntegrationTests/BenchmarkDotNet.IntegrationTests.csproj b/tests/BenchmarkDotNet.IntegrationTests/BenchmarkDotNet.IntegrationTests.csproj index ad9101c924..88b6b6605d 100755 --- a/tests/BenchmarkDotNet.IntegrationTests/BenchmarkDotNet.IntegrationTests.csproj +++ b/tests/BenchmarkDotNet.IntegrationTests/BenchmarkDotNet.IntegrationTests.csproj @@ -31,7 +31,7 @@ - + diff --git a/tests/BenchmarkDotNet.Tests/BenchmarkDotNet.Tests.csproj b/tests/BenchmarkDotNet.Tests/BenchmarkDotNet.Tests.csproj index e0c05a6bf8..aa55557878 100755 --- a/tests/BenchmarkDotNet.Tests/BenchmarkDotNet.Tests.csproj +++ b/tests/BenchmarkDotNet.Tests/BenchmarkDotNet.Tests.csproj @@ -15,7 +15,7 @@ - +