diff --git a/DllImportGenerator/DllImportGenerator.sln b/DllImportGenerator/DllImportGenerator.sln index 502dd987bd65..f2925d3ba91a 100644 --- a/DllImportGenerator/DllImportGenerator.sln +++ b/DllImportGenerator/DllImportGenerator.sln @@ -15,12 +15,14 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "TestAssets", "TestAssets", EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NativeExports", "TestAssets\NativeExports\NativeExports.csproj", "{32FDA079-0E9F-4A36-ADA5-6593B67A54AC}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DllImportGenerator.IntegrationTests", "DllImportGenerator.IntegrationTests\DllImportGenerator.IntegrationTests.csproj", "{162C204A-ED59-4EF3-A5FA-E58CC06FAB4D}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DllImportGenerator.IntegrationTests", "DllImportGenerator.IntegrationTests\DllImportGenerator.IntegrationTests.csproj", "{162C204A-ED59-4EF3-A5FA-E58CC06FAB4D}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tools", "Tools", "{69D56AC9-232B-4E76-B6C1-33A7B06B6855}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PInvokeDump", "Tools\PInvokeDump\PInvokeDump.csproj", "{6FD4AF19-0CAA-413C-A2BD-C888AA2E8CFB}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Benchmarking", "Tools\Benchmarking\Benchmarking.csproj", "{927F1081-FFE6-4897-9030-D9023F7EE604}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -47,14 +49,18 @@ Global {32FDA079-0E9F-4A36-ADA5-6593B67A54AC}.Debug|Any CPU.Build.0 = Debug|Any CPU {32FDA079-0E9F-4A36-ADA5-6593B67A54AC}.Release|Any CPU.ActiveCfg = Release|Any CPU {32FDA079-0E9F-4A36-ADA5-6593B67A54AC}.Release|Any CPU.Build.0 = Release|Any CPU - {6FD4AF19-0CAA-413C-A2BD-C888AA2E8CFB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {6FD4AF19-0CAA-413C-A2BD-C888AA2E8CFB}.Debug|Any CPU.Build.0 = Debug|Any CPU - {6FD4AF19-0CAA-413C-A2BD-C888AA2E8CFB}.Release|Any CPU.ActiveCfg = Release|Any CPU - {6FD4AF19-0CAA-413C-A2BD-C888AA2E8CFB}.Release|Any CPU.Build.0 = Release|Any CPU {162C204A-ED59-4EF3-A5FA-E58CC06FAB4D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {162C204A-ED59-4EF3-A5FA-E58CC06FAB4D}.Debug|Any CPU.Build.0 = Debug|Any CPU {162C204A-ED59-4EF3-A5FA-E58CC06FAB4D}.Release|Any CPU.ActiveCfg = Release|Any CPU {162C204A-ED59-4EF3-A5FA-E58CC06FAB4D}.Release|Any CPU.Build.0 = Release|Any CPU + {6FD4AF19-0CAA-413C-A2BD-C888AA2E8CFB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6FD4AF19-0CAA-413C-A2BD-C888AA2E8CFB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6FD4AF19-0CAA-413C-A2BD-C888AA2E8CFB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6FD4AF19-0CAA-413C-A2BD-C888AA2E8CFB}.Release|Any CPU.Build.0 = Release|Any CPU + {927F1081-FFE6-4897-9030-D9023F7EE604}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {927F1081-FFE6-4897-9030-D9023F7EE604}.Debug|Any CPU.Build.0 = Debug|Any CPU + {927F1081-FFE6-4897-9030-D9023F7EE604}.Release|Any CPU.ActiveCfg = Release|Any CPU + {927F1081-FFE6-4897-9030-D9023F7EE604}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -62,6 +68,7 @@ Global GlobalSection(NestedProjects) = preSolution {32FDA079-0E9F-4A36-ADA5-6593B67A54AC} = {2CFB9A7A-4AAF-4B6A-8CC8-540F64C3B45F} {6FD4AF19-0CAA-413C-A2BD-C888AA2E8CFB} = {69D56AC9-232B-4E76-B6C1-33A7B06B6855} + {927F1081-FFE6-4897-9030-D9023F7EE604} = {69D56AC9-232B-4E76-B6C1-33A7B06B6855} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {5344B739-3A02-402A-8777-0D54DEC4F3BA} diff --git a/DllImportGenerator/Tools/Benchmarking/Benchmarking.csproj b/DllImportGenerator/Tools/Benchmarking/Benchmarking.csproj new file mode 100644 index 000000000000..6a9860dd79ea --- /dev/null +++ b/DllImportGenerator/Tools/Benchmarking/Benchmarking.csproj @@ -0,0 +1,12 @@ + + + + net5.0 + true + + + + + + + diff --git a/DllImportGenerator/Tools/Benchmarking/CodeGenEventListener.cs b/DllImportGenerator/Tools/Benchmarking/CodeGenEventListener.cs new file mode 100644 index 000000000000..7e80f5a01c67 --- /dev/null +++ b/DllImportGenerator/Tools/Benchmarking/CodeGenEventListener.cs @@ -0,0 +1,293 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.Tracing; +using System.IO; +using Iced.Intel; + +using Decoder = Iced.Intel.Decoder; + +namespace Benchmarking +{ + internal class CodeGenEventListener : EventListener + { + // See src/coreclr/src/vm/ClrEtwAll.man + private const int JitKeyword = 0x10; + private const int InteropKeyword = 0x2000; + + // Event IDs + private const int ILStubGeneratedId = 88; + private const int VerboseMethodLoadId = 143; + + private readonly Dictionary pinvokeInstances = new Dictionary(); + + /// + /// Event fired when code is generated for a method. + /// + public event EventHandler NewMethodCodeGen; + + protected override void OnEventSourceCreated(EventSource eventSource) + { + if (eventSource.Name.Equals("Microsoft-Windows-DotNETRuntime")) + { + this.EnableEvents( + eventSource, + EventLevel.Verbose, + (EventKeywords)(JitKeyword | InteropKeyword)); + } + } + + protected override void OnEventWritten(EventWrittenEventArgs eventData) + { + switch (eventData.EventId) + { + case ILStubGeneratedId: this.OnILStubGenerated(eventData); break; + case VerboseMethodLoadId: this.OnVerboseMethodLoad(eventData); break; + } + } + + private void OnNewMethodCodeGen(MethodCodeGen mcg) + { + var h = this.NewMethodCodeGen; + if (h is not null) + { + h(this, mcg); + } + } + + private void OnILStubGenerated(EventWrittenEventArgs eventData) + { + Debug.Assert(eventData.EventId == ILStubGeneratedId); + + ulong methodId = 0; + string fqClassName = string.Empty; + string methodName = string.Empty; + string ilCode = string.Empty; + uint metadataToken = 0; + + // See https://docs.microsoft.com/dotnet/framework/performance/interop-etw-events + for (int i = 0; i < eventData.Payload.Count; i++) + { + object payload = eventData.Payload[i]; + string payloadName = eventData.PayloadNames[i]; + + if (payloadName == "StubMethodID") + { + methodId = (ulong)payload; + } + else if (payloadName == "ManagedInteropMethodNamespace") + { + fqClassName = (string)payload; + } + else if (payloadName == "ManagedInteropMethodName") + { + methodName = (string)payload; + } + else if (payloadName == "ManagedInteropMethodToken") + { + metadataToken = (uint)payload; + } + else if (payloadName == "StubMethodILCode") + { + ilCode = (string)payload; + } + } + + Debug.Assert(methodId != 0); + pinvokeInstances.Add(methodId, new PInvokeInstance() + { + MetadataToken = metadataToken, + FullyQualifiedClassName = fqClassName, + MethodName = methodName, + ILCode = ilCode, + });; + } + + private void OnVerboseMethodLoad(EventWrittenEventArgs eventData) + { + Debug.Assert(eventData.EventId == VerboseMethodLoadId); + + using var generatedCode = new StringWriter(); + + ulong methodId = 0; + ulong startAddress = 0; + uint size = 0; + string namespaceName = "?"; + string methodName = "?"; + string methodSignature = "?"; + string ilCode = string.Empty; + uint flags = 0; + uint metadataToken = 0; + + // See https://docs.microsoft.com/dotnet/framework/performance/method-etw-events + for (int i = 0; i < eventData.Payload.Count; i++) + { + object payload = eventData.Payload[i]; + string payloadName = eventData.PayloadNames[i]; + + if (payloadName == "MethodID") + { + methodId = (ulong)payload; + } + else if (payloadName == "MethodStartAddress") + { + startAddress = (ulong)payload; + } + else if (payloadName == "MethodSize") + { + size = (uint)payload; + } + else if (payloadName == "MethodNamespace") + { + namespaceName = (string)payload; + } + else if (payloadName == "MethodName") + { + methodName = (string)payload; + } + else if (payloadName == "MethodSignature") + { + methodSignature = (string)payload; + } + else if (payloadName == "MethodFlags") + { + flags = (uint)payload; + } + else if (payloadName == "MethodToken") + { + metadataToken = (uint)payload; + } + } + + bool isILStub = this.pinvokeInstances.TryGetValue(methodId, out PInvokeInstance pin); + if (isILStub) + { + Debug.Assert(metadataToken == 0); + metadataToken = pin.MetadataToken; + namespaceName = pin.FullyQualifiedClassName; + methodName = pin.MethodName; + ilCode = pin.ILCode; + } + + string flagString = ""; + + if ((flags & 0x8) != 0) + { + flagString += " jitted"; + } + else + { + flagString += " prejitted"; + } + + uint optLevel = ((flags >> 7) & 0x7); + + switch (optLevel) + { + case 1: flagString += " minopts"; break; + case 2: flagString += " fullopts"; break; + case 3: flagString += " tier0"; break; + case 4: flagString += " tier1"; break; + case 5: flagString += " tier1-OSR"; break; + default: flagString += " unknown codegen"; break; + } + + generatedCode.WriteLine($"{namespaceName}{Type.Delimiter}{methodName}{(isILStub ? " (ILStub)" : string.Empty)}"); + generatedCode.WriteLine(methodSignature); + generatedCode.WriteLine($"0x{startAddress:X16} {size:D6}{flagString,-20}"); + + unsafe + { + var codeReader = new MemoryCodeReader((byte*)startAddress, size); + var decoder = Decoder.Create(IntPtr.Size * 8, codeReader); + decoder.IP = startAddress; + ulong endRip = startAddress + (ulong)size; + const int HEXBYTES_COLUMN_BYTE_LENGTH = 10; + var instructions = new InstructionList(); + + while (decoder.IP < endRip) + { + // The method allocates an uninitialized element at the end of the list and + // returns a reference to it which is initialized by Decode(). + decoder.Decode(out instructions.AllocUninitializedElement()); + } + + // Formatters: Masm*, Nasm*, Gas* (AT&T) and Intel* (XED) + var formatter = new NasmFormatter(); + formatter.Options.DigitSeparator = "`"; + formatter.Options.FirstOperandCharIndex = 10; + var output = new StringOutput(); + // Use InstructionList's ref iterator (C# 7.3) to prevent copying 32 bytes every iteration + foreach (ref var instr in instructions) + { + // Don't use instr.ToString(), it allocates more, uses masm syntax and default options + // Console.WriteLine(instr); + formatter.Format(instr, output); + int instrLen = instr.Length; + int byteBaseIndex = (int)(instr.IP - startAddress); + for (int i = 0; i < instrLen; i++) + { + generatedCode.Write(codeReader[byteBaseIndex + i].ToString("X2")); + } + + int missingBytes = HEXBYTES_COLUMN_BYTE_LENGTH - instrLen; + for (int i = 0; i < missingBytes; i++) + { + generatedCode.Write(" "); + } + + generatedCode.Write(' '); + generatedCode.WriteLine(output.ToStringAndReset()); + } + } + + this.OnNewMethodCodeGen(new MethodCodeGen() + { + MetadataToken = metadataToken, + IsILStub = isILStub, + FullyQualifiedClassName = namespaceName, + MethodName = methodName, + GeneratedCode = generatedCode.ToString(), + GeneratedCodeSize = size, + ILCode = ilCode, + }); + } + + private record PInvokeInstance + { + public uint MetadataToken { get; init; } + public string FullyQualifiedClassName { get; init; } + public string MethodName { get; init; } + public string ILCode { get; init; } + } + + private unsafe class MemoryCodeReader : CodeReader + { + private readonly byte* baseAddress; + private readonly uint length; + private uint next = 0; + + public MemoryCodeReader(byte* baseAddress, uint length) + { + Debug.Assert(baseAddress is not null); + this.baseAddress = baseAddress; + this.length = length; + } + + public bool CanReadByte() => next < this.length; + + public override int ReadByte() + { + if (next >= length) + { + return -1; + } + + return baseAddress[next++]; + } + + public byte this[int i] => baseAddress[i]; + } + + } +} diff --git a/DllImportGenerator/Tools/Benchmarking/CodeGenMonitor.cs b/DllImportGenerator/Tools/Benchmarking/CodeGenMonitor.cs new file mode 100644 index 000000000000..61c9e255c457 --- /dev/null +++ b/DllImportGenerator/Tools/Benchmarking/CodeGenMonitor.cs @@ -0,0 +1,172 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; + +using Benchmarking.Visualizer; + +namespace Benchmarking +{ + /// + /// Data about the method and its generated code. + /// + public record MethodCodeGen + { + public uint MetadataToken { get; init; } + public bool IsILStub { get; init; } + public string FullyQualifiedClassName { get; init; } + public string MethodName { get; init; } + public string GeneratedCode { get; init; } + public uint GeneratedCodeSize { get; init; } + public string ILCode { get; init; } + } + + /// + /// Class used to monitor code generation in the CLR + /// + public sealed class CodeGenMonitor : IDisposable + { + public const int MetadataTokenNil = 0; + + private readonly CodeGenEventListener listener; + private readonly int retryDelayInMs; + private readonly HtmlCompareTwoPane htmlCompare; + + private bool isDisposed = false; + + private Dictionary> methods = new Dictionary>(StringComparer.OrdinalIgnoreCase); + + /// + /// Initialize a instance. + /// + /// Time delay (ms) to use when requesting a function name that isn't immediately known + public CodeGenMonitor(int retryDelayInMs = 20) + { + this.listener = new CodeGenEventListener(); + this.listener.NewMethodCodeGen += NewMethodCodeGen; + this.retryDelayInMs = retryDelayInMs; + + this.htmlCompare = new HtmlCompareTwoPane() + { + Title = "Compare generated code", + Pane1Title = "Code Gen 1", + Pane2Title = "Code Gen 2", + SelectionTitle = "All methods", + }; + } + + /// + /// Asynchronously get the generated code for the supplied method. + /// + /// Fully qualified method name + /// [Optional] method's metadata token - used to clarify overloads. + /// Number of times to retry if method isn't immediately found + /// A instance + public async Task GetLastCodeGenForAsync(string fullyQualifiedMethodName, int metadataToken = MetadataTokenNil, int retryCount = 5) + { + for (int i = 0; i < retryCount; ++i) + { + lock (this.methods) + { + if (this.methods.TryGetValue(fullyQualifiedMethodName, out List mcgs)) + { + var ret = mcgs.Last(); + if (metadataToken != MetadataTokenNil) + { + // Return the last matching element in the collection + var lastMaybe = mcgs.FindLast((m) => m.MetadataToken == (uint)metadataToken); + if (lastMaybe is not null) + { + ret = lastMaybe; + } + } + + return ret; + } + } + + await Task.Delay(this.retryDelayInMs); + } + + return null; + } + + /// + /// Generate a static HTML page to compare all collected data. + /// + /// Regular expression to filter through all collected data. + /// Time delay to wait prior to accessing current state + /// Static HTML code to view in browser + public string GenerateHtml(string pattern = "", int waitForMonitorInMs = 2_000) + { + if (waitForMonitorInMs > 0) + { + Thread.Sleep(waitForMonitorInMs); + } + + lock (this.methods) + { + return this.htmlCompare.Generate(this.FilterTo_Unsafe(pattern)); + } + } + + private IEnumerable<(string Name, string Content)> FilterTo_Unsafe(string pattern) + { + var regEx = new Regex(pattern); + foreach (var mcgs in this.methods) + { + if (!string.IsNullOrEmpty(pattern) + && !regEx.IsMatch(mcgs.Key)) + { + continue; + } + + if (mcgs.Value.Count == 1) + { + var mcg = mcgs.Value.Last(); + yield return new(mcgs.Key, mcg.GeneratedCode + Environment.NewLine + mcg.ILCode); + } + else + { + int i = 1; + foreach (var mcg in mcgs.Value) + { + yield return new(mcgs.Key + $" ({i})", mcg.GeneratedCode + Environment.NewLine + mcg.ILCode); + ++i; + } + } + } + } + + private void NewMethodCodeGen(object sender, MethodCodeGen e) + { + lock (this.methods) + { + string key = e.FullyQualifiedClassName + Type.Delimiter + e.MethodName; + if (!methods.TryGetValue(key, out List inst)) + { + methods.Add(key, new List() { e }); + } + else + { + inst.Add(e); + } + } + } + + public void Dispose() + { + if (this.isDisposed) + { + return; + } + + this.listener.NewMethodCodeGen -= NewMethodCodeGen; + this.listener.Dispose(); + + this.isDisposed = true; + } + } +} diff --git a/DllImportGenerator/Tools/Benchmarking/Visualizer/HtmlCompareTwoPane.cs b/DllImportGenerator/Tools/Benchmarking/Visualizer/HtmlCompareTwoPane.cs new file mode 100644 index 000000000000..c0171a59b178 --- /dev/null +++ b/DllImportGenerator/Tools/Benchmarking/Visualizer/HtmlCompareTwoPane.cs @@ -0,0 +1,181 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Web; + +namespace Benchmarking.Visualizer +{ + internal class HtmlCompareTwoPane + { + public string Title { get; init; } + public string SelectionTitle { get; init; } + public string Pane1Title { get; init; } + public string Pane2Title { get; init; } + + private const string DefaultStyle = +@" + * { box-sizing: border-box; } + html { + overflow: hidden; + } + .row { + display: flex; + } + .column { + float: left; + padding: 10px; + } + .left { + width: 20%; + height: 100vh; + } + .middle, .right { + width: 40%; + height: 100vh; + border: black; + border-width: 1px; + } + .constrainedDiv { + height: 80%; + overflow: auto; + } + .optionNameDiv { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + .comparePane { + width: 100%; + font-family: 'Courier New', Courier, monospace; + font-size: medium; + white-space: pre; + resize: none; + } + .paneTgtDiv button { + margin: 1pt; + } + #filterInput { + background-position: 10px 12px; + background-repeat: no-repeat; + width: 100%; + font-size: 16px; + padding: 12px 20px 12px 5px; + border: 1px solid #ddd; + margin-bottom: 12px; + } + #allOptions { + list-style-type: none; + padding: 0; + margin: 0; + } + #allOptions li > div { + border: 1px solid #ddd; + margin-top: 1px; + background-color: #f6f6f6; + padding: 12px; + text-decoration: none; + font-size: 18px; + color: black; + } + #allOptions li div:hover:not(.header) { + background-color: #eee; + } +"; + + public string Generate(IEnumerable<(string Name, string Content)> options) + { + var optionEntries = new StringBuilder(); + var jsonData = new StringBuilder(); + + int i = 0; + foreach (var opt in options) + { + optionEntries.AppendLine( +@$"
  • +
    {opt.Name}
    +
    +
  • "); + // Escape the input twice since it is being embedded in a string as a JSON object. + jsonData.AppendLine($"\"{{ \\\"content\\\": \\\"{HttpUtility.JavaScriptStringEncode(HttpUtility.JavaScriptStringEncode(opt.Content))}\\\" }}\","); + + ++i; + } + + return +@$" + + + + {this.Title} + + + + + + + + +
    +
    +

    {this.SelectionTitle}

    + +
    +
      +{optionEntries} +
    +
    +
    +
    +

    {this.Pane1Title}

    + +
    +
    +

    {this.Pane2Title}

    + +
    +
    + + +"; + } + } +}