Perfmaps IPC changes for DiagnosticClient (#3873)
authorDavid Mason <davmason@microsoft.com>
Sat, 17 Jun 2023 07:21:38 +0000 (00:21 -0700)
committerGitHub <noreply@github.com>
Sat, 17 Jun 2023 07:21:38 +0000 (07:21 +0000)
Diagnostic side changes for https://github.com/dotnet/runtime/pull/85801

documentation/design-docs/ipc-protocol.md
src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsClient/DiagnosticsClient.cs
src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsClient/PerfMapType.cs [new file with mode: 0644]
src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsIpc/IpcCommands.cs
src/tests/Microsoft.Diagnostics.NETCore.Client/DiagnosticsClientApiShim.cs
src/tests/Microsoft.Diagnostics.NETCore.Client/DiagnosticsClientApiShimExtensions.cs
src/tests/Microsoft.Diagnostics.NETCore.Client/PerfMapTests.cs [new file with mode: 0644]

index 3777067e0052621325090360c1f5bdd550c22a2c..60a268c760c3a30ae352b9b7b2a93a6dbd21e36e 100644 (file)
@@ -380,6 +380,8 @@ enum class ProcessCommandId : uint8_t
     ResumeRuntime      = 0x01,
     ProcessEnvironment = 0x02,
     ProcessInfo2       = 0x04,
+    EnablePerfMap      = 0x05,
+    DisablePerfMap     = 0x06,
     ApplyStartupHook   = 0x07
     // future
 }
