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.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.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 efd2054eb4..f7b38bf4aa 100644
--- a/src/BenchmarkDotNet/BenchmarkDotNet.csproj
+++ b/src/BenchmarkDotNet/BenchmarkDotNet.csproj
@@ -16,15 +16,14 @@
-
+
-
-
+
@@ -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 @@
-
+