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(); + 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 08cb25f881..946a5f6371 100755 --- a/tests/BenchmarkDotNet.Tests/BenchmarkDotNet.Tests.csproj +++ b/tests/BenchmarkDotNet.Tests/BenchmarkDotNet.Tests.csproj @@ -33,6 +33,7 @@ + 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