diff --git a/BenchmarkDotNet.sln b/BenchmarkDotNet.sln
index 6ae36196aa..9537707749 100644
--- a/BenchmarkDotNet.sln
+++ b/BenchmarkDotNet.sln
@@ -47,6 +47,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "templates", "templates", "{
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BenchmarkDotNet.Templates", "templates\BenchmarkDotNet.Templates.csproj", "{B620D10A-CD8E-4A34-8B27-FD6257E63AD0}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BenchmarkDotNet.Diagnostics.dotTrace", "src\BenchmarkDotNet.Diagnostics.dotTrace\BenchmarkDotNet.Diagnostics.dotTrace.csproj", "{C5BDA61F-3A56-4B59-901D-0A17E78F4076}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -125,6 +127,10 @@ Global
{B620D10A-CD8E-4A34-8B27-FD6257E63AD0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B620D10A-CD8E-4A34-8B27-FD6257E63AD0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B620D10A-CD8E-4A34-8B27-FD6257E63AD0}.Release|Any CPU.Build.0 = Release|Any CPU
+ {C5BDA61F-3A56-4B59-901D-0A17E78F4076}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {C5BDA61F-3A56-4B59-901D-0A17E78F4076}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {C5BDA61F-3A56-4B59-901D-0A17E78F4076}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {C5BDA61F-3A56-4B59-901D-0A17E78F4076}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -148,6 +154,7 @@ Global
{B4405781-40D3-42B8-B168-00E711FABA15} = {14195214-591A-45B7-851A-19D3BA2413F9}
{D9F5065B-6190-431B-850C-117E3D64AB33} = {D6597E3A-6892-4A68-8E14-042FC941FDA2}
{B620D10A-CD8E-4A34-8B27-FD6257E63AD0} = {63B94FD6-3F3D-4E04-9727-48E86AC4384C}
+ {C5BDA61F-3A56-4B59-901D-0A17E78F4076} = {D6597E3A-6892-4A68-8E14-042FC941FDA2}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {4D9AF12B-1F7F-45A7-9E8C-E4E46ADCBD1F}
diff --git a/docs/articles/samples/IntroDotTraceDiagnoser.md b/docs/articles/samples/IntroDotTraceDiagnoser.md
new file mode 100644
index 0000000000..cf6028b3a1
--- /dev/null
+++ b/docs/articles/samples/IntroDotTraceDiagnoser.md
@@ -0,0 +1,22 @@
+---
+uid: BenchmarkDotNet.Samples.IntroDotTraceDiagnoser
+---
+
+## Sample: IntroDotTraceDiagnoser
+
+If you want to get a performance profile of your benchmarks, just add the `[DotTraceDiagnoser]` attribute, as shown below.
+As a result, BenchmarkDotNet performs bonus benchmark runs using attached
+ [dotTrace Command-Line Profiler](https://www.jetbrains.com/help/profiler/Performance_Profiling__Profiling_Using_the_Command_Line.html).
+The obtained snapshots are saved to the `artifacts` folder.
+These snapshots can be opened using the [standalone dotTrace](https://www.jetbrains.com/profiler/),
+ or [dotTrace in Rider](https://www.jetbrains.com/help/rider/Performance_Profiling.html).
+
+### Source code
+
+[!code-csharp[IntroDotTraceDiagnoser.cs](../../../samples/BenchmarkDotNet.Samples/IntroDotTraceDiagnoser.cs)]
+
+### Links
+
+* The permanent link to this sample: @BenchmarkDotNet.Samples.IntroDotTraceDiagnoser
+
+---
\ No newline at end of file
diff --git a/docs/articles/samples/toc.yml b/docs/articles/samples/toc.yml
index e5c84fee1d..8098a637c3 100644
--- a/docs/articles/samples/toc.yml
+++ b/docs/articles/samples/toc.yml
@@ -36,6 +36,8 @@
href: IntroDisassemblyDry.md
- name: IntroDisassemblyRyuJit
href: IntroDisassemblyRyuJit.md
+- name: IntroDotTraceDiagnoser
+ href: IntroDotTraceDiagnoser.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 5dcabeb9f7..180148454a 100644
--- a/samples/BenchmarkDotNet.Samples/BenchmarkDotNet.Samples.csproj
+++ b/samples/BenchmarkDotNet.Samples/BenchmarkDotNet.Samples.csproj
@@ -21,6 +21,7 @@
+
diff --git a/samples/BenchmarkDotNet.Samples/IntroDotTraceDiagnoser.cs b/samples/BenchmarkDotNet.Samples/IntroDotTraceDiagnoser.cs
new file mode 100644
index 0000000000..351207c78b
--- /dev/null
+++ b/samples/BenchmarkDotNet.Samples/IntroDotTraceDiagnoser.cs
@@ -0,0 +1,26 @@
+using BenchmarkDotNet.Attributes;
+using BenchmarkDotNet.Diagnostics.dotTrace;
+
+namespace BenchmarkDotNet.Samples
+{
+ // Enables dotTrace profiling for all jobs
+ [DotTraceDiagnoser]
+ // Adds the default "external-process" job
+ // Profiling is performed using dotTrace command-line Tools
+ // See: https://www.jetbrains.com/help/profiler/Performance_Profiling__Profiling_Using_the_Command_Line.html
+ [SimpleJob]
+ // Adds an "in-process" job
+ // Profiling is performed using dotTrace SelfApi
+ // NuGet reference: https://www.nuget.org/packages/JetBrains.Profiler.SelfApi
+ [InProcess]
+ public class IntroDotTraceDiagnoser
+ {
+ [Benchmark]
+ public void Fibonacci() => Fibonacci(30);
+
+ private static int Fibonacci(int n)
+ {
+ return n <= 1 ? n : Fibonacci(n - 1) + Fibonacci(n - 2);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/BenchmarkDotNet.Diagnostics.dotTrace/BenchmarkDotNet.Diagnostics.dotTrace.csproj b/src/BenchmarkDotNet.Diagnostics.dotTrace/BenchmarkDotNet.Diagnostics.dotTrace.csproj
new file mode 100644
index 0000000000..65418cd9d1
--- /dev/null
+++ b/src/BenchmarkDotNet.Diagnostics.dotTrace/BenchmarkDotNet.Diagnostics.dotTrace.csproj
@@ -0,0 +1,19 @@
+
+
+
+ net6.0;net462;netcoreapp3.1
+ $(NoWarn);1591
+ BenchmarkDotNet.Diagnostics.dotTrace
+ BenchmarkDotNet.Diagnostics.dotTrace
+ BenchmarkDotNet.Diagnostics.dotTrace
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/BenchmarkDotNet.Diagnostics.dotTrace/DotTraceDiagnoser.cs b/src/BenchmarkDotNet.Diagnostics.dotTrace/DotTraceDiagnoser.cs
new file mode 100644
index 0000000000..17d8108753
--- /dev/null
+++ b/src/BenchmarkDotNet.Diagnostics.dotTrace/DotTraceDiagnoser.cs
@@ -0,0 +1,148 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.IO;
+using System.Linq;
+using BenchmarkDotNet.Analysers;
+using BenchmarkDotNet.Diagnosers;
+using BenchmarkDotNet.Engines;
+using BenchmarkDotNet.Exporters;
+using BenchmarkDotNet.Extensions;
+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.dotTrace
+{
+ public class DotTraceDiagnoser : IProfiler
+ {
+ private readonly Uri nugetUrl;
+ private readonly string toolsDownloadFolder;
+
+ public DotTraceDiagnoser(Uri nugetUrl = null, string toolsDownloadFolder = null)
+ {
+ this.nugetUrl = nugetUrl;
+ this.toolsDownloadFolder = toolsDownloadFolder;
+ }
+
+ public IEnumerable Ids => new[] { "DotTrace" };
+ public string ShortName => "dotTrace";
+
+ 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 job = parameters.BenchmarkCase.Job;
+ bool isInProcess = job.GetToolchain().IsInProcess;
+ var logger = parameters.Config.GetCompositeLogger();
+ DotTraceToolBase tool = isInProcess
+ ? new InProcessDotTraceTool(logger, nugetUrl, downloadTo: toolsDownloadFolder)
+ : new ExternalDotTraceTool(logger, nugetUrl, downloadTo: toolsDownloadFolder);
+
+ var runtimeMoniker = job.Environment.GetRuntime().RuntimeMoniker;
+ if (!IsSupported(runtimeMoniker))
+ {
+ logger.WriteLineError($"Runtime '{runtimeMoniker}' is not supported by dotTrace");
+ 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 dotTrace");
+ }
+ }
+
+ 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:
+ return true;
+ case RuntimeMoniker.NotRecognized:
+ case RuntimeMoniker.Mono:
+ case RuntimeMoniker.NativeAot60:
+ case RuntimeMoniker.NativeAot70:
+ case RuntimeMoniker.NativeAot80:
+ case RuntimeMoniker.Wasm:
+ case RuntimeMoniker.WasmNet50:
+ case RuntimeMoniker.WasmNet60:
+ case RuntimeMoniker.WasmNet70:
+ case RuntimeMoniker.WasmNet80:
+ case RuntimeMoniker.MonoAOTLLVM:
+ case RuntimeMoniker.MonoAOTLLVMNet60:
+ case RuntimeMoniker.MonoAOTLLVMNet70:
+ case RuntimeMoniker.MonoAOTLLVMNet80:
+ case RuntimeMoniker.Mono60:
+ case RuntimeMoniker.Mono70:
+ case RuntimeMoniker.Mono80:
+#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 dotTrace snapshots were generated:");
+ foreach (string snapshotFilePath in snapshotFilePaths)
+ logger.WriteLineInfo($"* {snapshotFilePath}");
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/BenchmarkDotNet.Diagnostics.dotTrace/DotTraceDiagnoserAttribute.cs b/src/BenchmarkDotNet.Diagnostics.dotTrace/DotTraceDiagnoserAttribute.cs
new file mode 100644
index 0000000000..de803e6443
--- /dev/null
+++ b/src/BenchmarkDotNet.Diagnostics.dotTrace/DotTraceDiagnoserAttribute.cs
@@ -0,0 +1,21 @@
+using System;
+using BenchmarkDotNet.Configs;
+
+namespace BenchmarkDotNet.Diagnostics.dotTrace
+{
+ [AttributeUsage(AttributeTargets.Class)]
+ public class DotTraceDiagnoserAttribute : Attribute, IConfigSource
+ {
+ public IConfig Config { get; }
+
+ public DotTraceDiagnoserAttribute()
+ {
+ Config = ManualConfig.CreateEmpty().AddDiagnoser(new DotTraceDiagnoser());
+ }
+
+ public DotTraceDiagnoserAttribute(Uri nugetUrl = null, string toolsDownloadFolder = null)
+ {
+ Config = ManualConfig.CreateEmpty().AddDiagnoser(new DotTraceDiagnoser(nugetUrl, toolsDownloadFolder));
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/BenchmarkDotNet.Diagnostics.dotTrace/DotTraceToolBase.cs b/src/BenchmarkDotNet.Diagnostics.dotTrace/DotTraceToolBase.cs
new file mode 100644
index 0000000000..c41ffc53e5
--- /dev/null
+++ b/src/BenchmarkDotNet.Diagnostics.dotTrace/DotTraceToolBase.cs
@@ -0,0 +1,145 @@
+using System;
+using System.IO;
+using System.Reflection;
+using BenchmarkDotNet.Diagnosers;
+using BenchmarkDotNet.Helpers;
+using BenchmarkDotNet.Loggers;
+using JetBrains.Profiler.SelfApi;
+
+namespace BenchmarkDotNet.Diagnostics.dotTrace
+{
+ internal abstract class DotTraceToolBase
+ {
+ private readonly ILogger logger;
+ private readonly Uri nugetUrl;
+ private readonly NuGetApi nugetApi;
+ private readonly string downloadTo;
+
+ protected DotTraceToolBase(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 dotTrace prerequisite is installed...");
+ var progress = new Progress(logger, "Installing DotTrace");
+ DotTrace.EnsurePrerequisiteAsync(progress, nugetUrl, nugetApi, downloadTo).Wait();
+ logger.WriteLineInfo("dotTrace prerequisite is installed");
+ logger.WriteLineInfo($"dotTrace runner path: {GetRunnerPath()}");
+ }
+ catch (Exception e)
+ {
+ logger.WriteLineError(e.ToString());
+ }
+ }
+
+ protected abstract bool AttachOnly { get; }
+ protected abstract void Attach(DiagnoserActionParameters parameters, string snapshotFile);
+ protected abstract void StartCollectingData();
+ protected abstract void SaveData();
+ protected abstract void Detach();
+
+ public string Start(DiagnoserActionParameters parameters)
+ {
+ string snapshotFile = ArtifactFileNameHelper.GetFilePath(parameters, "snapshots", DateTime.Now, "dtp", ".0000".Length);
+ string snapshotDirectory = Path.GetDirectoryName(snapshotFile);
+ logger.WriteLineInfo($"Target snapshot file: {snapshotFile}");
+ if (!Directory.Exists(snapshotDirectory))
+ {
+ try
+ {
+ Directory.CreateDirectory(snapshotDirectory);
+ }
+ catch (Exception e)
+ {
+ logger.WriteLineError($"Failed to create directory: {snapshotDirectory}");
+ logger.WriteLineError(e.ToString());
+ }
+ }
+
+ try
+ {
+ logger.WriteLineInfo("Attaching dotTrace to the process...");
+ Attach(parameters, snapshotFile);
+ logger.WriteLineInfo("dotTrace is successfully attached");
+ }
+ catch (Exception e)
+ {
+ logger.WriteLineError(e.ToString());
+ return snapshotFile;
+ }
+
+ if (!AttachOnly)
+ {
+ try
+ {
+ logger.WriteLineInfo("Start collecting data using dataTrace...");
+ StartCollectingData();
+ logger.WriteLineInfo("Data collecting is successfully started");
+ }
+ catch (Exception e)
+ {
+ logger.WriteLineError(e.ToString());
+ }
+ }
+
+ return snapshotFile;
+ }
+
+ public void Stop(DiagnoserActionParameters parameters)
+ {
+ if (!AttachOnly)
+ {
+ try
+ {
+ logger.WriteLineInfo("Saving dotTrace snapshot...");
+ SaveData();
+ logger.WriteLineInfo("dotTrace snapshot is successfully saved to the artifact folder");
+ }
+ catch (Exception e)
+ {
+ logger.WriteLineError(e.ToString());
+ }
+
+ try
+ {
+ logger.WriteLineInfo("Detaching dotTrace from the process...");
+ Detach();
+ logger.WriteLineInfo("dotTrace is successfully detached");
+ }
+ catch (Exception e)
+ {
+ logger.WriteLineError(e.ToString());
+ }
+ }
+ }
+
+ protected string GetRunnerPath()
+ {
+ var consoleRunnerPackageField = typeof(DotTrace).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.dotTrace/ExternalDotTraceTool.cs b/src/BenchmarkDotNet.Diagnostics.dotTrace/ExternalDotTraceTool.cs
new file mode 100644
index 0000000000..e3b0df28a4
--- /dev/null
+++ b/src/BenchmarkDotNet.Diagnostics.dotTrace/ExternalDotTraceTool.cs
@@ -0,0 +1,86 @@
+using System;
+using System.Diagnostics;
+using System.Threading;
+using System.Threading.Tasks;
+using BenchmarkDotNet.Diagnosers;
+using BenchmarkDotNet.Helpers;
+using BenchmarkDotNet.Loggers;
+using JetBrains.Profiler.SelfApi;
+using ILogger = BenchmarkDotNet.Loggers.ILogger;
+
+namespace BenchmarkDotNet.Diagnostics.dotTrace
+{
+ internal class ExternalDotTraceTool : DotTraceToolBase
+ {
+ private static readonly TimeSpan AttachTimeout = TimeSpan.FromMinutes(5);
+
+ public ExternalDotTraceTool(ILogger logger, Uri nugetUrl = null, NuGetApi nugetApi = NuGetApi.V3, string downloadTo = null) :
+ base(logger, nugetUrl, nugetApi, downloadTo) { }
+
+ protected override bool AttachOnly => true;
+
+ 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=\"{snapshotFile}\" --service-output=on";
+
+ logger.WriteLineInfo($"Starting process: '{runnerPath} {arguments}'");
+
+ var processStartInfo = new ProcessStartInfo
+ {
+ FileName = runnerPath,
+ WorkingDirectory = "",
+ Arguments = arguments,
+ UseShellExecute = false,
+ CreateNoWindow = true,
+ RedirectStandardOutput = true,
+ RedirectStandardError = true
+ };
+
+ var attachWaitingTask = new TaskCompletionSource();
+ var process = new Process { StartInfo = processStartInfo };
+ try
+ {
+ process.OutputDataReceived += (_, args) =>
+ {
+ string content = args.Data;
+ if (content != null)
+ {
+ logger.WriteLineInfo("[dotTrace] " + content);
+ if (content.Contains("##dotTrace[\"started\""))
+ attachWaitingTask.TrySetResult(true);
+ }
+ };
+ process.ErrorDataReceived += (_, args) =>
+ {
+ string content = args.Data;
+ if (content != null)
+ logger.WriteLineError("[dotTrace] " + args.Data);
+ };
+ process.Exited += (_, _) => { attachWaitingTask.TrySetResult(false); };
+ process.Start();
+ 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 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})");
+ }
+
+ protected override void StartCollectingData() { }
+
+ protected override void SaveData() { }
+
+ protected override void Detach() { }
+ }
+}
\ No newline at end of file
diff --git a/src/BenchmarkDotNet.Diagnostics.dotTrace/InProcessDotTraceTool.cs b/src/BenchmarkDotNet.Diagnostics.dotTrace/InProcessDotTraceTool.cs
new file mode 100644
index 0000000000..a124e3e495
--- /dev/null
+++ b/src/BenchmarkDotNet.Diagnostics.dotTrace/InProcessDotTraceTool.cs
@@ -0,0 +1,28 @@
+using System;
+using BenchmarkDotNet.Diagnosers;
+using BenchmarkDotNet.Loggers;
+using JetBrains.Profiler.SelfApi;
+
+namespace BenchmarkDotNet.Diagnostics.dotTrace
+{
+ internal class InProcessDotTraceTool : DotTraceToolBase
+ {
+ public InProcessDotTraceTool(ILogger logger, Uri nugetUrl = null, NuGetApi nugetApi = NuGetApi.V3, string downloadTo = null) :
+ base(logger, nugetUrl, nugetApi, downloadTo) { }
+
+ protected override bool AttachOnly => false;
+
+ protected override void Attach(DiagnoserActionParameters parameters, string snapshotFile)
+ {
+ var config = new DotTrace.Config();
+ config.SaveToFile(snapshotFile);
+ DotTrace.Attach(config);
+ }
+
+ protected override void StartCollectingData() => DotTrace.StartCollectingData();
+
+ protected override void SaveData() => DotTrace.SaveData();
+
+ protected override void Detach() => DotTrace.Detach();
+ }
+}
\ No newline at end of file
diff --git a/src/BenchmarkDotNet.Diagnostics.dotTrace/Progress.cs b/src/BenchmarkDotNet.Diagnostics.dotTrace/Progress.cs
new file mode 100644
index 0000000000..1d8249f31e
--- /dev/null
+++ b/src/BenchmarkDotNet.Diagnostics.dotTrace/Progress.cs
@@ -0,0 +1,38 @@
+using System;
+using System.Diagnostics;
+using BenchmarkDotNet.Loggers;
+
+namespace BenchmarkDotNet.Diagnostics.dotTrace
+{
+ 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.dotTrace/Properties/AssemblyInfo.cs b/src/BenchmarkDotNet.Diagnostics.dotTrace/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000000..270fdc2c9c
--- /dev/null
+++ b/src/BenchmarkDotNet.Diagnostics.dotTrace/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/Helpers/ArtifactFileNameHelper.cs b/src/BenchmarkDotNet/Helpers/ArtifactFileNameHelper.cs
index 490d47c2ea..62b4d31d94 100644
--- a/src/BenchmarkDotNet/Helpers/ArtifactFileNameHelper.cs
+++ b/src/BenchmarkDotNet/Helpers/ArtifactFileNameHelper.cs
@@ -17,12 +17,17 @@ internal static class ArtifactFileNameHelper
internal static string GetTraceFilePath(DiagnoserActionParameters details, DateTime creationTime, string fileExtension)
{
- string nameNoLimit = GetFilePathNoLimits(details, creationTime, fileExtension);
+ return GetFilePath(details, null, creationTime, fileExtension, "userheap.etl".Length);
+ }
+
+ internal static string GetFilePath(DiagnoserActionParameters details, string? subfolder, DateTime? creationTime, string fileExtension, int reserve)
+ {
+ string nameNoLimit = GetFilePathNoLimits(details, subfolder, creationTime, fileExtension);
- // long paths can be enabled on Windows but it does not mean that ETW is going to work fine..
+ // long paths can be enabled on Windows but it does not mean that everything is going to work fine..
// so we always use 260 as limit on Windows
int limit = RuntimeInformation.IsWindows()
- ? WindowsOldPathLimit - "userheap.etl".Length // the session files get merged, they need to have same name (without extension)
+ ? WindowsOldPathLimit - reserve
: CommonSenseLimit;
if (nameNoLimit.Length <= limit)
@@ -30,17 +35,17 @@ internal static string GetTraceFilePath(DiagnoserActionParameters details, DateT
return nameNoLimit;
}
- return GetLimitedFilePath(details, creationTime, fileExtension, limit);
+ return GetLimitedFilePath(details, subfolder, creationTime, fileExtension, limit);
}
- private static string GetFilePathNoLimits(DiagnoserActionParameters details, DateTime creationTime, string fileExtension)
+ private static string GetFilePathNoLimits(DiagnoserActionParameters details, string? subfolder, DateTime? creationTime, string fileExtension)
{
string fileName = $@"{FolderNameHelper.ToFolderName(details.BenchmarkCase.Descriptor.Type)}.{FullNameProvider.GetMethodName(details.BenchmarkCase)}";
- return GetFilePath(fileName, details, creationTime, fileExtension);
+ return GetFilePath(fileName, details, subfolder, creationTime, fileExtension);
}
- private static string GetLimitedFilePath(DiagnoserActionParameters details, DateTime creationTime, string fileExtension, int limit)
+ private static string GetLimitedFilePath(DiagnoserActionParameters details, string? subfolder, DateTime? creationTime, string fileExtension, int limit)
{
string shortTypeName = FolderNameHelper.ToFolderName(details.BenchmarkCase.Descriptor.Type, includeNamespace: false);
string methodName = details.BenchmarkCase.Descriptor.WorkloadMethod.Name;
@@ -50,7 +55,7 @@ private static string GetLimitedFilePath(DiagnoserActionParameters details, Date
string fileName = $@"{shortTypeName}.{methodName}{parameters}";
- string finalResult = GetFilePath(fileName, details, creationTime, fileExtension);
+ string finalResult = GetFilePath(fileName, details, subfolder, creationTime, fileExtension);
if (finalResult.Length > limit)
{
@@ -61,20 +66,23 @@ private static string GetLimitedFilePath(DiagnoserActionParameters details, Date
return finalResult;
}
- private static string GetFilePath(string fileName, DiagnoserActionParameters details, DateTime creationTime, string fileExtension)
+ private static string GetFilePath(string fileName, DiagnoserActionParameters details, string? subfolder, DateTime? creationTime, string fileExtension)
{
// if we run for more than one toolchain, the output file name should contain the name too so we can differ net462 vs netcoreapp2.1 etc
if (details.Config.GetJobs().Select(job => ToolchainExtensions.GetToolchain(job)).Distinct().Count() > 1)
fileName += $"-{details.BenchmarkCase.Job.Environment.Runtime?.Name ?? details.BenchmarkCase.GetToolchain()?.Name ?? details.BenchmarkCase.Job.Id}";
- fileName += $"-{creationTime.ToString(BenchmarkRunnerClean.DateTimeFormat)}";
+ if (creationTime.HasValue)
+ fileName += $"-{creationTime.Value.ToString(BenchmarkRunnerClean.DateTimeFormat)}";
fileName = FolderNameHelper.ToFolderName(fileName);
if (!string.IsNullOrEmpty(fileExtension))
fileName = $"{fileName}.{fileExtension}";
- return Path.Combine(details.Config.ArtifactsPath, fileName);
+ return subfolder != null
+ ? Path.Combine(details.Config.ArtifactsPath, subfolder, fileName)
+ : Path.Combine(details.Config.ArtifactsPath, fileName);
}
}
}
diff --git a/src/BenchmarkDotNet/Properties/AssemblyInfo.cs b/src/BenchmarkDotNet/Properties/AssemblyInfo.cs
index 7e3f0231cd..21e7364a24 100644
--- a/src/BenchmarkDotNet/Properties/AssemblyInfo.cs
+++ b/src/BenchmarkDotNet/Properties/AssemblyInfo.cs
@@ -13,8 +13,10 @@
[assembly: InternalsVisibleTo("BenchmarkDotNet.Tests,PublicKey=" + BenchmarkDotNetInfo.PublicKey)]
[assembly: InternalsVisibleTo("BenchmarkDotNet.IntegrationTests,PublicKey=" + BenchmarkDotNetInfo.PublicKey)]
[assembly: InternalsVisibleTo("BenchmarkDotNet.Diagnostics.Windows,PublicKey=" + BenchmarkDotNetInfo.PublicKey)]
+[assembly: InternalsVisibleTo("BenchmarkDotNet.Diagnostics.dotTrace,PublicKey=" + BenchmarkDotNetInfo.PublicKey)]
#else
[assembly: InternalsVisibleTo("BenchmarkDotNet.Tests")]
[assembly: InternalsVisibleTo("BenchmarkDotNet.IntegrationTests")]
[assembly: InternalsVisibleTo("BenchmarkDotNet.Diagnostics.Windows")]
+[assembly: InternalsVisibleTo("BenchmarkDotNet.Diagnostics.dotTrace")]
#endif
\ No newline at end of file
diff --git a/tests/BenchmarkDotNet.IntegrationTests/BenchmarkDotNet.IntegrationTests.csproj b/tests/BenchmarkDotNet.IntegrationTests/BenchmarkDotNet.IntegrationTests.csproj
index fe44824ed6..1cad4d32b7 100644
--- a/tests/BenchmarkDotNet.IntegrationTests/BenchmarkDotNet.IntegrationTests.csproj
+++ b/tests/BenchmarkDotNet.IntegrationTests/BenchmarkDotNet.IntegrationTests.csproj
@@ -27,6 +27,7 @@
+
diff --git a/tests/BenchmarkDotNet.IntegrationTests/BenchmarkTestExecutor.cs b/tests/BenchmarkDotNet.IntegrationTests/BenchmarkTestExecutor.cs
index daf62c5643..032e2643d8 100644
--- a/tests/BenchmarkDotNet.IntegrationTests/BenchmarkTestExecutor.cs
+++ b/tests/BenchmarkDotNet.IntegrationTests/BenchmarkTestExecutor.cs
@@ -58,6 +58,8 @@ protected Reports.Summary CanExecute(Type type, IConfig config = null, bool full
if (!config.GetLoggers().OfType().Any())
config = config.AddLogger(Output != null ? new OutputLogger(Output) : ConsoleLogger.Default);
+ if (!config.GetLoggers().OfType().Any())
+ config = config.AddLogger(ConsoleLogger.Default);
if (!config.GetColumnProviders().Any())
config = config.AddColumnProvider(DefaultColumnProviders.Instance);
diff --git a/tests/BenchmarkDotNet.IntegrationTests/DotTraceTests.cs b/tests/BenchmarkDotNet.IntegrationTests/DotTraceTests.cs
new file mode 100644
index 0000000000..80f11519e8
--- /dev/null
+++ b/tests/BenchmarkDotNet.IntegrationTests/DotTraceTests.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.dotTrace;
+using BenchmarkDotNet.Jobs;
+using BenchmarkDotNet.Portability;
+using BenchmarkDotNet.Toolchains.InProcess.Emit;
+using Xunit;
+using Xunit.Abstractions;
+
+namespace BenchmarkDotNet.IntegrationTests
+{
+ public class DotTraceTests : BenchmarkTestExecutor
+ {
+ public DotTraceTests(ITestOutputHelper output) : base(output) { }
+
+ [Fact]
+ public void DotTraceSmokeTest()
+ {
+ 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(".dtp", 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);
+ }
+
+ [DotTraceDiagnoser]
+ public class Benchmarks
+ {
+ [Benchmark]
+ public int Foo()
+ {
+ var list = new List
+
diff --git a/tests/BenchmarkDotNet.Tests/dotTrace/DotTraceTests.cs b/tests/BenchmarkDotNet.Tests/dotTrace/DotTraceTests.cs
new file mode 100644
index 0000000000..a71cae2aef
--- /dev/null
+++ b/tests/BenchmarkDotNet.Tests/dotTrace/DotTraceTests.cs
@@ -0,0 +1,17 @@
+using System;
+using BenchmarkDotNet.Diagnostics.dotTrace;
+using BenchmarkDotNet.Jobs;
+using Xunit;
+
+namespace BenchmarkDotNet.Tests.dotTrace
+{
+ public class DotTraceTests
+ {
+ [Fact]
+ public void AllRuntimeMonikerAreKnown()
+ {
+ foreach (RuntimeMoniker moniker in Enum.GetValues(typeof(RuntimeMoniker)))
+ DotTraceDiagnoser.IsSupported(moniker); // Just check that it doesn't throw exceptions
+ }
+ }
+}
\ No newline at end of file