From 78dfe7187920ca3ee4be81df9aed6d202fef6448 Mon Sep 17 00:00:00 2001 From: Martin Othamar Date: Sat, 30 Mar 2024 21:30:00 +0100 Subject: [PATCH 1/4] DotMemoryDiagnoser implementation --- BenchmarkDotNet.sln | 7 + docs/articles/guides/nuget.md | 1 + .../samples/IntroDotMemoryDiagnoser.md | 22 +++ docs/articles/samples/toc.yml | 2 + .../BenchmarkDotNet.Samples.csproj | 1 + .../IntroDotMemoryDiagnoser.cs | 52 ++++++ ...nchmarkDotNet.Diagnostics.dotMemory.csproj | 20 +++ .../DotMemoryDiagnoser.cs | 156 ++++++++++++++++++ .../DotMemoryDiagnoserAttribute.cs | 21 +++ .../DotMemoryToolBase.cs | 126 ++++++++++++++ .../ExternalDotMemoryTool.cs | 126 ++++++++++++++ .../InProcessDotMemoryTool.cs | 24 +++ .../Progress.cs | 38 +++++ .../Properties/AssemblyInfo.cs | 11 ++ .../ExternalDotTraceTool.cs | 2 +- .../Properties/AssemblyInfo.cs | 2 + .../BenchmarkDotNet.IntegrationTests.csproj | 1 + .../DotMemoryTests.cs | 65 ++++++++ .../BenchmarkDotNet.Tests.csproj | 1 + .../dotMemory/DotMemoryTests.cs | 17 ++ 20 files changed, 694 insertions(+), 1 deletion(-) create mode 100644 docs/articles/samples/IntroDotMemoryDiagnoser.md create mode 100644 samples/BenchmarkDotNet.Samples/IntroDotMemoryDiagnoser.cs create mode 100644 src/BenchmarkDotNet.Diagnostics.dotMemory/BenchmarkDotNet.Diagnostics.dotMemory.csproj create mode 100644 src/BenchmarkDotNet.Diagnostics.dotMemory/DotMemoryDiagnoser.cs create mode 100644 src/BenchmarkDotNet.Diagnostics.dotMemory/DotMemoryDiagnoserAttribute.cs create mode 100644 src/BenchmarkDotNet.Diagnostics.dotMemory/DotMemoryToolBase.cs create mode 100644 src/BenchmarkDotNet.Diagnostics.dotMemory/ExternalDotMemoryTool.cs create mode 100644 src/BenchmarkDotNet.Diagnostics.dotMemory/InProcessDotMemoryTool.cs create mode 100644 src/BenchmarkDotNet.Diagnostics.dotMemory/Progress.cs create mode 100644 src/BenchmarkDotNet.Diagnostics.dotMemory/Properties/AssemblyInfo.cs create mode 100644 tests/BenchmarkDotNet.IntegrationTests/DotMemoryTests.cs create mode 100644 tests/BenchmarkDotNet.Tests/dotMemory/DotMemoryTests.cs diff --git a/BenchmarkDotNet.sln b/BenchmarkDotNet.sln index eed29c80af..d51cb4e029 100644 --- a/BenchmarkDotNet.sln +++ b/BenchmarkDotNet.sln @@ -53,6 +53,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BenchmarkDotNet.Integration EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BenchmarkDotNet.TestAdapter", "src\BenchmarkDotNet.TestAdapter\BenchmarkDotNet.TestAdapter.csproj", "{4C9C89B8-7C4E-4ECF-B3C9-324C8772EDAC}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BenchmarkDotNet.Diagnostics.dotMemory", "src\BenchmarkDotNet.Diagnostics.dotMemory\BenchmarkDotNet.Diagnostics.dotMemory.csproj", "{2E2283A3-6DA6-4482-8518-99D6D9F689AB}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -143,6 +145,10 @@ Global {4C9C89B8-7C4E-4ECF-B3C9-324C8772EDAC}.Debug|Any CPU.Build.0 = Debug|Any CPU {4C9C89B8-7C4E-4ECF-B3C9-324C8772EDAC}.Release|Any CPU.ActiveCfg = Release|Any CPU {4C9C89B8-7C4E-4ECF-B3C9-324C8772EDAC}.Release|Any CPU.Build.0 = Release|Any CPU + {2E2283A3-6DA6-4482-8518-99D6D9F689AB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2E2283A3-6DA6-4482-8518-99D6D9F689AB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2E2283A3-6DA6-4482-8518-99D6D9F689AB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2E2283A3-6DA6-4482-8518-99D6D9F689AB}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -169,6 +175,7 @@ Global {C5BDA61F-3A56-4B59-901D-0A17E78F4076} = {D6597E3A-6892-4A68-8E14-042FC941FDA2} {AACA2C63-A85B-47AB-99FC-72C3FF408B14} = {14195214-591A-45B7-851A-19D3BA2413F9} {4C9C89B8-7C4E-4ECF-B3C9-324C8772EDAC} = {D6597E3A-6892-4A68-8E14-042FC941FDA2} + {2E2283A3-6DA6-4482-8518-99D6D9F689AB} = {D6597E3A-6892-4A68-8E14-042FC941FDA2} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {4D9AF12B-1F7F-45A7-9E8C-E4E46ADCBD1F} diff --git a/docs/articles/guides/nuget.md b/docs/articles/guides/nuget.md index 0e2de62a79..d1359c03d9 100644 --- a/docs/articles/guides/nuget.md +++ b/docs/articles/guides/nuget.md @@ -13,6 +13,7 @@ We have the following set of NuGet packages (you can install it directly from `n * `BenchmarkDotNet.Annotations`: Basic BenchmarkDotNet annotations for your benchmarks. * `BenchmarkDotNet.Diagnostics.Windows`: an additional optional package that provides a set of Windows diagnosers. * `BenchmarkDotNet.Diagnostics.dotTrace`: an additional optional package that provides DotTraceDiagnoser. +* `BenchmarkDotNet.Diagnostics.dotMemory`: an additional optional package that provides DotMemoryDiagnoser. * `BenchmarkDotNet.Templates`: Templates for BenchmarkDotNet. You might find other NuGet packages that start with `BenchmarkDotNet` name, but they are internal BDN packages that should not be installed manually. All that matters are the three packages mentioned above. diff --git a/docs/articles/samples/IntroDotMemoryDiagnoser.md b/docs/articles/samples/IntroDotMemoryDiagnoser.md new file mode 100644 index 0000000000..dd5d78e086 --- /dev/null +++ b/docs/articles/samples/IntroDotMemoryDiagnoser.md @@ -0,0 +1,22 @@ +--- +uid: BenchmarkDotNet.Samples.IntroDotMemoryDiagnoser +--- + +## Sample: IntroDotMemoryDiagnoser + +If you want to get a memory alloation profile of your benchmarks, just add the `[DotMemoryDiagnoser]` attribute, as shown below. +As a result, BenchmarkDotNet performs bonus benchmark runs using attached + [dotMemory Command-Line Profiler](https://www.jetbrains.com/help/dotmemory/Working_with_dotMemory_Command-Line_Profiler.html). +The obtained dotMemory workspaces are saved to the `artifacts` folder. +These dotMemory workspaces can be opened using the [standalone dotMemory](https://www.jetbrains.com/dotmemory/), + or [dotMemory in Rider](https://www.jetbrains.com/help/rider/Memory_profiling_of_.NET_code.html). + +### Source code + +[!code-csharp[IntroDotMemoryDiagnoser.cs](../../../samples/BenchmarkDotNet.Samples/IntroDotMemoryDiagnoser.cs)] + +### Links + +* The permanent link to this sample: @BenchmarkDotNet.Samples.IntroDotMemoryDiagnoser + +--- \ No newline at end of file diff --git a/docs/articles/samples/toc.yml b/docs/articles/samples/toc.yml index 8098a637c3..7e4c7671e1 100644 --- a/docs/articles/samples/toc.yml +++ b/docs/articles/samples/toc.yml @@ -38,6 +38,8 @@ href: IntroDisassemblyRyuJit.md - name: IntroDotTraceDiagnoser href: IntroDotTraceDiagnoser.md +- name: IntroDotMemoryDiagnoser + href: IntroDotMemoryDiagnoser.md - name: IntroEnvVars href: IntroEnvVars.md - name: IntroEventPipeProfiler diff --git a/samples/BenchmarkDotNet.Samples/BenchmarkDotNet.Samples.csproj b/samples/BenchmarkDotNet.Samples/BenchmarkDotNet.Samples.csproj index 1af13f61c3..36c3f60b32 100644 --- a/samples/BenchmarkDotNet.Samples/BenchmarkDotNet.Samples.csproj +++ b/samples/BenchmarkDotNet.Samples/BenchmarkDotNet.Samples.csproj @@ -26,6 +26,7 @@ + diff --git a/samples/BenchmarkDotNet.Samples/IntroDotMemoryDiagnoser.cs b/samples/BenchmarkDotNet.Samples/IntroDotMemoryDiagnoser.cs new file mode 100644 index 0000000000..846e2178f4 --- /dev/null +++ b/samples/BenchmarkDotNet.Samples/IntroDotMemoryDiagnoser.cs @@ -0,0 +1,52 @@ +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Diagnostics.dotMemory; +using System.Collections.Generic; + +namespace BenchmarkDotNet.Samples +{ + // Enables dotMemory profiling for all jobs + [DotMemoryDiagnoser] + // Adds the default "external-process" job + // Profiling is performed using dotMemory Command-Line Profiler + // See: https://www.jetbrains.com/help/dotmemory/Working_with_dotMemory_Command-Line_Profiler.html + [SimpleJob] + // Adds an "in-process" job + // Profiling is performed using dotMemory SelfApi + // NuGet reference: https://www.nuget.org/packages/JetBrains.Profiler.SelfApi + [InProcess] + public class IntroDotMemoryDiagnoser + { + [Params(1024)] + public int Size; + + private byte[] dataArray; + private IEnumerable dataEnumerable; + + [GlobalSetup] + public void Setup() + { + dataArray = new byte[Size]; + dataEnumerable = dataArray; + } + + [Benchmark] + public int IterateArray() + { + var count = 0; + foreach (var _ in dataArray) + count++; + + return count; + } + + [Benchmark] + public int IterateEnumerable() + { + var count = 0; + foreach (var _ in dataEnumerable) + count++; + + return count; + } + } +} \ No newline at end of file diff --git a/src/BenchmarkDotNet.Diagnostics.dotMemory/BenchmarkDotNet.Diagnostics.dotMemory.csproj b/src/BenchmarkDotNet.Diagnostics.dotMemory/BenchmarkDotNet.Diagnostics.dotMemory.csproj new file mode 100644 index 0000000000..9ee6b4be18 --- /dev/null +++ b/src/BenchmarkDotNet.Diagnostics.dotMemory/BenchmarkDotNet.Diagnostics.dotMemory.csproj @@ -0,0 +1,20 @@ + + + + net6.0;net462;netcoreapp3.1 + $(NoWarn);1591 + BenchmarkDotNet.Diagnostics.dotMemory + BenchmarkDotNet.Diagnostics.dotMemory + BenchmarkDotNet.Diagnostics.dotMemory + enable + + + + + + + + + + + diff --git a/src/BenchmarkDotNet.Diagnostics.dotMemory/DotMemoryDiagnoser.cs b/src/BenchmarkDotNet.Diagnostics.dotMemory/DotMemoryDiagnoser.cs new file mode 100644 index 0000000000..69a5e952c2 --- /dev/null +++ b/src/BenchmarkDotNet.Diagnostics.dotMemory/DotMemoryDiagnoser.cs @@ -0,0 +1,156 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using BenchmarkDotNet.Analysers; +using BenchmarkDotNet.Diagnosers; +using BenchmarkDotNet.Engines; +using BenchmarkDotNet.Exporters; +using BenchmarkDotNet.Jobs; +using BenchmarkDotNet.Loggers; +using BenchmarkDotNet.Portability; +using BenchmarkDotNet.Reports; +using BenchmarkDotNet.Running; +using BenchmarkDotNet.Toolchains; +using BenchmarkDotNet.Validators; +using RunMode = BenchmarkDotNet.Diagnosers.RunMode; + +namespace BenchmarkDotNet.Diagnostics.dotMemory +{ + public class DotMemoryDiagnoser : IProfiler + { + private readonly Uri? nugetUrl; + private readonly string? toolsDownloadFolder; + + private DotMemoryToolBase? tool; + + public DotMemoryDiagnoser(Uri? nugetUrl = null, string? toolsDownloadFolder = null) + { + this.nugetUrl = nugetUrl; + this.toolsDownloadFolder = toolsDownloadFolder; + } + + public IEnumerable Ids => new[] { "DotMemory" }; + public string ShortName => "dotMemory"; + + public RunMode GetRunMode(BenchmarkCase benchmarkCase) + { + return IsSupported(benchmarkCase.Job.Environment.GetRuntime().RuntimeMoniker) ? RunMode.ExtraRun : RunMode.None; + } + + private readonly List snapshotFilePaths = new (); + + public void Handle(HostSignal signal, DiagnoserActionParameters parameters) + { + var logger = parameters.Config.GetCompositeLogger(); + var job = parameters.BenchmarkCase.Job; + if (tool is null) + { + bool isInProcess = job.GetToolchain().IsInProcess; + tool ??= isInProcess + ? new InProcessDotMemoryTool(logger, nugetUrl, downloadTo: toolsDownloadFolder) + : new ExternalDotMemoryTool(logger, nugetUrl, downloadTo: toolsDownloadFolder); + } + + var runtimeMoniker = job.Environment.GetRuntime().RuntimeMoniker; + if (!IsSupported(runtimeMoniker)) + { + logger.WriteLineError($"Runtime '{runtimeMoniker}' is not supported by dotMemory"); + return; + } + + switch (signal) + { + case HostSignal.BeforeAnythingElse: + tool.Init(parameters); + break; + case HostSignal.BeforeActualRun: + snapshotFilePaths.Add(tool.Start(parameters)); + break; + case HostSignal.AfterActualRun: + tool.Stop(parameters); + break; + } + } + + public IEnumerable Exporters => Enumerable.Empty(); + public IEnumerable Analysers => Enumerable.Empty(); + + public IEnumerable Validate(ValidationParameters validationParameters) + { + var runtimeMonikers = validationParameters.Benchmarks.Select(b => b.Job.Environment.GetRuntime().RuntimeMoniker).Distinct(); + foreach (var runtimeMoniker in runtimeMonikers) + { + if (!IsSupported(runtimeMoniker)) + yield return new ValidationError(true, $"Runtime '{runtimeMoniker}' is not supported by dotMemory"); + } + } + + internal static bool IsSupported(RuntimeMoniker runtimeMoniker) + { + switch (runtimeMoniker) + { + case RuntimeMoniker.HostProcess: + case RuntimeMoniker.Net461: + case RuntimeMoniker.Net462: + case RuntimeMoniker.Net47: + case RuntimeMoniker.Net471: + case RuntimeMoniker.Net472: + case RuntimeMoniker.Net48: + case RuntimeMoniker.Net481: + case RuntimeMoniker.Net50: + case RuntimeMoniker.Net60: + case RuntimeMoniker.Net70: + case RuntimeMoniker.Net80: + case RuntimeMoniker.Net90: + return true; + case RuntimeMoniker.NotRecognized: + case RuntimeMoniker.Mono: + case RuntimeMoniker.NativeAot60: + case RuntimeMoniker.NativeAot70: + case RuntimeMoniker.NativeAot80: + case RuntimeMoniker.NativeAot90: + case RuntimeMoniker.Wasm: + case RuntimeMoniker.WasmNet50: + case RuntimeMoniker.WasmNet60: + case RuntimeMoniker.WasmNet70: + case RuntimeMoniker.WasmNet80: + case RuntimeMoniker.WasmNet90: + case RuntimeMoniker.MonoAOTLLVM: + case RuntimeMoniker.MonoAOTLLVMNet60: + case RuntimeMoniker.MonoAOTLLVMNet70: + case RuntimeMoniker.MonoAOTLLVMNet80: + case RuntimeMoniker.MonoAOTLLVMNet90: + case RuntimeMoniker.Mono60: + case RuntimeMoniker.Mono70: + case RuntimeMoniker.Mono80: + case RuntimeMoniker.Mono90: +#pragma warning disable CS0618 // Type or member is obsolete + case RuntimeMoniker.NetCoreApp50: +#pragma warning restore CS0618 // Type or member is obsolete + return false; + case RuntimeMoniker.NetCoreApp20: + case RuntimeMoniker.NetCoreApp21: + case RuntimeMoniker.NetCoreApp22: + return RuntimeInformation.IsWindows(); + case RuntimeMoniker.NetCoreApp30: + case RuntimeMoniker.NetCoreApp31: + return RuntimeInformation.IsWindows() || RuntimeInformation.IsLinux(); + default: + throw new ArgumentOutOfRangeException(nameof(runtimeMoniker), runtimeMoniker, $"Runtime moniker {runtimeMoniker} is not supported"); + } + } + + public IEnumerable ProcessResults(DiagnoserResults results) => ImmutableArray.Empty; + + public void DisplayResults(ILogger logger) + { + if (snapshotFilePaths.Any()) + { + logger.WriteLineInfo("The following dotMemory snapshots were generated:"); + foreach (string snapshotFilePath in snapshotFilePaths) + logger.WriteLineInfo($"* {snapshotFilePath}"); + } + } + } +} \ No newline at end of file diff --git a/src/BenchmarkDotNet.Diagnostics.dotMemory/DotMemoryDiagnoserAttribute.cs b/src/BenchmarkDotNet.Diagnostics.dotMemory/DotMemoryDiagnoserAttribute.cs new file mode 100644 index 0000000000..a474312978 --- /dev/null +++ b/src/BenchmarkDotNet.Diagnostics.dotMemory/DotMemoryDiagnoserAttribute.cs @@ -0,0 +1,21 @@ +using System; +using BenchmarkDotNet.Configs; + +namespace BenchmarkDotNet.Diagnostics.dotMemory +{ + [AttributeUsage(AttributeTargets.Class)] + public class DotMemoryDiagnoserAttribute : Attribute, IConfigSource + { + public IConfig Config { get; } + + public DotMemoryDiagnoserAttribute() + { + Config = ManualConfig.CreateEmpty().AddDiagnoser(new DotMemoryDiagnoser()); + } + + public DotMemoryDiagnoserAttribute(Uri? nugetUrl = null, string? toolsDownloadFolder = null) + { + Config = ManualConfig.CreateEmpty().AddDiagnoser(new DotMemoryDiagnoser(nugetUrl, toolsDownloadFolder)); + } + } +} \ No newline at end of file diff --git a/src/BenchmarkDotNet.Diagnostics.dotMemory/DotMemoryToolBase.cs b/src/BenchmarkDotNet.Diagnostics.dotMemory/DotMemoryToolBase.cs new file mode 100644 index 0000000000..891cf2b6c3 --- /dev/null +++ b/src/BenchmarkDotNet.Diagnostics.dotMemory/DotMemoryToolBase.cs @@ -0,0 +1,126 @@ +using System; +using System.IO; +using System.Reflection; +using BenchmarkDotNet.Diagnosers; +using BenchmarkDotNet.Helpers; +using BenchmarkDotNet.Loggers; +using JetBrains.Profiler.SelfApi; + +namespace BenchmarkDotNet.Diagnostics.dotMemory +{ + internal abstract class DotMemoryToolBase + { + private readonly ILogger logger; + private readonly Uri? nugetUrl; + private readonly NuGetApi nugetApi; + private readonly string? downloadTo; + + protected DotMemoryToolBase(ILogger logger, Uri? nugetUrl = null, NuGetApi nugetApi = NuGetApi.V3, string? downloadTo = null) + { + this.logger = logger; + this.nugetUrl = nugetUrl; + this.nugetApi = nugetApi; + this.downloadTo = downloadTo; + } + + public void Init(DiagnoserActionParameters parameters) + { + try + { + logger.WriteLineInfo("Ensuring that dotMemory prerequisite is installed..."); + var progress = new Progress(logger, "Installing DotMemory"); + DotMemory.EnsurePrerequisiteAsync(progress, nugetUrl, nugetApi, downloadTo).Wait(); + logger.WriteLineInfo("dotMemory prerequisite is installed"); + logger.WriteLineInfo($"dotMemory runner path: {GetRunnerPath()}"); + } + catch (Exception e) + { + logger.WriteLineError(e.ToString()); + } + } + + protected abstract void Attach(DiagnoserActionParameters parameters, string snapshotFile); + protected abstract void Snapshot(DiagnoserActionParameters parameters); + protected abstract void Detach(); + + public string Start(DiagnoserActionParameters parameters) + { + string snapshotFile = ArtifactFileNameHelper.GetFilePath(parameters, "snapshots", DateTime.Now, "dmw", ".0000".Length); + string? snapshotDirectory = Path.GetDirectoryName(snapshotFile); + logger.WriteLineInfo($"Target snapshot file: {snapshotFile}"); + if (!Directory.Exists(snapshotDirectory) && snapshotDirectory != null) + { + try + { + Directory.CreateDirectory(snapshotDirectory); + } + catch (Exception e) + { + logger.WriteLineError($"Failed to create directory: {snapshotDirectory}"); + logger.WriteLineError(e.ToString()); + } + } + + try + { + logger.WriteLineInfo("Attaching dotMemory to the process..."); + Attach(parameters, snapshotFile); + logger.WriteLineInfo("dotMemory is successfully attached"); + } + catch (Exception e) + { + logger.WriteLineError(e.ToString()); + return snapshotFile; + } + + return snapshotFile; + } + + public void Stop(DiagnoserActionParameters parameters) + { + try + { + logger.WriteLineInfo("Taking dotMemory snapshot..."); + Snapshot(parameters); + logger.WriteLineInfo("dotMemory snapshot is successfully taken"); + } + catch (Exception e) + { + logger.WriteLineError(e.ToString()); + } + + try + { + logger.WriteLineInfo("Detaching dotMemory from the process..."); + Detach(); + logger.WriteLineInfo("dotMemory is successfully detached"); + } + catch (Exception e) + { + logger.WriteLineError(e.ToString()); + } + } + + protected string GetRunnerPath() + { + var consoleRunnerPackageField = typeof(DotMemory).GetField("ConsoleRunnerPackage", BindingFlags.NonPublic | BindingFlags.Static); + if (consoleRunnerPackageField == null) + throw new InvalidOperationException("Field 'ConsoleRunnerPackage' not found."); + + object? consoleRunnerPackage = consoleRunnerPackageField.GetValue(null); + if (consoleRunnerPackage == null) + throw new InvalidOperationException("Unable to get value of 'ConsoleRunnerPackage'."); + + var consoleRunnerPackageType = consoleRunnerPackage.GetType(); + var getRunnerPathMethod = consoleRunnerPackageType.GetMethod("GetRunnerPath"); + if (getRunnerPathMethod == null) + throw new InvalidOperationException("Method 'GetRunnerPath' not found."); + + string? runnerPath = getRunnerPathMethod.Invoke(consoleRunnerPackage, null) as string; + if (runnerPath == null) + throw new InvalidOperationException("Unable to invoke 'GetRunnerPath'."); + + return runnerPath; + } + } +} \ No newline at end of file diff --git a/src/BenchmarkDotNet.Diagnostics.dotMemory/ExternalDotMemoryTool.cs b/src/BenchmarkDotNet.Diagnostics.dotMemory/ExternalDotMemoryTool.cs new file mode 100644 index 0000000000..b14eede2c9 --- /dev/null +++ b/src/BenchmarkDotNet.Diagnostics.dotMemory/ExternalDotMemoryTool.cs @@ -0,0 +1,126 @@ +using System; +using System.Diagnostics; +using System.Threading.Tasks; +using BenchmarkDotNet.Diagnosers; +using BenchmarkDotNet.Loggers; +using JetBrains.Profiler.SelfApi; +using ILogger = BenchmarkDotNet.Loggers.ILogger; + +namespace BenchmarkDotNet.Diagnostics.dotMemory +{ + internal class ExternalDotMemoryTool : DotMemoryToolBase + { + private static readonly TimeSpan AttachTimeout = TimeSpan.FromMinutes(5); + private static readonly TimeSpan SnapshotTimeout = TimeSpan.FromSeconds(30); + + private Process? process; + private TaskCompletionSource? snapshotWaitingTask; + + public ExternalDotMemoryTool(ILogger logger, Uri? nugetUrl = null, NuGetApi nugetApi = NuGetApi.V3, string? downloadTo = null) : + base(logger, nugetUrl, nugetApi, downloadTo) { } + + protected override void Attach(DiagnoserActionParameters parameters, string snapshotFile) + { + var logger = parameters.Config.GetCompositeLogger(); + + string runnerPath = GetRunnerPath(); + int pid = parameters.Process.Id; + string arguments = $"attach {pid} --save-to-file=\"{snapshotFile}\" --service-output"; + + logger.WriteLineInfo($"Starting process: '{runnerPath} {arguments}'"); + + var processStartInfo = new ProcessStartInfo + { + FileName = runnerPath, + WorkingDirectory = "", + Arguments = arguments, + UseShellExecute = false, + CreateNoWindow = true, + RedirectStandardOutput = true, + RedirectStandardError = true, + RedirectStandardInput = true, + }; + + var attachWaitingTask = new TaskCompletionSource(); + snapshotWaitingTask = new TaskCompletionSource(); + process = new Process { StartInfo = processStartInfo }; + try + { + process.OutputDataReceived += (_, args) => + { + string? content = args.Data; + if (content != null) + { + logger.WriteLineInfo("[dotMemory] " + content); + if (content.Contains("##dotMemory[\"connected\"")) + attachWaitingTask.TrySetResult(true); + + if (content.Contains("##dotMemory[\"snapshot-saved\"")) + snapshotWaitingTask.TrySetResult(true); + } + }; + process.ErrorDataReceived += (_, args) => + { + string? content = args.Data; + if (content != null) + logger.WriteLineError("[dotMemory] " + args.Data); + }; + process.Exited += (_, _) => + { + attachWaitingTask.TrySetResult(false); + snapshotWaitingTask.TrySetResult(false); + }; + process.Start(); + process.StandardInput.AutoFlush = true; + process.BeginOutputReadLine(); + process.BeginErrorReadLine(); + } + catch (Exception e) + { + attachWaitingTask.TrySetResult(false); + logger.WriteLineError(e.ToString()); + } + + if (!attachWaitingTask.Task.Wait(AttachTimeout)) + throw new Exception($"Failed to attach dotMemory to the target process (timeout: {AttachTimeout.TotalSeconds} sec)"); + if (!attachWaitingTask.Task.Result) + throw new Exception($"Failed to attach dotMemory to the target process (ExitCode={process.ExitCode})"); + } + + protected override void Snapshot(DiagnoserActionParameters parameters) + { + if (process is null || snapshotWaitingTask is null) + throw new InvalidOperationException("dotMemory process is not attached"); + + if (snapshotWaitingTask.Task.IsCompleted) + { + if (!snapshotWaitingTask.Task.Result) + throw new InvalidOperationException($"Can't create dotMemory snapshot, dotMemory process exited (ExitCode={process.ExitCode})"); + + snapshotWaitingTask = new TaskCompletionSource(); + } + + int pid = parameters.Process.Id; + + process.StandardInput.WriteLine($"##dotMemory[\"get-snapshot\", {{pid:{pid}}}]"); + + if (!snapshotWaitingTask.Task.Wait(SnapshotTimeout)) + throw new Exception($"Failed to create dotMemory snapshot (timeout: {SnapshotTimeout.TotalSeconds} sec)"); + if (!snapshotWaitingTask.Task.Result) + throw new Exception($"Failed to create dotMemory snapshot (ExitCode={process.ExitCode})"); + } + + protected override void Detach() + { + if (process is null) + throw new InvalidOperationException("dotMemory process is not attached"); + + process.StandardInput.WriteLine("##dotMemory[\"disconnect\"]"); + process.StandardInput.Close(); + process.WaitForExit(); + + process.Dispose(); + process = null; + } + } +} \ No newline at end of file diff --git a/src/BenchmarkDotNet.Diagnostics.dotMemory/InProcessDotMemoryTool.cs b/src/BenchmarkDotNet.Diagnostics.dotMemory/InProcessDotMemoryTool.cs new file mode 100644 index 0000000000..c80b815e86 --- /dev/null +++ b/src/BenchmarkDotNet.Diagnostics.dotMemory/InProcessDotMemoryTool.cs @@ -0,0 +1,24 @@ +using System; +using BenchmarkDotNet.Diagnosers; +using BenchmarkDotNet.Loggers; +using JetBrains.Profiler.SelfApi; + +namespace BenchmarkDotNet.Diagnostics.dotMemory +{ + internal class InProcessDotMemoryTool : DotMemoryToolBase + { + public InProcessDotMemoryTool(ILogger logger, Uri? nugetUrl = null, NuGetApi nugetApi = NuGetApi.V3, string? downloadTo = null) : + base(logger, nugetUrl, nugetApi, downloadTo) { } + + protected override void Attach(DiagnoserActionParameters parameters, string snapshotFile) + { + var config = new DotMemory.Config(); + config.SaveToFile(snapshotFile); + DotMemory.Attach(config); + } + + protected override void Snapshot(DiagnoserActionParameters parameters) => DotMemory.GetSnapshot(); + + protected override void Detach() => DotMemory.Detach(); + } +} \ No newline at end of file diff --git a/src/BenchmarkDotNet.Diagnostics.dotMemory/Progress.cs b/src/BenchmarkDotNet.Diagnostics.dotMemory/Progress.cs new file mode 100644 index 0000000000..738997bb6d --- /dev/null +++ b/src/BenchmarkDotNet.Diagnostics.dotMemory/Progress.cs @@ -0,0 +1,38 @@ +using System; +using System.Diagnostics; +using BenchmarkDotNet.Loggers; + +namespace BenchmarkDotNet.Diagnostics.dotMemory +{ + public class Progress : IProgress + { + private static readonly TimeSpan ReportInterval = TimeSpan.FromSeconds(0.1); + + private readonly ILogger logger; + private readonly string title; + + public Progress(ILogger logger, string title) + { + this.logger = logger; + this.title = title; + } + + private int lastProgress; + private Stopwatch? stopwatch; + + public void Report(double value) + { + int progress = (int)Math.Floor(value); + bool needToReport = stopwatch == null || + (stopwatch != null && stopwatch?.Elapsed > ReportInterval) || + progress == 100; + + if (lastProgress != progress && needToReport) + { + logger.WriteLineInfo($"{title}: {progress}%"); + lastProgress = progress; + stopwatch = Stopwatch.StartNew(); + } + } + } +} \ No newline at end of file diff --git a/src/BenchmarkDotNet.Diagnostics.dotMemory/Properties/AssemblyInfo.cs b/src/BenchmarkDotNet.Diagnostics.dotMemory/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..270fdc2c9c --- /dev/null +++ b/src/BenchmarkDotNet.Diagnostics.dotMemory/Properties/AssemblyInfo.cs @@ -0,0 +1,11 @@ +using System; +using System.Runtime.CompilerServices; +using BenchmarkDotNet.Properties; + +[assembly: CLSCompliant(true)] + +#if RELEASE +[assembly: InternalsVisibleTo("BenchmarkDotNet.Tests,PublicKey=" + BenchmarkDotNetInfo.PublicKey)] +#else +[assembly: InternalsVisibleTo("BenchmarkDotNet.Tests")] +#endif \ No newline at end of file diff --git a/src/BenchmarkDotNet.Diagnostics.dotTrace/ExternalDotTraceTool.cs b/src/BenchmarkDotNet.Diagnostics.dotTrace/ExternalDotTraceTool.cs index dfc9903b82..c7f1cf18c8 100644 --- a/src/BenchmarkDotNet.Diagnostics.dotTrace/ExternalDotTraceTool.cs +++ b/src/BenchmarkDotNet.Diagnostics.dotTrace/ExternalDotTraceTool.cs @@ -70,7 +70,7 @@ protected override void Attach(DiagnoserActionParameters parameters, string snap } if (!attachWaitingTask.Task.Wait(AttachTimeout)) - throw new Exception($"Failed to attach dotTrace to the target process (timeout: {AttachTimeout.TotalSeconds} sec"); + throw new Exception($"Failed to attach dotTrace to the target process (timeout: {AttachTimeout.TotalSeconds} sec)"); if (!attachWaitingTask.Task.Result) throw new Exception($"Failed to attach dotTrace to the target process (ExitCode={process.ExitCode})"); } diff --git a/src/BenchmarkDotNet/Properties/AssemblyInfo.cs b/src/BenchmarkDotNet/Properties/AssemblyInfo.cs index cec4ef220c..ee7077a55e 100644 --- a/src/BenchmarkDotNet/Properties/AssemblyInfo.cs +++ b/src/BenchmarkDotNet/Properties/AssemblyInfo.cs @@ -14,6 +14,7 @@ [assembly: InternalsVisibleTo("BenchmarkDotNet.IntegrationTests,PublicKey=" + BenchmarkDotNetInfo.PublicKey)] [assembly: InternalsVisibleTo("BenchmarkDotNet.Diagnostics.Windows,PublicKey=" + BenchmarkDotNetInfo.PublicKey)] [assembly: InternalsVisibleTo("BenchmarkDotNet.Diagnostics.dotTrace,PublicKey=" + BenchmarkDotNetInfo.PublicKey)] +[assembly: InternalsVisibleTo("BenchmarkDotNet.Diagnostics.dotMemory,PublicKey=" + BenchmarkDotNetInfo.PublicKey)] [assembly: InternalsVisibleTo("BenchmarkDotNet.IntegrationTests.ManualRunning,PublicKey=" + BenchmarkDotNetInfo.PublicKey)] [assembly: InternalsVisibleTo("BenchmarkDotNet.IntegrationTests.ManualRunning.MultipleFrameworks,PublicKey=" + BenchmarkDotNetInfo.PublicKey)] [assembly: InternalsVisibleTo("BenchmarkDotNet.TestAdapter,PublicKey=" + BenchmarkDotNetInfo.PublicKey)] @@ -22,6 +23,7 @@ [assembly: InternalsVisibleTo("BenchmarkDotNet.IntegrationTests")] [assembly: InternalsVisibleTo("BenchmarkDotNet.Diagnostics.Windows")] [assembly: InternalsVisibleTo("BenchmarkDotNet.Diagnostics.dotTrace")] +[assembly: InternalsVisibleTo("BenchmarkDotNet.Diagnostics.dotMemory")] [assembly: InternalsVisibleTo("BenchmarkDotNet.IntegrationTests.ManualRunning")] [assembly: InternalsVisibleTo("BenchmarkDotNet.IntegrationTests.ManualRunning.MultipleFrameworks")] [assembly: InternalsVisibleTo("BenchmarkDotNet.TestAdapter")] diff --git a/tests/BenchmarkDotNet.IntegrationTests/BenchmarkDotNet.IntegrationTests.csproj b/tests/BenchmarkDotNet.IntegrationTests/BenchmarkDotNet.IntegrationTests.csproj index 8949cce2c0..a928af1424 100644 --- a/tests/BenchmarkDotNet.IntegrationTests/BenchmarkDotNet.IntegrationTests.csproj +++ b/tests/BenchmarkDotNet.IntegrationTests/BenchmarkDotNet.IntegrationTests.csproj @@ -29,6 +29,7 @@ + diff --git a/tests/BenchmarkDotNet.IntegrationTests/DotMemoryTests.cs b/tests/BenchmarkDotNet.IntegrationTests/DotMemoryTests.cs new file mode 100644 index 0000000000..a3a3ffca9d --- /dev/null +++ b/tests/BenchmarkDotNet.IntegrationTests/DotMemoryTests.cs @@ -0,0 +1,65 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Configs; +using BenchmarkDotNet.Diagnostics.dotMemory; +using BenchmarkDotNet.Jobs; +using BenchmarkDotNet.Portability; +using BenchmarkDotNet.Toolchains.InProcess.Emit; +using Xunit; +using Xunit.Abstractions; + +namespace BenchmarkDotNet.IntegrationTests +{ + public class DotMemoryTests : BenchmarkTestExecutor + { + public DotMemoryTests(ITestOutputHelper output) : base(output) { } + + [Fact] + public void DotMemorySmokeTest() + { + if (!RuntimeInformation.IsWindows() && RuntimeInformation.IsMono) + { + Output.WriteLine("Skip Mono on non-Windows"); + return; + } + + var config = new ManualConfig().AddJob( + Job.Dry.WithId("ExternalProcess"), + Job.Dry.WithToolchain(InProcessEmitToolchain.Instance).WithId("InProcess") + ); + string snapshotDirectory = Path.Combine(Directory.GetCurrentDirectory(), "BenchmarkDotNet.Artifacts", "snapshots"); + if (Directory.Exists(snapshotDirectory)) + Directory.Delete(snapshotDirectory, true); + + CanExecute(config); + + Output.WriteLine("---------------------------------------------"); + Output.WriteLine("SnapshotDirectory:" + snapshotDirectory); + var snapshots = Directory.EnumerateFiles(snapshotDirectory) + .Where(filePath => Path.GetExtension(filePath).Equals(".dmw", StringComparison.OrdinalIgnoreCase)) + .Select(Path.GetFileName) + .OrderBy(fileName => fileName) + .ToList(); + Output.WriteLine("Snapshots:"); + foreach (string snapshot in snapshots) + Output.WriteLine("* " + snapshot); + Assert.Equal(2, snapshots.Count); + } + + [DotMemoryDiagnoser] + public class Benchmarks + { + [Benchmark] + public int Foo() + { + var list = new List(); + for (int i = 0; i < 1000000; i++) + list.Add(new object()); + return list.Count; + } + } + } +} \ No newline at end of file diff --git a/tests/BenchmarkDotNet.Tests/BenchmarkDotNet.Tests.csproj b/tests/BenchmarkDotNet.Tests/BenchmarkDotNet.Tests.csproj index c85c101890..fcdc416bbf 100755 --- a/tests/BenchmarkDotNet.Tests/BenchmarkDotNet.Tests.csproj +++ b/tests/BenchmarkDotNet.Tests/BenchmarkDotNet.Tests.csproj @@ -36,6 +36,7 @@ + diff --git a/tests/BenchmarkDotNet.Tests/dotMemory/DotMemoryTests.cs b/tests/BenchmarkDotNet.Tests/dotMemory/DotMemoryTests.cs new file mode 100644 index 0000000000..7142486280 --- /dev/null +++ b/tests/BenchmarkDotNet.Tests/dotMemory/DotMemoryTests.cs @@ -0,0 +1,17 @@ +using System; +using BenchmarkDotNet.Diagnostics.dotMemory; +using BenchmarkDotNet.Jobs; +using Xunit; + +namespace BenchmarkDotNet.Tests.dotMemory +{ + public class DotMemoryTests + { + [Fact] + public void AllRuntimeMonikerAreKnown() + { + foreach (RuntimeMoniker moniker in Enum.GetValues(typeof(RuntimeMoniker))) + DotMemoryDiagnoser.IsSupported(moniker); // Just check that it doesn't throw exceptions + } + } +} \ No newline at end of file From dfaacb987ad56ac8c64f4b02d2e71a5e327cf01d Mon Sep 17 00:00:00 2001 From: Martin Othamar Date: Sun, 31 Mar 2024 03:38:07 +0200 Subject: [PATCH 2/4] Typo fix, less alloc in test --- docs/articles/samples/IntroDotMemoryDiagnoser.md | 2 +- tests/BenchmarkDotNet.IntegrationTests/DotMemoryTests.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/articles/samples/IntroDotMemoryDiagnoser.md b/docs/articles/samples/IntroDotMemoryDiagnoser.md index dd5d78e086..e16e96a83c 100644 --- a/docs/articles/samples/IntroDotMemoryDiagnoser.md +++ b/docs/articles/samples/IntroDotMemoryDiagnoser.md @@ -4,7 +4,7 @@ uid: BenchmarkDotNet.Samples.IntroDotMemoryDiagnoser ## Sample: IntroDotMemoryDiagnoser -If you want to get a memory alloation profile of your benchmarks, just add the `[DotMemoryDiagnoser]` attribute, as shown below. +If you want to get a memory allocation profile of your benchmarks, just add the `[DotMemoryDiagnoser]` attribute, as shown below. As a result, BenchmarkDotNet performs bonus benchmark runs using attached [dotMemory Command-Line Profiler](https://www.jetbrains.com/help/dotmemory/Working_with_dotMemory_Command-Line_Profiler.html). The obtained dotMemory workspaces are saved to the `artifacts` folder. diff --git a/tests/BenchmarkDotNet.IntegrationTests/DotMemoryTests.cs b/tests/BenchmarkDotNet.IntegrationTests/DotMemoryTests.cs index a3a3ffca9d..4b18c74dbc 100644 --- a/tests/BenchmarkDotNet.IntegrationTests/DotMemoryTests.cs +++ b/tests/BenchmarkDotNet.IntegrationTests/DotMemoryTests.cs @@ -56,7 +56,7 @@ public class Benchmarks public int Foo() { var list = new List(); - for (int i = 0; i < 1000000; i++) + for (int i = 0; i < 4; i++) list.Add(new object()); return list.Count; } From ad3a9e5785d5a169a4bc529005c988f9d9c9d71e Mon Sep 17 00:00:00 2001 From: Martin Othamar Date: Sun, 31 Mar 2024 04:19:01 +0200 Subject: [PATCH 3/4] attempt to fix reinit logic for tool --- .../DotMemoryDiagnoser.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/BenchmarkDotNet.Diagnostics.dotMemory/DotMemoryDiagnoser.cs b/src/BenchmarkDotNet.Diagnostics.dotMemory/DotMemoryDiagnoser.cs index 69a5e952c2..d827c2eb42 100644 --- a/src/BenchmarkDotNet.Diagnostics.dotMemory/DotMemoryDiagnoser.cs +++ b/src/BenchmarkDotNet.Diagnostics.dotMemory/DotMemoryDiagnoser.cs @@ -44,10 +44,10 @@ public void Handle(HostSignal signal, DiagnoserActionParameters parameters) { var logger = parameters.Config.GetCompositeLogger(); var job = parameters.BenchmarkCase.Job; - if (tool is null) + bool isInProcess = job.GetToolchain().IsInProcess; + if (tool is null || (isInProcess ? tool is ExternalDotMemoryTool : tool is InProcessDotMemoryTool)) { - bool isInProcess = job.GetToolchain().IsInProcess; - tool ??= isInProcess + tool = isInProcess ? new InProcessDotMemoryTool(logger, nugetUrl, downloadTo: toolsDownloadFolder) : new ExternalDotMemoryTool(logger, nugetUrl, downloadTo: toolsDownloadFolder); } From 999dae773b2c3569c68f655c91acbf4d982ac9b2 Mon Sep 17 00:00:00 2001 From: Martin Othamar Date: Sun, 31 Mar 2024 18:00:36 +0200 Subject: [PATCH 4/4] Simplify implementation --- .../DotMemoryDiagnoser.cs | 22 +-- ...{DotMemoryToolBase.cs => DotMemoryTool.cs} | 34 +++-- .../ExternalDotMemoryTool.cs | 126 ------------------ .../InProcessDotMemoryTool.cs | 24 ---- .../DotMemoryTests.cs | 15 ++- 5 files changed, 47 insertions(+), 174 deletions(-) rename src/BenchmarkDotNet.Diagnostics.dotMemory/{DotMemoryToolBase.cs => DotMemoryTool.cs} (82%) delete mode 100644 src/BenchmarkDotNet.Diagnostics.dotMemory/ExternalDotMemoryTool.cs delete mode 100644 src/BenchmarkDotNet.Diagnostics.dotMemory/InProcessDotMemoryTool.cs diff --git a/src/BenchmarkDotNet.Diagnostics.dotMemory/DotMemoryDiagnoser.cs b/src/BenchmarkDotNet.Diagnostics.dotMemory/DotMemoryDiagnoser.cs index d827c2eb42..cbba52fb56 100644 --- a/src/BenchmarkDotNet.Diagnostics.dotMemory/DotMemoryDiagnoser.cs +++ b/src/BenchmarkDotNet.Diagnostics.dotMemory/DotMemoryDiagnoser.cs @@ -11,7 +11,6 @@ using BenchmarkDotNet.Portability; using BenchmarkDotNet.Reports; using BenchmarkDotNet.Running; -using BenchmarkDotNet.Toolchains; using BenchmarkDotNet.Validators; using RunMode = BenchmarkDotNet.Diagnosers.RunMode; @@ -22,7 +21,7 @@ public class DotMemoryDiagnoser : IProfiler private readonly Uri? nugetUrl; private readonly string? toolsDownloadFolder; - private DotMemoryToolBase? tool; + private DotMemoryTool? tool; public DotMemoryDiagnoser(Uri? nugetUrl = null, string? toolsDownloadFolder = null) { @@ -44,13 +43,6 @@ public void Handle(HostSignal signal, DiagnoserActionParameters parameters) { var logger = parameters.Config.GetCompositeLogger(); var job = parameters.BenchmarkCase.Job; - bool isInProcess = job.GetToolchain().IsInProcess; - if (tool is null || (isInProcess ? tool is ExternalDotMemoryTool : tool is InProcessDotMemoryTool)) - { - tool = isInProcess - ? new InProcessDotMemoryTool(logger, nugetUrl, downloadTo: toolsDownloadFolder) - : new ExternalDotMemoryTool(logger, nugetUrl, downloadTo: toolsDownloadFolder); - } var runtimeMoniker = job.Environment.GetRuntime().RuntimeMoniker; if (!IsSupported(runtimeMoniker)) @@ -62,13 +54,21 @@ public void Handle(HostSignal signal, DiagnoserActionParameters parameters) switch (signal) { case HostSignal.BeforeAnythingElse: - tool.Init(parameters); + if (tool is not null) + throw new InvalidOperationException("DotMemory tool is already initialized"); + tool = new DotMemoryTool(logger, nugetUrl, downloadTo: toolsDownloadFolder); + tool.Init(); break; case HostSignal.BeforeActualRun: + if (tool is null) + throw new InvalidOperationException("DotMemory tool is not initialized"); snapshotFilePaths.Add(tool.Start(parameters)); break; case HostSignal.AfterActualRun: - tool.Stop(parameters); + if (tool is null) + throw new InvalidOperationException("DotMemory tool is not initialized"); + tool.Stop(); + tool = null; break; } } diff --git a/src/BenchmarkDotNet.Diagnostics.dotMemory/DotMemoryToolBase.cs b/src/BenchmarkDotNet.Diagnostics.dotMemory/DotMemoryTool.cs similarity index 82% rename from src/BenchmarkDotNet.Diagnostics.dotMemory/DotMemoryToolBase.cs rename to src/BenchmarkDotNet.Diagnostics.dotMemory/DotMemoryTool.cs index 891cf2b6c3..1e640d6708 100644 --- a/src/BenchmarkDotNet.Diagnostics.dotMemory/DotMemoryToolBase.cs +++ b/src/BenchmarkDotNet.Diagnostics.dotMemory/DotMemoryTool.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics; using System.IO; using System.Reflection; using BenchmarkDotNet.Diagnosers; @@ -8,14 +9,14 @@ namespace BenchmarkDotNet.Diagnostics.dotMemory { - internal abstract class DotMemoryToolBase + internal sealed class DotMemoryTool { private readonly ILogger logger; private readonly Uri? nugetUrl; private readonly NuGetApi nugetApi; private readonly string? downloadTo; - protected DotMemoryToolBase(ILogger logger, Uri? nugetUrl = null, NuGetApi nugetApi = NuGetApi.V3, string? downloadTo = null) + public DotMemoryTool(ILogger logger, Uri? nugetUrl = null, NuGetApi nugetApi = NuGetApi.V3, string? downloadTo = null) { this.logger = logger; this.nugetUrl = nugetUrl; @@ -23,7 +24,7 @@ protected DotMemoryToolBase(ILogger logger, Uri? nugetUrl = null, NuGetApi nuget this.downloadTo = downloadTo; } - public void Init(DiagnoserActionParameters parameters) + public void Init() { try { @@ -39,10 +40,6 @@ public void Init(DiagnoserActionParameters parameters) } } - protected abstract void Attach(DiagnoserActionParameters parameters, string snapshotFile); - protected abstract void Snapshot(DiagnoserActionParameters parameters); - protected abstract void Detach(); - public string Start(DiagnoserActionParameters parameters) { string snapshotFile = ArtifactFileNameHelper.GetFilePath(parameters, "snapshots", DateTime.Now, "dmw", ".0000".Length); @@ -76,12 +73,12 @@ public string Start(DiagnoserActionParameters parameters) return snapshotFile; } - public void Stop(DiagnoserActionParameters parameters) + public void Stop() { try { logger.WriteLineInfo("Taking dotMemory snapshot..."); - Snapshot(parameters); + Snapshot(); logger.WriteLineInfo("dotMemory snapshot is successfully taken"); } catch (Exception e) @@ -101,7 +98,24 @@ public void Stop(DiagnoserActionParameters parameters) } } - protected string GetRunnerPath() + private void Attach(DiagnoserActionParameters parameters, string snapshotFile) + { + var config = new DotMemory.Config(); + + var pid = parameters.Process.Id; + var currentPid = Process.GetCurrentProcess().Id; + if (pid != currentPid) + config = config.ProfileExternalProcess(pid); + + config = config.SaveToFile(snapshotFile); + DotMemory.Attach(config); + } + + private void Snapshot() => DotMemory.GetSnapshot(); + + private void Detach() => DotMemory.Detach(); + + private string GetRunnerPath() { var consoleRunnerPackageField = typeof(DotMemory).GetField("ConsoleRunnerPackage", BindingFlags.NonPublic | BindingFlags.Static); if (consoleRunnerPackageField == null) diff --git a/src/BenchmarkDotNet.Diagnostics.dotMemory/ExternalDotMemoryTool.cs b/src/BenchmarkDotNet.Diagnostics.dotMemory/ExternalDotMemoryTool.cs deleted file mode 100644 index b14eede2c9..0000000000 --- a/src/BenchmarkDotNet.Diagnostics.dotMemory/ExternalDotMemoryTool.cs +++ /dev/null @@ -1,126 +0,0 @@ -using System; -using System.Diagnostics; -using System.Threading.Tasks; -using BenchmarkDotNet.Diagnosers; -using BenchmarkDotNet.Loggers; -using JetBrains.Profiler.SelfApi; -using ILogger = BenchmarkDotNet.Loggers.ILogger; - -namespace BenchmarkDotNet.Diagnostics.dotMemory -{ - internal class ExternalDotMemoryTool : DotMemoryToolBase - { - private static readonly TimeSpan AttachTimeout = TimeSpan.FromMinutes(5); - private static readonly TimeSpan SnapshotTimeout = TimeSpan.FromSeconds(30); - - private Process? process; - private TaskCompletionSource? snapshotWaitingTask; - - public ExternalDotMemoryTool(ILogger logger, Uri? nugetUrl = null, NuGetApi nugetApi = NuGetApi.V3, string? downloadTo = null) : - base(logger, nugetUrl, nugetApi, downloadTo) { } - - protected override void Attach(DiagnoserActionParameters parameters, string snapshotFile) - { - var logger = parameters.Config.GetCompositeLogger(); - - string runnerPath = GetRunnerPath(); - int pid = parameters.Process.Id; - string arguments = $"attach {pid} --save-to-file=\"{snapshotFile}\" --service-output"; - - logger.WriteLineInfo($"Starting process: '{runnerPath} {arguments}'"); - - var processStartInfo = new ProcessStartInfo - { - FileName = runnerPath, - WorkingDirectory = "", - Arguments = arguments, - UseShellExecute = false, - CreateNoWindow = true, - RedirectStandardOutput = true, - RedirectStandardError = true, - RedirectStandardInput = true, - }; - - var attachWaitingTask = new TaskCompletionSource(); - snapshotWaitingTask = new TaskCompletionSource(); - process = new Process { StartInfo = processStartInfo }; - try - { - process.OutputDataReceived += (_, args) => - { - string? content = args.Data; - if (content != null) - { - logger.WriteLineInfo("[dotMemory] " + content); - if (content.Contains("##dotMemory[\"connected\"")) - attachWaitingTask.TrySetResult(true); - - if (content.Contains("##dotMemory[\"snapshot-saved\"")) - snapshotWaitingTask.TrySetResult(true); - } - }; - process.ErrorDataReceived += (_, args) => - { - string? content = args.Data; - if (content != null) - logger.WriteLineError("[dotMemory] " + args.Data); - }; - process.Exited += (_, _) => - { - attachWaitingTask.TrySetResult(false); - snapshotWaitingTask.TrySetResult(false); - }; - process.Start(); - process.StandardInput.AutoFlush = true; - process.BeginOutputReadLine(); - process.BeginErrorReadLine(); - } - catch (Exception e) - { - attachWaitingTask.TrySetResult(false); - logger.WriteLineError(e.ToString()); - } - - if (!attachWaitingTask.Task.Wait(AttachTimeout)) - throw new Exception($"Failed to attach dotMemory to the target process (timeout: {AttachTimeout.TotalSeconds} sec)"); - if (!attachWaitingTask.Task.Result) - throw new Exception($"Failed to attach dotMemory to the target process (ExitCode={process.ExitCode})"); - } - - protected override void Snapshot(DiagnoserActionParameters parameters) - { - if (process is null || snapshotWaitingTask is null) - throw new InvalidOperationException("dotMemory process is not attached"); - - if (snapshotWaitingTask.Task.IsCompleted) - { - if (!snapshotWaitingTask.Task.Result) - throw new InvalidOperationException($"Can't create dotMemory snapshot, dotMemory process exited (ExitCode={process.ExitCode})"); - - snapshotWaitingTask = new TaskCompletionSource(); - } - - int pid = parameters.Process.Id; - - process.StandardInput.WriteLine($"##dotMemory[\"get-snapshot\", {{pid:{pid}}}]"); - - if (!snapshotWaitingTask.Task.Wait(SnapshotTimeout)) - throw new Exception($"Failed to create dotMemory snapshot (timeout: {SnapshotTimeout.TotalSeconds} sec)"); - if (!snapshotWaitingTask.Task.Result) - throw new Exception($"Failed to create dotMemory snapshot (ExitCode={process.ExitCode})"); - } - - protected override void Detach() - { - if (process is null) - throw new InvalidOperationException("dotMemory process is not attached"); - - process.StandardInput.WriteLine("##dotMemory[\"disconnect\"]"); - process.StandardInput.Close(); - process.WaitForExit(); - - process.Dispose(); - process = null; - } - } -} \ No newline at end of file diff --git a/src/BenchmarkDotNet.Diagnostics.dotMemory/InProcessDotMemoryTool.cs b/src/BenchmarkDotNet.Diagnostics.dotMemory/InProcessDotMemoryTool.cs deleted file mode 100644 index c80b815e86..0000000000 --- a/src/BenchmarkDotNet.Diagnostics.dotMemory/InProcessDotMemoryTool.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System; -using BenchmarkDotNet.Diagnosers; -using BenchmarkDotNet.Loggers; -using JetBrains.Profiler.SelfApi; - -namespace BenchmarkDotNet.Diagnostics.dotMemory -{ - internal class InProcessDotMemoryTool : DotMemoryToolBase - { - public InProcessDotMemoryTool(ILogger logger, Uri? nugetUrl = null, NuGetApi nugetApi = NuGetApi.V3, string? downloadTo = null) : - base(logger, nugetUrl, nugetApi, downloadTo) { } - - protected override void Attach(DiagnoserActionParameters parameters, string snapshotFile) - { - var config = new DotMemory.Config(); - config.SaveToFile(snapshotFile); - DotMemory.Attach(config); - } - - protected override void Snapshot(DiagnoserActionParameters parameters) => DotMemory.GetSnapshot(); - - protected override void Detach() => DotMemory.Detach(); - } -} \ No newline at end of file diff --git a/tests/BenchmarkDotNet.IntegrationTests/DotMemoryTests.cs b/tests/BenchmarkDotNet.IntegrationTests/DotMemoryTests.cs index 4b18c74dbc..1e8dc79d00 100644 --- a/tests/BenchmarkDotNet.IntegrationTests/DotMemoryTests.cs +++ b/tests/BenchmarkDotNet.IntegrationTests/DotMemoryTests.cs @@ -46,17 +46,26 @@ public void DotMemorySmokeTest() Output.WriteLine("Snapshots:"); foreach (string snapshot in snapshots) Output.WriteLine("* " + snapshot); - Assert.Equal(2, snapshots.Count); + Assert.Equal(4, snapshots.Count); } [DotMemoryDiagnoser] public class Benchmarks { [Benchmark] - public int Foo() + public int Foo0() { var list = new List(); - for (int i = 0; i < 4; i++) + for (int i = 0; i < 1000; i++) + list.Add(new object()); + return list.Count; + } + + [Benchmark] + public int Foo1() + { + var list = new List(); + for (int i = 0; i < 1000; i++) list.Add(new object()); return list.Count; }