From d66daf30d2e8affb60caa6af7364aa942e4e20d9 Mon Sep 17 00:00:00 2001 From: David Mason Date: Sat, 17 Jun 2023 00:21:38 -0700 Subject: [PATCH] Perfmaps IPC changes for DiagnosticClient (#3873) Diagnostic side changes for https://github.com/dotnet/runtime/pull/85801 --- documentation/design-docs/ipc-protocol.md | 85 +++++ .../DiagnosticsClient/DiagnosticsClient.cs | 46 +++ .../DiagnosticsClient/PerfMapType.cs | 17 + .../DiagnosticsIpc/IpcCommands.cs | 2 + .../DiagnosticsClientApiShim.cs | 26 ++ .../DiagnosticsClientApiShimExtensions.cs | 10 + .../PerfMapTests.cs | 307 ++++++++++++++++++ 7 files changed, 493 insertions(+) create mode 100644 src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsClient/PerfMapType.cs create mode 100644 src/tests/Microsoft.Diagnostics.NETCore.Client/PerfMapTests.cs diff --git a/documentation/design-docs/ipc-protocol.md b/documentation/design-docs/ipc-protocol.md index 3777067e0..60a268c76 100644 --- a/documentation/design-docs/ipc-protocol.md +++ b/documentation/design-docs/ipc-protocol.md @@ -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` diff --git a/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsClient/DiagnosticsClient.cs b/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsClient/DiagnosticsClient.cs index 2a7fe61cd..898d6f72a 100644 --- a/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsClient/DiagnosticsClient.cs +++ b/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsClient/DiagnosticsClient.cs @@ -312,6 +312,41 @@ namespace Microsoft.Diagnostics.NETCore.Client ValidateResponseMessage(response, nameof(ApplyStartupHookAsync)); } + /// + /// + /// + /// + 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)); + } + + /// + /// + /// + 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)); + } + /// /// Get all the active processes that can be attached to. /// @@ -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 index 000000000..768d595ed --- /dev/null +++ b/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsClient/PerfMapType.cs @@ -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 + } +} diff --git a/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsIpc/IpcCommands.cs b/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsIpc/IpcCommands.cs index 1a7d96792..6f86eb59a 100644 --- a/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsIpc/IpcCommands.cs +++ b/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsIpc/IpcCommands.cs @@ -47,6 +47,8 @@ namespace Microsoft.Diagnostics.NETCore.Client GetProcessEnvironment = 0x02, SetEnvironmentVariable = 0x03, GetProcessInfo2 = 0x04, + EnablePerfMap = 0x05, + DisablePerfMap = 0x06, ApplyStartupHook = 0x07 } } diff --git a/src/tests/Microsoft.Diagnostics.NETCore.Client/DiagnosticsClientApiShim.cs b/src/tests/Microsoft.Diagnostics.NETCore.Client/DiagnosticsClientApiShim.cs index 23448c463..21aa5a6e4 100644 --- a/src/tests/Microsoft.Diagnostics.NETCore.Client/DiagnosticsClientApiShim.cs +++ b/src/tests/Microsoft.Diagnostics.NETCore.Client/DiagnosticsClientApiShim.cs @@ -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(); + } + } } } diff --git a/src/tests/Microsoft.Diagnostics.NETCore.Client/DiagnosticsClientApiShimExtensions.cs b/src/tests/Microsoft.Diagnostics.NETCore.Client/DiagnosticsClientApiShimExtensions.cs index dce505059..dc5492e5f 100644 --- a/src/tests/Microsoft.Diagnostics.NETCore.Client/DiagnosticsClientApiShimExtensions.cs +++ b/src/tests/Microsoft.Diagnostics.NETCore.Client/DiagnosticsClientApiShimExtensions.cs @@ -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 index 000000000..e70194a41 --- /dev/null +++ b/src/tests/Microsoft.Diagnostics.NETCore.Client/PerfMapTests.cs @@ -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 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; + } + } +} -- 2.34.1