Skip to content

DotMemoryDiagnoser implementation #2549

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions BenchmarkDotNet.sln
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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}
Expand Down
1 change: 1 addition & 0 deletions docs/articles/guides/nuget.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
22 changes: 22 additions & 0 deletions docs/articles/samples/IntroDotMemoryDiagnoser.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
---
uid: BenchmarkDotNet.Samples.IntroDotMemoryDiagnoser
---

## Sample: IntroDotMemoryDiagnoser

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.
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

---
2 changes: 2 additions & 0 deletions docs/articles/samples/toc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@
href: IntroDisassemblyRyuJit.md
- name: IntroDotTraceDiagnoser
href: IntroDotTraceDiagnoser.md
- name: IntroDotMemoryDiagnoser
href: IntroDotMemoryDiagnoser.md
- name: IntroEnvVars
href: IntroEnvVars.md
- name: IntroEventPipeProfiler
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\BenchmarkDotNet.Diagnostics.dotTrace\BenchmarkDotNet.Diagnostics.dotTrace.csproj" />
<ProjectReference Include="..\..\src\BenchmarkDotNet.Diagnostics.dotMemory\BenchmarkDotNet.Diagnostics.dotMemory.csproj" />
<ProjectReference Include="..\..\src\BenchmarkDotNet\BenchmarkDotNet.csproj" />
<ProjectReference Include="..\..\src\BenchmarkDotNet.Diagnostics.Windows\BenchmarkDotNet.Diagnostics.Windows.csproj" />
<ProjectReference Include="..\..\src\BenchmarkDotNet.TestAdapter\BenchmarkDotNet.TestAdapter.csproj" />
Expand Down
52 changes: 52 additions & 0 deletions samples/BenchmarkDotNet.Samples/IntroDotMemoryDiagnoser.cs
Original file line number Diff line number Diff line change
@@ -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<byte> 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;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\build\common.props" />
<PropertyGroup>
<TargetFrameworks>net6.0;net462;netcoreapp3.1</TargetFrameworks>
<NoWarn>$(NoWarn);1591</NoWarn>
<AssemblyTitle>BenchmarkDotNet.Diagnostics.dotMemory</AssemblyTitle>
<AssemblyName>BenchmarkDotNet.Diagnostics.dotMemory</AssemblyName>
<PackageId>BenchmarkDotNet.Diagnostics.dotMemory</PackageId>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\BenchmarkDotNet\BenchmarkDotNet.csproj" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="JetBrains.Profiler.SelfApi" Version="2.5.0" />
</ItemGroup>

</Project>
156 changes: 156 additions & 0 deletions src/BenchmarkDotNet.Diagnostics.dotMemory/DotMemoryDiagnoser.cs
Original file line number Diff line number Diff line change
@@ -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.Validators;
using RunMode = BenchmarkDotNet.Diagnosers.RunMode;

namespace BenchmarkDotNet.Diagnostics.dotMemory
{
public class DotMemoryDiagnoser : IProfiler
{
private readonly Uri? nugetUrl;
private readonly string? toolsDownloadFolder;

private DotMemoryTool? tool;

public DotMemoryDiagnoser(Uri? nugetUrl = null, string? toolsDownloadFolder = null)
{
this.nugetUrl = nugetUrl;
this.toolsDownloadFolder = toolsDownloadFolder;
}

public IEnumerable<string> 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<string> snapshotFilePaths = new ();

public void Handle(HostSignal signal, DiagnoserActionParameters parameters)
{
var logger = parameters.Config.GetCompositeLogger();
var job = parameters.BenchmarkCase.Job;

var runtimeMoniker = job.Environment.GetRuntime().RuntimeMoniker;
if (!IsSupported(runtimeMoniker))
{
logger.WriteLineError($"Runtime '{runtimeMoniker}' is not supported by dotMemory");
return;
}

switch (signal)
{
case HostSignal.BeforeAnythingElse:
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:
if (tool is null)
throw new InvalidOperationException("DotMemory tool is not initialized");
tool.Stop();
tool = null;
break;
}
}

public IEnumerable<IExporter> Exporters => Enumerable.Empty<IExporter>();
public IEnumerable<IAnalyser> Analysers => Enumerable.Empty<IAnalyser>();

public IEnumerable<ValidationError> 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<Metric> ProcessResults(DiagnoserResults results) => ImmutableArray<Metric>.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}");
}
}
}
}
Original file line number Diff line number Diff line change
@@ -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));
}
}
}
Loading