@@ -846,6 +848,89 @@ struct Payload
 }
 ```
 
+### `EnablePerfMap`
+
+Command Code: `0x0405`
+
+The `EnablePerfMap` command instructs the runtime to start emitting perfmap or jitdump files for the process. These files are used by the perf tool to correlate jitted code addresses in a trace.
+
+In the event of an [error](#Errors), the runtime will attempt to send an error message and subsequently close the connection.
+
+#### Inputs:
+
+Header: `{ Magic; Size; 0x0405; 0x0000 }`
+
+Payload:
+* `uint32_t perfMapType`: the type of generation to enable
+
+#### Returns (as an IPC Message Payload):
+
+Header: `{ Magic; 28; 0xFF00; 0x0000; }`
+
+`EnablePerfMap` returns:
+* `int32 hresult`: The result of enabling the perfmap or jitdump files (`0` indicates success)
+
+##### Details:
+
+Inputs:
+```c++
+enum class PerfMapType
+{
+    DISABLED = 0,
+    ALL      = 1,
+    JITDUMP  = 2,
+    PERFMAP  = 3
+}
+
+struct Payload
+{
+    uint32_t perfMapType;
+}
+```
+
+Returns:
+```c
+Payload
+{
+    int32 hresult
+}
+```
+
+> Available since .NET 8.0
+
+### `DisablePerfMap`
+
+Command Code: `0x0406`
+
+The `DisablePerfMap` command instructs the runtime to stop emitting perfmap or jitdump files for the process. These files are used by the perf tool to correlate jitted code addresses in a trace.
+
+In the event of an [error](#Errors), the runtime will attempt to send an error message and subsequently close the connection.
+
+#### Inputs:
+
+Header: `{ Magic; Size; 0x0405; 0x0000 }`
+
+Payload: There is no payload with this command.
+
+#### Returns (as an IPC Message Payload):
+
+Header: `{ Magic; 28; 0xFF00; 0x0000; }`
+
+`DisablePerfMap` returns:
+* `int32 hresult`: The result of enabling the perfmap or jitdump files (`0` indicates success)
+
+##### Details:
+
+Returns:
+```c
+Payload
+{
+    int32 hresult
+}
+```
+
+> Available since .NET 8.0
+
 ### `ApplyStartupHook`
 
 Command Code: `0x0407`
index 2a7fe61cd1c5d648b20848e44a63fa9d1b32af15..898d6f72a343c1b4a455200dcd15c8cfd9ae472e 100644 (file)
@@ -312,6 +312,41 @@ namespace Microsoft.Diagnostics.NETCore.Client
             ValidateResponseMessage(response, nameof(ApplyStartupHookAsync));
         }
 
+        /// <summary>
+        ///
+        /// </summary>
+        /// <param name="type"></param>
+        public void EnablePerfMap(PerfMapType type)
+        {
+            IpcMessage request = CreateEnablePerfMapMessage(type);
+            IpcMessage response = IpcClient.SendMessage(_endpoint, request);
+            ValidateResponseMessage(response, nameof(EnablePerfMap));
+        }
+
+        internal async Task EnablePerfMapAsync(PerfMapType type, CancellationToken token)
+        {
+            IpcMessage request = CreateEnablePerfMapMessage(type);
+            IpcMessage response = await IpcClient.SendMessageAsync(_endpoint, request, token).ConfigureAwait(false);
+            ValidateResponseMessage(response, nameof(EnablePerfMapAsync));
+        }
+
+        /// <summary>
+        ///
+        /// </summary>
+        public void DisablePerfMap()
+        {
+            IpcMessage request = CreateDisablePerfMapMessage();
+            IpcMessage response = IpcClient.SendMessage(_endpoint, request);
+            ValidateResponseMessage(response, nameof(DisablePerfMap));
+        }
+
+        internal async Task DisablePerfMapAsync(CancellationToken token)
+        {
+            IpcMessage request = CreateDisablePerfMapMessage();
+            IpcMessage response = await IpcClient.SendMessageAsync(_endpoint, request, token).ConfigureAwait(false);
+            ValidateResponseMessage(response, nameof(DisablePerfMapAsync));
+        }
+
         /// <summary>
         /// Get all the active processes that can be attached to.
         /// </summary>
@@ -602,6 +637,17 @@ namespace Microsoft.Diagnostics.NETCore.Client
             return new IpcMessage(DiagnosticsServerCommandSet.Process, (byte)ProcessCommandId.ApplyStartupHook, serializedConfiguration);
         }
 
+        private static IpcMessage CreateEnablePerfMapMessage(PerfMapType type)
+        {
+            byte[] payload = SerializePayload((uint)type);
+            return new IpcMessage(DiagnosticsServerCommandSet.Process, (byte)ProcessCommandId.EnablePerfMap, payload);
+        }
+
+        private static IpcMessage CreateDisablePerfMapMessage()
+        {
+            return new IpcMessage(DiagnosticsServerCommandSet.Process, (byte)ProcessCommandId.DisablePerfMap);
+        }
+
         private static ProcessInfo GetProcessInfoFromResponse(IpcResponse response, string operationName)
         {
             ValidateResponseMessage(response.Message, operationName);
diff --git a/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsClient/PerfMapType.cs b/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsClient/PerfMapType.cs
new file mode 100644 (file)
index 0000000..768d595
--- /dev/null
@@ -0,0 +1,17 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace Microsoft.Diagnostics.NETCore.Client
+{
+    public enum PerfMapType
+    {
+        None = 0,
+        All = 1,
+        JitDump = 2,
+        PerfMap = 3
+    }
+}
index 1a7d967920a5c1a00e56f539d80830882ca3d3f2..6f86eb59a8b25758097af985bb3bf1d928d53a87 100644 (file)
@@ -47,6 +47,8 @@ namespace Microsoft.Diagnostics.NETCore.Client
         GetProcessEnvironment = 0x02,
         SetEnvironmentVariable = 0x03,
         GetProcessInfo2 = 0x04,
+        EnablePerfMap = 0x05,
+        DisablePerfMap = 0x06,
         ApplyStartupHook = 0x07
     }
 }
index 23448c4638383e20f190428fbab7a242453ad1d0..21aa5a6e4a9277105db87da6a74cd5d8092a8f16 100644 (file)
@@ -88,5 +88,31 @@ namespace Microsoft.Diagnostics.NETCore.Client
                 return _client.StartEventPipeSession(provider);
             }
         }
+
+        public async Task EnablePerfMap(PerfMapType type, TimeSpan timeout)
+        {
+            if (_useAsync)
+            {
+                using CancellationTokenSource cancellation = new(timeout);
+                await _client.EnablePerfMapAsync(type, cancellation.Token).ConfigureAwait(false);
+            }
+            else
+            {
+                _client.EnablePerfMap(type);
+            }
+        }
+
+        public async Task DisablePerfMap(TimeSpan timeout)
+        {
+            if (_useAsync)
+            {
+                using CancellationTokenSource cancellation = new(timeout);
+                await _client.DisablePerfMapAsync(cancellation.Token).ConfigureAwait(false);
+            }
+            else
+            {
+                _client.DisablePerfMap();
+            }
+        }
     }
 }
index dce5050599607e216bf714ebdfd8d99614b7deb0..dc5492e5fee4a737fd725d32b8b67f91edb8458b 100644 (file)
@@ -36,5 +36,15 @@ namespace Microsoft.Diagnostics.NETCore.Client
         {
             return shim.StartEventPipeSession(provider, DefaultPositiveVerificationTimeout);
         }
+
+        public static Task EnablePerfMap(this DiagnosticsClientApiShim shim, PerfMapType type)
+        {
+            return shim.EnablePerfMap(type, DefaultPositiveVerificationTimeout);
+        }
+
+        public static Task DisablePerfMap(this DiagnosticsClientApiShim shim)
+        {
+            return shim.DisablePerfMap(DefaultPositiveVerificationTimeout);
+        }
     }
 }
diff --git a/src/tests/Microsoft.Diagnostics.NETCore.Client/PerfMapTests.cs b/src/tests/Microsoft.Diagnostics.NETCore.Client/PerfMapTests.cs
new file mode 100644 (file)
index 0000000..e70194a
--- /dev/null
@@ -0,0 +1,307 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Runtime.InteropServices;
+using System.Security.Cryptography;
+using System.Text;
+using System.Threading.Tasks;
+using Microsoft.Diagnostics.CommonTestRunner;
+using Microsoft.Diagnostics.TestHelpers;
+using Xunit;
+using Xunit.Abstractions;
+using Xunit.Extensions;
+using TestRunner = Microsoft.Diagnostics.CommonTestRunner.TestRunner;
+
+namespace Microsoft.Diagnostics.NETCore.Client
+{
+    public class PerMapTests
+    {
+        private readonly ITestOutputHelper _output;
+
+        public static IEnumerable<object[]> Configurations => TestRunner.Configurations;
+
+        public PerMapTests(ITestOutputHelper outputHelper)
+        {
+            _output = outputHelper;
+        }
+
+        private static bool DoFilesExist(PerfMapType type, int pid)
+        {
+            if (type == PerfMapType.All || type == PerfMapType.PerfMap)
+            {
+                string expectedPerfMapFile = GetPerfMapFileName(pid);
+                string expectedPerfInfoFile = GetPerfInfoFileName(pid);
+
+                if (!File.Exists(expectedPerfMapFile) || !File.Exists(expectedPerfInfoFile))
+                {
+                    return false;
+                }
+            }
+
+            if (type == PerfMapType.All || type == PerfMapType.JitDump)
+            {
+                string expectedJitDumpFile = GetJitDumpFileName(pid);
+                if (!File.Exists(expectedJitDumpFile))
+                {
+                    return false;
+                }
+            }
+
+            return true;
+        }
+
+        private static string GetTmpDir()
+        {
+            string tmpDir = Environment.GetEnvironmentVariable("TMPDIR");
+            if (string.IsNullOrEmpty(tmpDir))
+            {
+                tmpDir = "/tmp";
+            }
+
+            return tmpDir;
+        }
+
+        private static string GetJitDumpFileName(int pid) => Path.Combine(GetTmpDir(), $"jit-{pid}.dump");
+
+        private static string GetPerfInfoFileName(int pid) => Path.Combine(GetTmpDir(), $"perfinfo-{pid}.map");
+
+        private static string GetPerfMapFileName(int pid) => Path.Combine(GetTmpDir(), $"perf-{pid}.map");
+
+        private string GetMethodNameFromPerfMapLine(string line)
+        {
+            string[] parts = line.Split(' ');
+            StringBuilder builder = new StringBuilder();
+            for (int i = 2; i < parts.Length; i++)
+            {
+                builder.Append(parts[i]);
+                builder.Append(' ');
+            }
+
+            return builder.ToString();
+        }
+
+        private string GetMethodNameFromJitDumpLine(string line) => throw new NotImplementedException();
+
+        private void CheckWellKnownMethods(PerfMapType type, int pid)
+        {
+            string[] wellKnownNames = new string[] { "Tracee.Program::Main", "System.PackedSpanHelpers::IndexOf" };
+
+            if (type == PerfMapType.All || type == PerfMapType.PerfMap)
+            {
+                bool[] sawNames = new bool[wellKnownNames.Length];
+                Array.Fill(sawNames, false);
+
+                string expectedPerfMapFile = GetPerfMapFileName(pid);
+                using (StreamReader reader = new StreamReader(expectedPerfMapFile))
+                {
+                    string line;
+                    while ((line = reader.ReadLine()) != null)
+                    {
+                        string methodName = GetMethodNameFromPerfMapLine(line);
+                        for (int i = 0; i < wellKnownNames.Length; ++i)
+                        {
+                            string candidate = wellKnownNames[i];
+                            if (methodName.Contains(candidate, StringComparison.OrdinalIgnoreCase))
+                            {
+                                sawNames[i] = true;
+                            }
+                        }
+                    }
+                }
+
+                for (int i = 0; i < sawNames.Length; ++i)
+                {
+                    Assert.True(sawNames[i], $"Saw method {wellKnownNames[i]} in PerfMap file");
+                }
+            }
+
+            if (type == PerfMapType.All || type == PerfMapType.JitDump)
+            {
+                bool[] sawNames = new bool[wellKnownNames.Length];
+                Array.Fill(sawNames, false);
+
+                string expectedJitDumpFile = GetJitDumpFileName(pid);
+                using (JitDumpParser parser = new JitDumpParser(expectedJitDumpFile, pid))
+                {
+                    string methodName;
+                    while ((methodName = parser.NextMethodName()) != null)
+                    {
+                        for (int i = 0; i < wellKnownNames.Length; ++i)
+                        {
+                            string candidate = wellKnownNames[i];
+                            if (methodName.Contains(candidate, StringComparison.OrdinalIgnoreCase))
+                            {
+                                sawNames[i] = true;
+                                break;
+                            }
+                        }
+                    }
+                }
+
+                for (int i = 0; i < sawNames.Length; ++i)
+                {
+                    Assert.True(sawNames[i], $"Saw method {wellKnownNames[i]} in JitDUmp file");
+                }
+            }
+        }
+
+        [SkippableTheory, MemberData(nameof(Configurations))]
+        public async Task GenerateAllTest(TestConfiguration config)
+        {
+            await GenerateTestCore(PerfMapType.All, config);
+        }
+
+        [SkippableTheory, MemberData(nameof(Configurations))]
+        public async Task GeneratePerfMapTest(TestConfiguration config)
+        {
+            await GenerateTestCore(PerfMapType.PerfMap, config);
+        }
+
+        [SkippableTheory, MemberData(nameof(Configurations))]
+        public async Task GenerateJitDumpTest(TestConfiguration config)
+        {
+            await GenerateTestCore(PerfMapType.JitDump, config);
+        }
+
+        private async Task GenerateTestCore(PerfMapType type, TestConfiguration config)
+        {
+            if (config.RuntimeFrameworkVersionMajor < 8)
+            {
+                throw new SkipTestException("Not supported on < .NET 8.0");
+            }
+
+            if (OS.Kind != OSKind.Linux)
+            {
+                throw new SkipTestException("Test only supported on Linux");
+            }
+
+            await using TestRunner runner = await TestRunner.Create(config, _output, "Tracee");
+            await runner.Start(testProcessTimeout: 60_000);
+
+            try
+            {
+                DiagnosticsClientApiShim clientShim = new(new DiagnosticsClient(runner.Pid), true);
+
+                Assert.False(DoFilesExist(type, runner.Pid));
+                await clientShim.EnablePerfMap(type);
+                await clientShim.DisablePerfMap();
+                Assert.True(DoFilesExist(type, runner.Pid));
+
+                CheckWellKnownMethods(type, runner.Pid);
+
+                runner.Stop();
+            }
+            finally
+            {
+                runner.PrintStatus();
+            }
+        }
+    }
+
+    internal class JitDumpParser : IDisposable
+    {
+        private class FileHeader
+        {
+            public uint Magic { get; private set; }
+            public uint Version { get; private set; }
+            public uint Size { get; private set; }
+            public uint Pid { get; private set; }
+
+            public FileHeader(BinaryReader reader)
+            {
+                // Validate header
+                Magic = reader.ReadUInt32();
+                Version = reader.ReadUInt32();
+                Size = reader.ReadUInt32();
+                // Skip elf_mach
+                reader.ReadUInt32();
+                // Skip padding
+                reader.ReadUInt32();
+                Pid = reader.ReadUInt32();
+                // Ignore timestamp
+                reader.ReadUInt64();
+                // Ignore flags
+                reader.ReadUInt64();
+            }
+        }
+
+        private class Record
+        {
+            public uint Id { get; private set; }
+            public uint Pid { get; private set; }
+            public ulong CodeAddr { get; private set; }
+            public ulong CodeSize { get; private set; }
+            public string Name { get; private set; }
+
+            public Record(BinaryReader reader)
+            {
+                Id = reader.ReadUInt32();
+                uint totalSize = reader.ReadUInt32();
+                // skip timestamp
+                reader.ReadUInt64();
+                Pid = reader.ReadUInt32();
+                // skip tid
+                reader.ReadUInt32();
+                // skip vma
+                reader.ReadUInt64();
+                CodeAddr = reader.ReadUInt64();
+                CodeSize = reader.ReadUInt64();
+                // skip code index
+                reader.ReadUInt64();
+                Name = ReadNullTerminatedASCIIString(reader);
+                // Skip remaining bytes
+                int readSoFar = 56 + Name.Length + 1;
+                int remainingSize = (int)totalSize - readSoFar;
+                reader.ReadBytes(remainingSize);
+            }
+
+            private string ReadNullTerminatedASCIIString(BinaryReader reader)
+            {
+                StringBuilder stringBuilder = new StringBuilder();
+                char ch;
+                while ((ch = (char)reader.ReadByte()) != 0)
+                {
+                    stringBuilder.Append(ch);
+                }
+
+                return stringBuilder.ToString();
+            }
+        }
+
+        private readonly BinaryReader _reader;
+        private readonly uint _pid;
+
+        public JitDumpParser(string jitDumpFile, int pid)
+        {
+            _reader = new BinaryReader(new FileStream(jitDumpFile, FileMode.Open));
+            _pid = (uint)pid;
+
+            FileHeader header = new FileHeader(_reader);
+            Assert.Equal(0x4A695444u, header.Magic);
+            Assert.Equal(1u, header.Version);
+            Assert.Equal(40u, header.Size);
+            Assert.Equal(_pid, header.Pid);
+
+        }
+
+        public void Dispose() => _reader.Dispose();
+
+        internal string NextMethodName()
+        {
+            if (_reader.PeekChar() == -1)
+            {
+                return null;
+            }
+
+            Record nextRecord = new Record(_reader);
+            Assert.Equal(_pid, nextRecord.Pid);
+            Assert.NotEqual(0u, nextRecord.CodeAddr);
+            Assert.NotEqual(0u, nextRecord.CodeSize);
+            return nextRecord.Name;
+        }
+    }
+}