diff --git a/.github/workflows/publish-nightly.yaml b/.github/workflows/publish-nightly.yaml
index 3a3dc0bc9b..f3e025a6de 100644
--- a/.github/workflows/publish-nightly.yaml
+++ b/.github/workflows/publish-nightly.yaml
@@ -11,13 +11,13 @@ jobs:
runs-on: ubuntu-latest
if: ${{ github.repository == 'dotnet/BenchmarkDotNet' }}
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v4
- name: Set date
run: echo "DATE=$(date +'%Y%m%d')" >> $GITHUB_ENV
- name: Pack
run: ./build.cmd pack /p:VersionSuffix=nightly.$DATE.$GITHUB_RUN_NUMBER
- name: Upload nupkg to artifacts
- uses: actions/upload-artifact@v3
+ uses: actions/upload-artifact@v4
with:
name: nupkgs
path: "**/*.*nupkg"
diff --git a/.github/workflows/report-test-results.yaml b/.github/workflows/report-test-results.yaml
index 005465b329..77aafa2fe7 100644
--- a/.github/workflows/report-test-results.yaml
+++ b/.github/workflows/report-test-results.yaml
@@ -12,12 +12,24 @@ jobs:
runs-on: ubuntu-latest
permissions: write-all
steps:
+ # Cleanup Old Files
+ - name: Cleanup Old Files
+ run: rm -rf $GITHUB_WORKSPACE/*.trx
+
+ # Download the Latest Artifacts with Unique Name
- name: Download Artifacts
- uses: dawidd6/action-download-artifact@v2
+ uses: dawidd6/action-download-artifact@v6
with:
- workflow: ${{ github.event.workflow_run.workflow_id }}
+ run_id: ${{ github.event.workflow_run.id }}
+
+ # Display the Structure of Downloaded Files
- name: Display structure of downloaded files
run: ls -R
+
+ # Display the Contents of .trx Files
+ - name: Display .trx file contents
+ run: cat **/*.trx || echo "No .trx files found"
+
- name: Report tests results
uses: AndreyAkinshin/test-reporter@0e2c48ebec2007001dd77dd4bcbcd450b96d5a38
with:
diff --git a/.github/workflows/run-tests.yaml b/.github/workflows/run-tests.yaml
index 90887cbecd..2856388209 100644
--- a/.github/workflows/run-tests.yaml
+++ b/.github/workflows/run-tests.yaml
@@ -17,22 +17,20 @@ jobs:
- name: Disable Windows Defender
run: Set-MpPreference -DisableRealtimeMonitoring $true
shell: powershell
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v4
+ # Build and Test
- name: Run task 'build'
shell: cmd
- run: |
- call "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Auxiliary\Build\vcvars64.bat"
- ./build.cmd build
+ run: ./build.cmd build
- name: Run task 'in-tests-core'
shell: cmd
- run: |
- call "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Auxiliary\Build\vcvars64.bat"
- ./build.cmd in-tests-core -e
+ run: ./build.cmd in-tests-core -e
+ # Upload Artifacts with Unique Name
- name: Upload test results
- uses: actions/upload-artifact@v3
+ uses: actions/upload-artifact@v4
if: always()
with:
- name: test-windows-core-trx
+ name: test-windows-core-trx-${{ github.run_id }}
path: "**/*.trx"
test-windows-full:
@@ -41,28 +39,27 @@ jobs:
- name: Disable Windows Defender
run: Set-MpPreference -DisableRealtimeMonitoring $true
shell: powershell
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v4
+ # Build and Test
- name: Run task 'build'
shell: cmd
- run: |
- call "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Auxiliary\Build\vcvars64.bat"
- ./build.cmd build
+ run: ./build.cmd build
- name: Run task 'in-tests-full'
shell: cmd
- run: |
- call "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Auxiliary\Build\vcvars64.bat"
- ./build.cmd in-tests-full -e
+ run: ./build.cmd in-tests-full -e
+ # Upload Artifacts with Unique Name
- name: Upload test results
- uses: actions/upload-artifact@v3
+ uses: actions/upload-artifact@v4
if: always()
with:
- name: test-windows-full-trx
+ name: test-windows-full-trx-${{ github.run_id }}
path: "**/*.trx"
test-linux:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v4
+ # Set up the environment
- name: Set up Clang
uses: egor-tensin/setup-clang@v1
with:
@@ -70,46 +67,80 @@ jobs:
platform: x64
- name: Set up zlib-static
run: sudo apt-get install -y libkrb5-dev
+ - name: Set up node
+ uses: actions/setup-node@v4
+ with:
+ node-version: "20"
+ - name: Set up v8
+ run: npm install jsvu -g && jsvu --os=linux64 --engines=v8 && echo "$HOME/.jsvu/bin" >> $GITHUB_PATH
+ - name: Install wasm-tools workload
+ run: ./build.cmd install-wasm-tools
+ # Build and Test
- name: Run task 'build'
run: ./build.cmd build
- name: Run task 'unit-tests'
run: ./build.cmd unit-tests -e
- name: Run task 'in-tests-core'
run: ./build.cmd in-tests-core -e
+ # Upload Artifacts with Unique Name
- name: Upload test results
- uses: actions/upload-artifact@v3
+ uses: actions/upload-artifact@v4
if: always()
with:
- name: test-linux-trx
+ name: test-linux-trx-${{ github.run_id }}
path: "**/*.trx"
test-macos:
runs-on: macos-13
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v4
+ - name: Set up node
+ uses: actions/setup-node@v4
+ with:
+ node-version: "20"
+ - name: Set up v8
+ run: npm install jsvu -g && jsvu --os=mac64 --engines=v8 && echo "$HOME/.jsvu/bin" >> $GITHUB_PATH
+ - name: Install wasm-tools workload
+ run: ./build.cmd install-wasm-tools
+ # Build and Test
- name: Run task 'build'
run: ./build.cmd build
- name: Run task 'unit-tests'
run: ./build.cmd unit-tests -e
- name: Run task 'in-tests-core'
run: ./build.cmd in-tests-core -e
+ # Upload Artifacts with Unique Name
- name: Upload test results
- uses: actions/upload-artifact@v3
+ uses: actions/upload-artifact@v4
if: always()
with:
- name: test-macos-trx
+ name: test-macos-trx-${{ github.run_id }}
path: "**/*.trx"
-
+
+ test-pack:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - name: Set up Clang
+ uses: egor-tensin/setup-clang@v1
+ with:
+ version: latest
+ platform: x64
+ - name: Set up zlib-static
+ run: sudo apt-get install -y libkrb5-dev
+ - name: Run task 'pack'
+ run: ./build.cmd pack
+
spellcheck-docs:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v3
- - uses: actions/setup-node@v3
+ - uses: actions/checkout@v4
+ - uses: actions/setup-node@v4
name: Setup node
with:
- node-version: "16"
+ node-version: "18"
- name: Install cSpell
- run: npm install -g cspell
+ run: npm install -g cspell@8.0.0
- name: Copy cSpell config
run: cp ./build/cSpell.json ./cSpell.json
- name: Run cSpell
diff --git a/BenchmarkDotNet.sln b/BenchmarkDotNet.sln
index ba31552c8a..1df6c0aabd 100644
--- a/BenchmarkDotNet.sln
+++ b/BenchmarkDotNet.sln
@@ -1,7 +1,7 @@
Microsoft Visual Studio Solution File, Format Version 12.00
-# Visual Studio 15
-VisualStudioVersion = 15.0.27130.2027
+# Visual Studio Version 17
+VisualStudioVersion = 17.8.34004.107
MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{D6597E3A-6892-4A68-8E14-042FC941FDA2}"
EndProject
@@ -51,6 +51,14 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BenchmarkDotNet.Diagnostics
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BenchmarkDotNet.IntegrationTests.ManualRunning.MultipleFrameworks", "tests\BenchmarkDotNet.IntegrationTests.ManualRunning.MultipleFrameworks\BenchmarkDotNet.IntegrationTests.ManualRunning.MultipleFrameworks.csproj", "{AACA2C63-A85B-47AB-99FC-72C3FF408B14}"
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
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BenchmarkDotNet.Exporters.Plotting", "src\BenchmarkDotNet.Exporters.Plotting\BenchmarkDotNet.Exporters.Plotting.csproj", "{B92ECCEF-7C27-4012-9E19-679F3C40A6A6}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BenchmarkDotNet.Exporters.Plotting.Tests", "tests\BenchmarkDotNet.Exporters.Plotting.Tests\BenchmarkDotNet.Exporters.Plotting.Tests.csproj", "{199AC83E-30BD-40CD-87CE-0C838AC0320D}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -137,6 +145,22 @@ Global
{AACA2C63-A85B-47AB-99FC-72C3FF408B14}.Debug|Any CPU.Build.0 = Debug|Any CPU
{AACA2C63-A85B-47AB-99FC-72C3FF408B14}.Release|Any CPU.ActiveCfg = Release|Any CPU
{AACA2C63-A85B-47AB-99FC-72C3FF408B14}.Release|Any CPU.Build.0 = Release|Any CPU
+ {4C9C89B8-7C4E-4ECF-B3C9-324C8772EDAC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {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
+ {B92ECCEF-7C27-4012-9E19-679F3C40A6A6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {B92ECCEF-7C27-4012-9E19-679F3C40A6A6}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {B92ECCEF-7C27-4012-9E19-679F3C40A6A6}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {B92ECCEF-7C27-4012-9E19-679F3C40A6A6}.Release|Any CPU.Build.0 = Release|Any CPU
+ {199AC83E-30BD-40CD-87CE-0C838AC0320D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {199AC83E-30BD-40CD-87CE-0C838AC0320D}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {199AC83E-30BD-40CD-87CE-0C838AC0320D}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {199AC83E-30BD-40CD-87CE-0C838AC0320D}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -162,6 +186,10 @@ Global
{B620D10A-CD8E-4A34-8B27-FD6257E63AD0} = {63B94FD6-3F3D-4E04-9727-48E86AC4384C}
{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}
+ {B92ECCEF-7C27-4012-9E19-679F3C40A6A6} = {D6597E3A-6892-4A68-8E14-042FC941FDA2}
+ {199AC83E-30BD-40CD-87CE-0C838AC0320D} = {14195214-591A-45B7-851A-19D3BA2413F9}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {4D9AF12B-1F7F-45A7-9E8C-E4E46ADCBD1F}
diff --git a/BenchmarkDotNet.sln.DotSettings b/BenchmarkDotNet.sln.DotSettings
index 9542bd9d5b..fc698670f8 100644
--- a/BenchmarkDotNet.sln.DotSettings
+++ b/BenchmarkDotNet.sln.DotSettings
@@ -52,6 +52,8 @@
OSX
RT
<Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
+ <Policy><Descriptor Staticness="Instance" AccessRightKinds="Private" Description="Instance fields (private)"><ElementKinds><Kind Name="FIELD" /><Kind Name="READONLY_FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /></Policy>
+ <Policy><Descriptor Staticness="Static" AccessRightKinds="Private" Description="Static fields (private)"><ElementKinds><Kind Name="FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /></Policy>
True
True
983040
@@ -68,6 +70,7 @@
True
<Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
<Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
+ True
<data><IncludeFilters /><ExcludeFilters><Filter ModuleMask="*" ModuleVersionMask="*" ClassMask="JetBrains.Annotations.*" FunctionMask="*" IsEnabled="True" /><Filter ModuleMask="*" ModuleVersionMask="*" ClassMask="SimpleJson.*" FunctionMask="*" IsEnabled="True" /></ExcludeFilters></data>
True
True
@@ -162,6 +165,7 @@
True
True
True
+ True
True
True
True
@@ -173,6 +177,7 @@
True
True
True
+ True
True
True
True
diff --git a/LICENSE.md b/LICENSE.md
index d20c387e81..6272f97306 100644
--- a/LICENSE.md
+++ b/LICENSE.md
@@ -1,6 +1,6 @@
### The MIT License
-Copyright (c) 2013–2023 .NET Foundation and contributors
+Copyright (c) 2013–2025 .NET Foundation and contributors
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
diff --git a/NuGet.Config b/NuGet.Config
index 2b16f5257e..7507704b8b 100644
--- a/NuGet.Config
+++ b/NuGet.Config
@@ -12,5 +12,7 @@
+
+
diff --git a/README.md b/README.md
index 424439148a..315215f058 100644
--- a/README.md
+++ b/README.md
@@ -7,10 +7,10 @@
[](https://www.nuget.org/packages/BenchmarkDotNet/)
- [](https://www.myget.org/feed/Packages/benchmarkdotnet)
+ [](https://www.myget.org/feed/benchmarkdotnet/package/nuget/BenchmarkDotNet)
[](https://www.nuget.org/packages/BenchmarkDotNet/)
[](https://github.com/dotnet/BenchmarkDotNet/stargazers)
- 
+ [](https://github.com/dotnet/BenchmarkDotNet/blob/master/LICENSE.md)
[](https://twitter.com/BenchmarkDotNet)
@@ -30,7 +30,7 @@ It's no harder than writing unit tests!
Under the hood, it performs a lot of [magic](#automation) that guarantees [reliable and precise](#reliability) results thanks to the [perfolizer](https://github.com/AndreyAkinshin/perfolizer) statistical engine.
BenchmarkDotNet protects you from popular benchmarking mistakes and warns you if something is wrong with your benchmark design or obtained measurements.
The results are presented in a [user-friendly](#friendliness) form that highlights all the important facts about your experiment.
-BenchmarkDotNet is already adopted by [17400+ GitHub projects](https://github.com/dotnet/BenchmarkDotNet/network/dependents) including
+BenchmarkDotNet is already adopted by [25800+ GitHub projects](https://github.com/dotnet/BenchmarkDotNet/network/dependents) including
[.NET Runtime](https://github.com/dotnet/runtime),
[.NET Compiler](https://github.com/dotnet/roslyn),
[.NET Performance](https://github.com/dotnet/performance),
@@ -125,7 +125,7 @@ Four aspects define the design of these features:
### Simplicity
-You shouldn't be an experienced performance engineer if you want to write benchmarks.
+You shouldn't have to be an experienced performance engineer if you want to write benchmarks.
You can design very complicated performance experiments in the declarative style using simple APIs.
For example, if you want to [parameterize](https://benchmarkdotnet.org/articles/features/parameterization.html) your benchmark,
@@ -135,8 +135,8 @@ If you want to compare benchmarks with each other,
mark one of the benchmarks as the [baseline](https://benchmarkdotnet.org/articles/features/baselines.html)
via `[Benchmark(Baseline = true)]`: BenchmarkDotNet will compare it with all of the other benchmarks.
If you want to compare performance in different environments, use [jobs](https://benchmarkdotnet.org/articles/configs/jobs.html).
-For example, you can run all the benchmarks on .NET Core 3.0 and Mono via
- `[SimpleJob(RuntimeMoniker.NetCoreApp30)]` and `[SimpleJob(RuntimeMoniker.Mono)]`.
+For example, you can run all the benchmarks on .NET 8.0 and Mono via
+ `[SimpleJob(RuntimeMoniker.Net80)]` and `[SimpleJob(RuntimeMoniker.Mono)]`.
If you don't like attributes, you can call most of the APIs via the fluent style and write code like this:
diff --git a/build/BenchmarkDotNet.Build/BenchmarkDotNet.Build.csproj b/build/BenchmarkDotNet.Build/BenchmarkDotNet.Build.csproj
index 3b5596f3b5..c7b97bc90a 100644
--- a/build/BenchmarkDotNet.Build/BenchmarkDotNet.Build.csproj
+++ b/build/BenchmarkDotNet.Build/BenchmarkDotNet.Build.csproj
@@ -1,15 +1,15 @@
Exe
- net7.0
+ net8.0
$(MSBuildProjectDirectory)
enable
-
-
-
-
-
+
+
+
+
+
-
\ No newline at end of file
+
diff --git a/build/BenchmarkDotNet.Build/BuildContext.cs b/build/BenchmarkDotNet.Build/BuildContext.cs
index a13c77c5ed..0a165721e0 100644
--- a/build/BenchmarkDotNet.Build/BuildContext.cs
+++ b/build/BenchmarkDotNet.Build/BuildContext.cs
@@ -59,6 +59,9 @@ public BuildContext(ICakeContext context)
BuildDirectory = RootDirectory.Combine("build");
ArtifactsDirectory = RootDirectory.Combine("artifacts");
+ var toolFileName = context.IsRunningOnWindows() ? "dotnet.exe" : "dotnet";
+ var toolFilePath = RootDirectory.Combine(".dotnet").CombineWithFilePath(toolFileName);
+ context.Tools.RegisterFile(toolFilePath);
SolutionFile = RootDirectory.CombineWithFilePath("BenchmarkDotNet.sln");
diff --git a/build/BenchmarkDotNet.Build/Folder.DotSettings b/build/BenchmarkDotNet.Build/Folder.DotSettings
index 53109cf04e..539b6fe39e 100644
--- a/build/BenchmarkDotNet.Build/Folder.DotSettings
+++ b/build/BenchmarkDotNet.Build/Folder.DotSettings
@@ -1,4 +1,7 @@
<Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
<Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
+ <Policy><Descriptor Staticness="Instance" AccessRightKinds="Private" Description="Instance fields (private)"><ElementKinds><Kind Name="FIELD" /><Kind Name="READONLY_FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /></Policy>
+ <Policy><Descriptor Staticness="Static" AccessRightKinds="Private" Description="Static fields (private)"><ElementKinds><Kind Name="FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /></Policy>
+ True
True
\ No newline at end of file
diff --git a/build/BenchmarkDotNet.Build/Program.cs b/build/BenchmarkDotNet.Build/Program.cs
index ce291ecd0f..38a1d4c28e 100644
--- a/build/BenchmarkDotNet.Build/Program.cs
+++ b/build/BenchmarkDotNet.Build/Program.cs
@@ -56,6 +56,26 @@ public HelpInfo GetHelp()
}
}
+[TaskName(Name)]
+[TaskDescription("Install wasm-tools workload")]
+public class InstallWasmToolsWorkload : FrostingTask, IHelpProvider
+{
+ private const string Name = "install-wasm-tools";
+
+ public override void Run(BuildContext context) => context.BuildRunner.InstallWorkload("wasm-tools");
+
+ public HelpInfo GetHelp()
+ {
+ return new HelpInfo
+ {
+ Examples = new[]
+ {
+ new Example(Name)
+ }
+ };
+ }
+}
+
[TaskName(Name)]
[TaskDescription("Run unit tests (fast)")]
[IsDependentOn(typeof(BuildTask))]
@@ -93,12 +113,12 @@ public class InTestsFullTask : FrostingTask, IHelpProvider
}
[TaskName(Name)]
-[TaskDescription("Run integration tests using .NET 7 (slow)")]
+[TaskDescription("Run integration tests using .NET 8 (slow)")]
[IsDependentOn(typeof(BuildTask))]
public class InTestsCoreTask : FrostingTask, IHelpProvider
{
private const string Name = "in-tests-core";
- public override void Run(BuildContext context) => context.UnitTestRunner.RunInTests("net7.0");
+ public override void Run(BuildContext context) => context.UnitTestRunner.RunInTests("net8.0");
public HelpInfo GetHelp() => new();
}
@@ -223,6 +243,9 @@ public class ReleaseTask : FrostingTask, IHelpProvider
new Example(Name)
.WithArgument(KnownOptions.Stable)
.WithArgument(KnownOptions.NextVersion, "0.1.1729")
+ .WithArgument(KnownOptions.Push),
+ new Example(Name)
+ .WithArgument(KnownOptions.Stable)
.WithArgument(KnownOptions.Push)
}
};
diff --git a/build/BenchmarkDotNet.Build/Runners/BuildRunner.cs b/build/BenchmarkDotNet.Build/Runners/BuildRunner.cs
index 01c490fce3..a38ce7e79d 100644
--- a/build/BenchmarkDotNet.Build/Runners/BuildRunner.cs
+++ b/build/BenchmarkDotNet.Build/Runners/BuildRunner.cs
@@ -5,6 +5,7 @@
using Cake.Common.Tools.DotNet.Build;
using Cake.Common.Tools.DotNet.Pack;
using Cake.Common.Tools.DotNet.Restore;
+using Cake.Common.Tools.DotNet.Workload.Install;
using Cake.Core;
using Cake.Core.IO;
@@ -28,6 +29,16 @@ public void Restore()
});
}
+ public void InstallWorkload(string workloadId)
+ {
+ context.DotNetWorkloadInstall(workloadId,
+ new DotNetWorkloadInstallSettings
+ {
+ IncludePreviews = true,
+ NoCache = true
+ });
+ }
+
public void Build()
{
context.Information("BuildSystemProvider: " + context.BuildSystem().Provider);
diff --git a/build/BenchmarkDotNet.Build/Runners/DocumentationRunner.cs b/build/BenchmarkDotNet.Build/Runners/DocumentationRunner.cs
index 6883abe79a..2d2128a86f 100644
--- a/build/BenchmarkDotNet.Build/Runners/DocumentationRunner.cs
+++ b/build/BenchmarkDotNet.Build/Runners/DocumentationRunner.cs
@@ -133,8 +133,8 @@ private void RunDocfx()
var currentDirectory = Directory.GetCurrentDirectory();
Directory.SetCurrentDirectory(docfxJsonFile.GetDirectory().FullPath);
- Microsoft.DocAsCode.Dotnet.DotnetApiCatalog.GenerateManagedReferenceYamlFiles(docfxJsonFile.FullPath).Wait();
- Microsoft.DocAsCode.Docset.Build(docfxJsonFile.FullPath).Wait();
+ Docfx.Dotnet.DotnetApiCatalog.GenerateManagedReferenceYamlFiles(docfxJsonFile.FullPath).Wait();
+ Docfx.Docset.Build(docfxJsonFile.FullPath).Wait();
Directory.SetCurrentDirectory(currentDirectory);
}
diff --git a/build/BenchmarkDotNet.Build/Runners/ReleaseRunner.cs b/build/BenchmarkDotNet.Build/Runners/ReleaseRunner.cs
index 0562639a9b..e9d1fcb9de 100644
--- a/build/BenchmarkDotNet.Build/Runners/ReleaseRunner.cs
+++ b/build/BenchmarkDotNet.Build/Runners/ReleaseRunner.cs
@@ -1,7 +1,6 @@
using System;
using System.Linq;
using System.Text;
-using System.Text.RegularExpressions;
using System.Threading.Tasks;
using BenchmarkDotNet.Build.Helpers;
using BenchmarkDotNet.Build.Meta;
@@ -34,9 +33,15 @@ public void Run()
else
EnvVar.NuGetToken.SetEmpty();
- var nextVersion = KnownOptions.NextVersion.AssertHasValue(context);
var currentVersion = context.VersionHistory.CurrentVersion;
var tag = "v" + currentVersion;
+ var nextVersion = KnownOptions.NextVersion.Resolve(context);
+ if (nextVersion == "")
+ {
+ var version = Version.Parse(currentVersion);
+ nextVersion = $"{version.Major}.{version.Minor}.{version.Build + 1}";
+ context.Information($"Evaluated NextVersion: {nextVersion}");
+ }
context.GitRunner.Tag(tag);
@@ -138,6 +143,7 @@ private void PublishGitHubRelease()
Draft = false,
Prerelease = false,
GenerateReleaseNotes = false,
+ DiscussionCategoryName = "Announcements",
Body = notes
}).Wait();
context.Information(" Success");
diff --git a/build/BenchmarkDotNet.Build/Runners/UnitTestRunner.cs b/build/BenchmarkDotNet.Build/Runners/UnitTestRunner.cs
index 48d5183e14..9b10d54ea4 100644
--- a/build/BenchmarkDotNet.Build/Runners/UnitTestRunner.cs
+++ b/build/BenchmarkDotNet.Build/Runners/UnitTestRunner.cs
@@ -7,28 +7,25 @@
namespace BenchmarkDotNet.Build.Runners;
-public class UnitTestRunner
+public class UnitTestRunner(BuildContext context)
{
- private readonly BuildContext context;
+ private FilePath UnitTestsProjectFile { get; } = context.RootDirectory
+ .Combine("tests")
+ .Combine("BenchmarkDotNet.Tests")
+ .CombineWithFilePath("BenchmarkDotNet.Tests.csproj");
- private FilePath UnitTestsProjectFile { get; }
- private FilePath IntegrationTestsProjectFile { get; }
- private DirectoryPath TestOutputDirectory { get; }
+ private FilePath ExporterTestsProjectFile { get; } = context.RootDirectory
+ .Combine("tests")
+ .Combine("BenchmarkDotNet.Exporters.Plotting.Tests")
+ .CombineWithFilePath("BenchmarkDotNet.Exporters.Plotting.Tests.csproj");
- public UnitTestRunner(BuildContext context)
- {
- this.context = context;
- UnitTestsProjectFile = context.RootDirectory
- .Combine("tests")
- .Combine("BenchmarkDotNet.Tests")
- .CombineWithFilePath("BenchmarkDotNet.Tests.csproj");
- IntegrationTestsProjectFile = context.RootDirectory
- .Combine("tests")
- .Combine("BenchmarkDotNet.IntegrationTests")
- .CombineWithFilePath("BenchmarkDotNet.IntegrationTests.csproj");
- TestOutputDirectory = context.RootDirectory
- .Combine("TestResults");
- }
+ private FilePath IntegrationTestsProjectFile { get; } = context.RootDirectory
+ .Combine("tests")
+ .Combine("BenchmarkDotNet.IntegrationTests")
+ .CombineWithFilePath("BenchmarkDotNet.IntegrationTests.csproj");
+
+ private DirectoryPath TestOutputDirectory { get; } = context.RootDirectory
+ .Combine("TestResults");
private DotNetTestSettings GetTestSettingsParameters(FilePath logFile, string tfm)
{
@@ -58,14 +55,15 @@ private void RunTests(FilePath projectFile, string alias, string tfm)
context.DotNetTest(projectFile.FullPath, settings);
}
- private void RunUnitTests(string tfm) => RunTests(UnitTestsProjectFile, "unit", tfm);
+ private void RunUnitTests(string tfm)
+ {
+ RunTests(UnitTestsProjectFile, "unit", tfm);
+ RunTests(ExporterTestsProjectFile, "exporters", tfm);
+ }
public void RunUnitTests()
{
- var targetFrameworks = context.IsRunningOnWindows()
- ? new[] { "net462", "net7.0" }
- : new[] { "net7.0" };
-
+ string[] targetFrameworks = context.IsRunningOnWindows() ? ["net462", "net8.0"] : ["net8.0"];
foreach (var targetFramework in targetFrameworks)
RunUnitTests(targetFramework);
}
diff --git a/build/build.ps1 b/build/build.ps1
index 2f6d5a5d11..d4db557409 100755
--- a/build/build.ps1
+++ b/build/build.ps1
@@ -58,11 +58,10 @@ if (!(Test-Path $InstallPath)) {
$ScriptPath = Join-Path $InstallPath 'dotnet-install.ps1'
(New-Object System.Net.WebClient).DownloadFile($DotNetInstallerUri, $ScriptPath);
& $ScriptPath -JSonFile $GlobalJsonPath -InstallDir $InstallPath;
-
- Remove-PathVariable "$InstallPath"
- $env:PATH = "$InstallPath;$env:PATH"
}
+Remove-PathVariable "$InstallPath"
+$env:PATH = "$InstallPath;$env:PATH"
$env:DOTNET_ROOT=$InstallPath
###########################################################################
diff --git a/build/build.sh b/build/build.sh
index ebf8ef04bd..e07aecf5ff 100755
--- a/build/build.sh
+++ b/build/build.sh
@@ -1,7 +1,7 @@
#!/usr/bin/env bash
# Define variables
-SCRIPT_DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && cd .. && pwd )
+PROJECT_ROOT=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && cd .. && pwd )
###########################################################################
# INSTALL .NET CORE CLI
@@ -12,17 +12,17 @@ export DOTNET_CLI_TELEMETRY_OPTOUT=1
export DOTNET_SYSTEM_NET_HTTP_USESOCKETSHTTPHANDLER=0
export DOTNET_ROLL_FORWARD_ON_NO_CANDIDATE_FX=2
-if [ ! -d "$SCRIPT_DIR/.dotnet" ]; then
- mkdir "$SCRIPT_DIR/.dotnet"
- curl -Lsfo "$SCRIPT_DIR/.dotnet/dotnet-install.sh" https://dot.net/v1/dotnet-install.sh
- bash "$SCRIPT_DIR/.dotnet/dotnet-install.sh" --jsonfile ./build/sdk/global.json --install-dir .dotnet --no-path
+if [ ! -d "$PROJECT_ROOT/.dotnet" ]; then
+ mkdir "$PROJECT_ROOT/.dotnet"
+ curl -Lsfo "$PROJECT_ROOT/.dotnet/dotnet-install.sh" https://dot.net/v1/dotnet-install.sh
+ bash "$PROJECT_ROOT/.dotnet/dotnet-install.sh" --jsonfile ./build/sdk/global.json --install-dir .dotnet --no-path
fi
-export PATH="$SCRIPT_DIR/.dotnet":$PATH
-export DOTNET_ROOT="$SCRIPT_DIR/.dotnet"
+export PATH="$PROJECT_ROOT/.dotnet":$PATH
+export DOTNET_ROOT="$PROJECT_ROOT/.dotnet"
###########################################################################
# RUN BUILD SCRIPT
###########################################################################
-dotnet run --configuration Release --project ./build/BenchmarkDotNet.Build/BenchmarkDotNet.Build.csproj -- "$@"
+dotnet run --configuration Release --project "$PROJECT_ROOT/build/BenchmarkDotNet.Build/BenchmarkDotNet.Build.csproj" -- "$@"
diff --git a/build/cSpell.json b/build/cSpell.json
index a224a62a9c..42795c7a70 100644
--- a/build/cSpell.json
+++ b/build/cSpell.json
@@ -12,6 +12,7 @@
"Cygwin",
"Diagnoser",
"diagnosers",
+ "diagsession",
"disassemblers",
"disassm",
"Jits",
@@ -29,6 +30,8 @@
"Pseudocode",
"runtimes",
"Serilog",
+ "vsprofiler",
+ "vstest",
"Tailcall",
"toolchains",
"unmanaged"
diff --git a/build/common.props b/build/common.props
index f84648040b..7fab76cf87 100644
--- a/build/common.props
+++ b/build/common.props
@@ -18,10 +18,13 @@
false
true
True
+ false
$(MSBuildThisFileDirectory)CodingStyle.ruleset
true
annotations
+
+ true
@@ -35,11 +38,11 @@
- 11.0
+ 12.0
- 0.13.8
+ 0.15.0
diff --git a/build/sdk/global.json b/build/sdk/global.json
index 5e4624ef91..c55fa7dfa6 100644
--- a/build/sdk/global.json
+++ b/build/sdk/global.json
@@ -1,6 +1,6 @@
{
"sdk": {
- "version": "7.0.305",
+ "version": "8.0.401",
"rollForward": "disable"
}
}
diff --git a/build/versions.txt b/build/versions.txt
index 1f6634ffbc..a7615e3fb3 100644
--- a/build/versions.txt
+++ b/build/versions.txt
@@ -52,4 +52,10 @@
0.13.5
0.13.6
0.13.7
-0.13.8
\ No newline at end of file
+0.13.8
+0.13.9
+0.13.10
+0.13.11
+0.13.12
+0.14.0
+0.15.0
\ No newline at end of file
diff --git a/docs/_changelog/footer/v0.13.10.md b/docs/_changelog/footer/v0.13.10.md
new file mode 100644
index 0000000000..68ef4fd0db
--- /dev/null
+++ b/docs/_changelog/footer/v0.13.10.md
@@ -0,0 +1,11 @@
+_Date: November 01, 2023_
+
+_Milestone: [v0.13.10](https://github.com/dotnet/BenchmarkDotNet/issues?q=milestone%3Av0.13.10)_
+([List of commits](https://github.com/dotnet/BenchmarkDotNet/compare/v0.13.9...v0.13.10))
+
+_NuGet Packages:_
+* https://www.nuget.org/packages/BenchmarkDotNet/0.13.10
+* https://www.nuget.org/packages/BenchmarkDotNet.Annotations/0.13.10
+* https://www.nuget.org/packages/BenchmarkDotNet.Diagnostics.dotTrace/0.13.10
+* https://www.nuget.org/packages/BenchmarkDotNet.Diagnostics.Windows/0.13.10
+* https://www.nuget.org/packages/BenchmarkDotNet.Templates/0.13.10
diff --git a/docs/_changelog/footer/v0.13.11.md b/docs/_changelog/footer/v0.13.11.md
new file mode 100644
index 0000000000..03dff00d30
--- /dev/null
+++ b/docs/_changelog/footer/v0.13.11.md
@@ -0,0 +1,11 @@
+_Date: December 06, 2023_
+
+_Milestone: [v0.13.11](https://github.com/dotnet/BenchmarkDotNet/issues?q=milestone%3Av0.13.11)_
+([List of commits](https://github.com/dotnet/BenchmarkDotNet/compare/v0.13.10...v0.13.11))
+
+_NuGet Packages:_
+* https://www.nuget.org/packages/BenchmarkDotNet/0.13.11
+* https://www.nuget.org/packages/BenchmarkDotNet.Annotations/0.13.11
+* https://www.nuget.org/packages/BenchmarkDotNet.Diagnostics.dotTrace/0.13.11
+* https://www.nuget.org/packages/BenchmarkDotNet.Diagnostics.Windows/0.13.11
+* https://www.nuget.org/packages/BenchmarkDotNet.Templates/0.13.11
diff --git a/docs/_changelog/footer/v0.13.12.md b/docs/_changelog/footer/v0.13.12.md
new file mode 100644
index 0000000000..246864bb8a
--- /dev/null
+++ b/docs/_changelog/footer/v0.13.12.md
@@ -0,0 +1,12 @@
+_Date: January 05, 2024_
+
+_Milestone: [v0.13.12](https://github.com/dotnet/BenchmarkDotNet/issues?q=milestone%3Av0.13.12)_
+([List of commits](https://github.com/dotnet/BenchmarkDotNet/compare/v0.13.11...v0.13.12))
+
+_NuGet Packages:_
+* https://www.nuget.org/packages/BenchmarkDotNet/0.13.12
+* https://www.nuget.org/packages/BenchmarkDotNet.Annotations/0.13.12
+* https://www.nuget.org/packages/BenchmarkDotNet.Diagnostics.dotTrace/0.13.12
+* https://www.nuget.org/packages/BenchmarkDotNet.Diagnostics.Windows/0.13.12
+* https://www.nuget.org/packages/BenchmarkDotNet.Templates/0.13.12
+* https://www.nuget.org/packages/BenchmarkDotNet.TestAdapter/0.13.12
diff --git a/docs/_changelog/footer/v0.13.9.md b/docs/_changelog/footer/v0.13.9.md
new file mode 100644
index 0000000000..962bc252cc
--- /dev/null
+++ b/docs/_changelog/footer/v0.13.9.md
@@ -0,0 +1,11 @@
+_Date: October 05, 2023_
+
+_Milestone: [v0.13.9](https://github.com/dotnet/BenchmarkDotNet/issues?q=milestone%3Av0.13.9)_
+([List of commits](https://github.com/dotnet/BenchmarkDotNet/compare/v0.13.8...v0.13.9))
+
+_NuGet Packages:_
+* https://www.nuget.org/packages/BenchmarkDotNet/0.13.9
+* https://www.nuget.org/packages/BenchmarkDotNet.Annotations/0.13.9
+* https://www.nuget.org/packages/BenchmarkDotNet.Diagnostics.dotTrace/0.13.9
+* https://www.nuget.org/packages/BenchmarkDotNet.Diagnostics.Windows/0.13.9
+* https://www.nuget.org/packages/BenchmarkDotNet.Templates/0.13.9
diff --git a/docs/_changelog/footer/v0.14.0.md b/docs/_changelog/footer/v0.14.0.md
new file mode 100644
index 0000000000..47d421f49d
--- /dev/null
+++ b/docs/_changelog/footer/v0.14.0.md
@@ -0,0 +1,14 @@
+_Date: August 06, 2024_
+
+_Milestone: [v0.14.0](https://github.com/dotnet/BenchmarkDotNet/issues?q=milestone%3Av0.14.0)_
+([List of commits](https://github.com/dotnet/BenchmarkDotNet/compare/v0.13.12...v0.14.0))
+
+_NuGet Packages:_
+* https://www.nuget.org/packages/BenchmarkDotNet/0.14.0
+* https://www.nuget.org/packages/BenchmarkDotNet.Annotations/0.14.0
+* https://www.nuget.org/packages/BenchmarkDotNet.Diagnostics.dotMemory/0.14.0
+* https://www.nuget.org/packages/BenchmarkDotNet.Diagnostics.dotTrace/0.14.0
+* https://www.nuget.org/packages/BenchmarkDotNet.Diagnostics.Windows/0.14.0
+* https://www.nuget.org/packages/BenchmarkDotNet.Exporters.Plotting/0.14.0
+* https://www.nuget.org/packages/BenchmarkDotNet.Templates/0.14.0
+* https://www.nuget.org/packages/BenchmarkDotNet.TestAdapter/0.14.0
diff --git a/docs/_changelog/footer/v0.15.0.md b/docs/_changelog/footer/v0.15.0.md
new file mode 100644
index 0000000000..c3ddd0fb17
--- /dev/null
+++ b/docs/_changelog/footer/v0.15.0.md
@@ -0,0 +1,14 @@
+_Date: TBA_
+
+_Milestone: [v0.15.0](https://github.com/dotnet/BenchmarkDotNet/issues?q=milestone%3Av0.15.0)_
+([List of commits](https://github.com/dotnet/BenchmarkDotNet/compare/v0.14.0...v0.15.0))
+
+_NuGet Packages:_
+* https://www.nuget.org/packages/BenchmarkDotNet/0.15.0
+* https://www.nuget.org/packages/BenchmarkDotNet.Annotations/0.15.0
+* https://www.nuget.org/packages/BenchmarkDotNet.Diagnostics.dotMemory/0.15.0
+* https://www.nuget.org/packages/BenchmarkDotNet.Diagnostics.dotTrace/0.15.0
+* https://www.nuget.org/packages/BenchmarkDotNet.Diagnostics.Windows/0.15.0
+* https://www.nuget.org/packages/BenchmarkDotNet.Exporters.Plotting/0.15.0
+* https://www.nuget.org/packages/BenchmarkDotNet.Templates/0.15.0
+* https://www.nuget.org/packages/BenchmarkDotNet.TestAdapter/0.15.0
diff --git a/docs/_changelog/header/v0.12.1.md b/docs/_changelog/header/v0.12.1.md
index 8378e7da70..5c02ea5c0a 100644
--- a/docs/_changelog/header/v0.12.1.md
+++ b/docs/_changelog/header/v0.12.1.md
@@ -241,7 +241,7 @@ public class IntroEventPipeProfiler
Once the benchmark run is finished, you get a `.speedscope.json` file that can be opened in [SpeedScope](https://www.speedscope.app/):
-
+
The new profiler supports several modes:
@@ -250,7 +250,7 @@ The new profiler supports several modes:
* `GcCollect` - Tracks GC collections only at very low overhead.
* `Jit` - Logging when Just in time (JIT) compilation occurs. Logging of the internal workings of the Just In Time compiler. This is fairly verbose. It details decisions about interesting optimization (like inlining and tail call)
-Please see Wojciech Nagórski's [blog post](https://wojciechnagorski.com/2020/04/cross-platform-profiling-.net-code-with-benchmarkdotnet/) for all the details.
+Please see Wojciech Nagórski's [blog post](https://wojciechnagorski.github.io/2020/04/cross-platform-profiling-.net-code-with-benchmarkdotnet/) for all the details.
Special thanks to [@WojciechNagorski](https://github.com/WojciechNagorski) for the implementation!
diff --git a/docs/_changelog/header/v0.13.0.md b/docs/_changelog/header/v0.13.0.md
index d0fca1471d..daf8ed8ea6 100644
--- a/docs/_changelog/header/v0.13.0.md
+++ b/docs/_changelog/header/v0.13.0.md
@@ -340,7 +340,7 @@ Big thanks to [@lukasz-pyrzyk](https://github.com/lukasz-pyrzyk), [@fleckert](ht
* `LangVersion` set to a non-numeric value like `latest` was crashing the build. Fixed by [@martincostello](https://github.com/martincostello) in [#1420](https://github.com/dotnet/BenchmarkDotNet/pull/1420).
* Windows 10 November 201**9** was being recognized as 201**8**. Fixed by [@kapsiR](https://github.com/kapsiR) in [#1437](https://github.com/dotnet/BenchmarkDotNet/pull/1437).
* Assemblies loaded via streams were not supported. Fixed by [@jeremyosterhoudt](https://github.com/jeremyosterhoudt) in [#1443](https://github.com/dotnet/BenchmarkDotNet/pull/1443).
-* [NativeMemoryProfiler](https://wojciechnagorski.com/2019/08/analyzing-native-memory-allocation-with-benchmarkdotnet/) was detecting small leaks that were false positives. Fixed by [@WojciechNagorski](https://github.com/WojciechNagorski) in [#1451](https://github.com/dotnet/BenchmarkDotNet/pull/1451) and [#1600](https://github.com/dotnet/BenchmarkDotNet/pull/1600).
+* [NativeMemoryProfiler](https://wojciechnagorski.github.io/2019/08/analyzing-native-memory-allocation-with-benchmarkdotnet/) was detecting small leaks that were false positives. Fixed by [@WojciechNagorski](https://github.com/WojciechNagorski) in [#1451](https://github.com/dotnet/BenchmarkDotNet/pull/1451) and [#1600](https://github.com/dotnet/BenchmarkDotNet/pull/1600).
* [DisassemblyDiagnoser](https://adamsitnik.com/Disassembly-Diagnoser/) was crashing on Linux. Fixed by [@damageboy](https://github.com/damageboy) in [#1459](https://github.com/dotnet/BenchmarkDotNet/pull/1459).
* Target framework moniker was being printed as toolchain name for Full .NET Framework benchmarks. Fixed by [@svick](https://github.com/svick) in [#1471](https://github.com/dotnet/BenchmarkDotNet/pull/1471).
* `[ParamsSource]` returning `IEnumerable
Exe
- net462;net7.0
+ net462;net8.0
+ false
+
@@ -19,11 +21,8 @@
-
- all
- runtime; build; native; contentfiles; analyzers
-
+
diff --git a/samples/BenchmarkDotNet.Samples/BenchmarkDotNet.Samples.csproj b/samples/BenchmarkDotNet.Samples/BenchmarkDotNet.Samples.csproj
index 180148454a..7cdc359d7d 100644
--- a/samples/BenchmarkDotNet.Samples/BenchmarkDotNet.Samples.csproj
+++ b/samples/BenchmarkDotNet.Samples/BenchmarkDotNet.Samples.csproj
@@ -2,7 +2,7 @@
BenchmarkDotNet.Samples
- net7.0;net462
+ net8.0;net462
true
BenchmarkDotNet.Samples
Exe
@@ -11,6 +11,8 @@
AnyCPU
true
$(NoWarn);CA1018;CA5351;CA1825
+
+ false
@@ -19,10 +21,16 @@
+
+
+
+
+
+
diff --git a/samples/BenchmarkDotNet.Samples/IntroDotMemoryDiagnoser.cs b/samples/BenchmarkDotNet.Samples/IntroDotMemoryDiagnoser.cs
new file mode 100644
index 0000000000..894bfc6c34
--- /dev/null
+++ b/samples/BenchmarkDotNet.Samples/IntroDotMemoryDiagnoser.cs
@@ -0,0 +1,46 @@
+using BenchmarkDotNet.Attributes;
+using BenchmarkDotNet.Diagnostics.dotMemory;
+using System.Collections.Generic;
+
+namespace BenchmarkDotNet.Samples
+{
+ // Profile benchmarks via dotMemory SelfApi profiling for all jobs
+ [DotMemoryDiagnoser]
+ [SimpleJob] // external-process execution
+ [InProcess] // in-process execution
+ 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/samples/BenchmarkDotNet.Samples/IntroDotTraceDiagnoser.cs b/samples/BenchmarkDotNet.Samples/IntroDotTraceDiagnoser.cs
index 351207c78b..047e6ee059 100644
--- a/samples/BenchmarkDotNet.Samples/IntroDotTraceDiagnoser.cs
+++ b/samples/BenchmarkDotNet.Samples/IntroDotTraceDiagnoser.cs
@@ -3,16 +3,11 @@
namespace BenchmarkDotNet.Samples
{
- // Enables dotTrace profiling for all jobs
+ // Profile benchmarks via dotTrace SelfApi profiling for all jobs
+ // See: https://www.nuget.org/packages/JetBrains.Profiler.SelfApi
[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]
+ [SimpleJob] // external-process execution
+ [InProcess] // in-process execution
public class IntroDotTraceDiagnoser
{
[Benchmark]
diff --git a/samples/BenchmarkDotNet.Samples/IntroEnvVars.cs b/samples/BenchmarkDotNet.Samples/IntroEnvVars.cs
index 59bd2db82f..66f5197119 100644
--- a/samples/BenchmarkDotNet.Samples/IntroEnvVars.cs
+++ b/samples/BenchmarkDotNet.Samples/IntroEnvVars.cs
@@ -10,13 +10,14 @@ public class IntroEnvVars
{
private class ConfigWithCustomEnvVars : ManualConfig
{
- private const string JitNoInline = "COMPlus_JitNoInline";
-
public ConfigWithCustomEnvVars()
{
- AddJob(Job.Default.WithRuntime(CoreRuntime.Core21).WithId("Inlining enabled"));
- AddJob(Job.Default.WithRuntime(CoreRuntime.Core21)
- .WithEnvironmentVariables(new EnvironmentVariable(JitNoInline, "1"))
+ AddJob(Job.Default.WithRuntime(CoreRuntime.Core80).WithId("Inlining enabled"));
+ AddJob(Job.Default.WithRuntime(CoreRuntime.Core80)
+ .WithEnvironmentVariables([
+ new EnvironmentVariable("DOTNET_JitNoInline", "1"),
+ new EnvironmentVariable("COMPlus_JitNoInline", "1")
+ ])
.WithId("Inlining disabled"));
}
}
@@ -27,4 +28,4 @@ public void Foo()
// Benchmark body
}
}
-}
\ No newline at end of file
+}
diff --git a/samples/BenchmarkDotNet.Samples/IntroExceptionDiagnoser.cs b/samples/BenchmarkDotNet.Samples/IntroExceptionDiagnoser.cs
index a43844e592..7e0f847c95 100644
--- a/samples/BenchmarkDotNet.Samples/IntroExceptionDiagnoser.cs
+++ b/samples/BenchmarkDotNet.Samples/IntroExceptionDiagnoser.cs
@@ -16,7 +16,10 @@ public void ThrowExceptionRandomly()
throw new Exception();
}
}
- catch { }
+ catch
+ {
+ // ignored
+ }
}
}
}
diff --git a/samples/BenchmarkDotNet.Samples/IntroFluentConfigBuilder.cs b/samples/BenchmarkDotNet.Samples/IntroFluentConfigBuilder.cs
index 94c34c1cbd..4ac29919ee 100644
--- a/samples/BenchmarkDotNet.Samples/IntroFluentConfigBuilder.cs
+++ b/samples/BenchmarkDotNet.Samples/IntroFluentConfigBuilder.cs
@@ -38,7 +38,7 @@ public static void Run()
.Run(
DefaultConfig.Instance
.AddJob(Job.Default.WithRuntime(ClrRuntime.Net462))
- .AddJob(Job.Default.WithRuntime(CoreRuntime.Core21))
+ .AddJob(Job.Default.WithRuntime(CoreRuntime.Core80))
.AddValidator(ExecutionValidator.FailOnError));
}
}
diff --git a/samples/BenchmarkDotNet.Samples/IntroOrderManual.cs b/samples/BenchmarkDotNet.Samples/IntroOrderManual.cs
index 625e78a816..a624edd205 100644
--- a/samples/BenchmarkDotNet.Samples/IntroOrderManual.cs
+++ b/samples/BenchmarkDotNet.Samples/IntroOrderManual.cs
@@ -22,7 +22,7 @@ private class Config : ManualConfig
private class FastestToSlowestOrderer : IOrderer
{
public IEnumerable GetExecutionOrder(ImmutableArray benchmarksCase,
- IEnumerable order = null) =>
+ IEnumerable? order = null) =>
from benchmark in benchmarksCase
orderby benchmark.Parameters["X"] descending,
benchmark.Descriptor.WorkloadMethodDisplayInfo
@@ -39,7 +39,7 @@ public string GetLogicalGroupKey(ImmutableArray allBenchmarksCase
benchmarkCase.Job.DisplayInfo + "_" + benchmarkCase.Parameters.DisplayInfo;
public IEnumerable> GetLogicalGroupOrder(IEnumerable> logicalGroups,
- IEnumerable order = null) =>
+ IEnumerable? order = null) =>
logicalGroups.OrderBy(it => it.Key);
public bool SeparateLogicalGroups => true;
diff --git a/samples/BenchmarkDotNet.Samples/IntroSmokeEmptyBasic.cs b/samples/BenchmarkDotNet.Samples/IntroSmokeEmptyBasic.cs
new file mode 100644
index 0000000000..39783dc74a
--- /dev/null
+++ b/samples/BenchmarkDotNet.Samples/IntroSmokeEmptyBasic.cs
@@ -0,0 +1,82 @@
+using BenchmarkDotNet.Attributes;
+
+namespace BenchmarkDotNet.Samples;
+
+[DisassemblyDiagnoser]
+public class IntroSmokeEmptyBasic
+{
+ [Benchmark] public void Void1() {}
+ [Benchmark] public void Void2() {}
+ [Benchmark] public void Void3() {}
+ [Benchmark] public void Void4() {}
+
+ [Benchmark] public byte Byte1() => 0;
+ [Benchmark] public byte Byte2() => 0;
+ [Benchmark] public byte Byte3() => 0;
+ [Benchmark] public byte Byte4() => 0;
+
+ [Benchmark] public sbyte Sbyte1() => 0;
+ [Benchmark] public sbyte Sbyte2() => 0;
+ [Benchmark] public sbyte Sbyte3() => 0;
+ [Benchmark] public sbyte Sbyte4() => 0;
+
+ [Benchmark] public short Short1() => 0;
+ [Benchmark] public short Short2() => 0;
+ [Benchmark] public short Short3() => 0;
+ [Benchmark] public short Short4() => 0;
+
+ [Benchmark] public ushort Ushort1() => 0;
+ [Benchmark] public ushort Ushort2() => 0;
+ [Benchmark] public ushort Ushort3() => 0;
+ [Benchmark] public ushort Ushort4() => 0;
+
+ [Benchmark] public int Int1() => 0;
+ [Benchmark] public int Int2() => 0;
+ [Benchmark] public int Int3() => 0;
+ [Benchmark] public int Int4() => 0;
+
+ [Benchmark] public uint Uint1() => 0u;
+ [Benchmark] public uint Uint2() => 0u;
+ [Benchmark] public uint Uint3() => 0u;
+ [Benchmark] public uint Uint4() => 0u;
+
+ [Benchmark] public bool Bool1() => false;
+ [Benchmark] public bool Bool2() => false;
+ [Benchmark] public bool Bool3() => false;
+ [Benchmark] public bool Bool4() => false;
+
+ [Benchmark] public char Char1() => 'a';
+ [Benchmark] public char Char2() => 'a';
+ [Benchmark] public char Char3() => 'a';
+ [Benchmark] public char Char4() => 'a';
+
+ [Benchmark] public float Float1() => 0f;
+ [Benchmark] public float Float2() => 0f;
+ [Benchmark] public float Float3() => 0f;
+ [Benchmark] public float Float4() => 0f;
+
+ [Benchmark] public double Double1() => 0d;
+ [Benchmark] public double Double2() => 0d;
+ [Benchmark] public double Double3() => 0d;
+ [Benchmark] public double Double4() => 0d;
+
+ [Benchmark] public long Long1() => 0L;
+ [Benchmark] public long Long2() => 0L;
+ [Benchmark] public long Long3() => 0L;
+ [Benchmark] public long Long4() => 0L;
+
+ [Benchmark] public ulong Ulong1() => 0uL;
+ [Benchmark] public ulong Ulong2() => 0uL;
+ [Benchmark] public ulong Ulong3() => 0uL;
+ [Benchmark] public ulong Ulong4() => 0uL;
+
+ [Benchmark] public string String1() => "";
+ [Benchmark] public string String2() => "";
+ [Benchmark] public string String3() => "";
+ [Benchmark] public string String4() => "";
+
+ [Benchmark] public object? Object1() => null;
+ [Benchmark] public object? Object2() => null;
+ [Benchmark] public object? Object3() => null;
+ [Benchmark] public object? Object4() => null;
+}
\ No newline at end of file
diff --git a/samples/BenchmarkDotNet.Samples/IntroSmokeIncrements.cs b/samples/BenchmarkDotNet.Samples/IntroSmokeIncrements.cs
new file mode 100644
index 0000000000..6dfd15433d
--- /dev/null
+++ b/samples/BenchmarkDotNet.Samples/IntroSmokeIncrements.cs
@@ -0,0 +1,138 @@
+using BenchmarkDotNet.Attributes;
+
+namespace BenchmarkDotNet.Samples;
+
+public class IntroSmokeIncrements
+{
+ public int Field;
+
+ [Benchmark]
+ public void Increment01()
+ {
+ Field++;
+ }
+
+ [Benchmark]
+ public void Increment02()
+ {
+ Field++;
+ Field++;
+ }
+
+ [Benchmark]
+ public void Increment03()
+ {
+ Field++;
+ Field++;
+ Field++;
+ }
+
+ [Benchmark]
+ public void Increment04()
+ {
+ Field++;
+ Field++;
+ Field++;
+ Field++;
+ }
+
+ [Benchmark]
+ public void Increment05()
+ {
+ Field++;
+ Field++;
+ Field++;
+ Field++;
+ Field++;
+ }
+
+ [Benchmark]
+ public void Increment06()
+ {
+ Field++;
+ Field++;
+ Field++;
+ Field++;
+ Field++;
+ Field++;
+ }
+
+ [Benchmark]
+ public void Increment07()
+ {
+ Field++;
+ Field++;
+ Field++;
+ Field++;
+ Field++;
+ Field++;
+ Field++;
+ }
+
+ [Benchmark]
+ public void Increment08()
+ {
+ Field++;
+ Field++;
+ Field++;
+ Field++;
+ Field++;
+ Field++;
+ Field++;
+ Field++;
+ }
+
+ [Benchmark]
+ public void Increment09()
+ {
+ Field++;
+ Field++;
+ Field++;
+ Field++;
+ Field++;
+ Field++;
+ Field++;
+ Field++;
+ Field++;
+ }
+
+ [Benchmark]
+ public void Increment10()
+ {
+ Field++;
+ Field++;
+ Field++;
+ Field++;
+ Field++;
+ Field++;
+ Field++;
+ Field++;
+ Field++;
+ Field++;
+ }
+
+ [Benchmark]
+ public void Increment20()
+ {
+ Field++;
+ Field++;
+ Field++;
+ Field++;
+ Field++;
+ Field++;
+ Field++;
+ Field++;
+ Field++;
+ Field++;
+ Field++;
+ Field++;
+ Field++;
+ Field++;
+ Field++;
+ Field++;
+ Field++;
+ Field++;
+ Field++;
+ Field++;
+ }
+}
\ No newline at end of file
diff --git a/samples/BenchmarkDotNet.Samples/IntroSmokeValueTypes.cs b/samples/BenchmarkDotNet.Samples/IntroSmokeValueTypes.cs
new file mode 100644
index 0000000000..66bce4d921
--- /dev/null
+++ b/samples/BenchmarkDotNet.Samples/IntroSmokeValueTypes.cs
@@ -0,0 +1,70 @@
+using System;
+using System.Threading.Tasks;
+using BenchmarkDotNet.Attributes;
+using BenchmarkDotNet.Environments;
+
+namespace BenchmarkDotNet.Samples;
+
+[MemoryDiagnoser, DisassemblyDiagnoser]
+public class IntroSmokeValueTypes
+{
+ [Benchmark] public Jit ReturnEnum() => Jit.RyuJit;
+
+ [Benchmark] public DateTime ReturnDateTime() => new DateTime();
+
+ [Benchmark] public DateTime? ReturnNullableDateTime() => new DateTime();
+ [Benchmark] public int? ReturnNullableInt() => 0;
+
+ public struct StructWithReferencesOnly { public object _ref; }
+ [Benchmark] public StructWithReferencesOnly ReturnStructWithReferencesOnly() => new StructWithReferencesOnly();
+
+ public struct EmptyStruct { }
+ [Benchmark] public EmptyStruct ReturnEmptyStruct() => new EmptyStruct();
+
+ [Benchmark] public ValueTuple ReturnGenericStructOfValueType() => new ValueTuple(0);
+ [Benchmark] public ValueTuple
-
+
diff --git a/src/BenchmarkDotNet.Diagnostics.Windows/Configs/InliningDiagnoserAttribute.cs b/src/BenchmarkDotNet.Diagnostics.Windows/Configs/InliningDiagnoserAttribute.cs
index 0a3f4e0dc5..c9878ed6fc 100644
--- a/src/BenchmarkDotNet.Diagnostics.Windows/Configs/InliningDiagnoserAttribute.cs
+++ b/src/BenchmarkDotNet.Diagnostics.Windows/Configs/InliningDiagnoserAttribute.cs
@@ -13,7 +13,7 @@ public InliningDiagnoserAttribute(bool logFailuresOnly = true, bool filterByName
Config = ManualConfig.CreateEmpty().AddDiagnoser(new InliningDiagnoser(logFailuresOnly, filterByNamespace));
}
- public InliningDiagnoserAttribute(bool logFailuresOnly = true, string[] allowedNamespaces = null)
+ public InliningDiagnoserAttribute(bool logFailuresOnly = true, string[]? allowedNamespaces = null)
{
Config = ManualConfig.CreateEmpty().AddDiagnoser(new InliningDiagnoser(logFailuresOnly, allowedNamespaces));
}
diff --git a/src/BenchmarkDotNet.Diagnostics.Windows/EtwDiagnoser.cs b/src/BenchmarkDotNet.Diagnostics.Windows/EtwDiagnoser.cs
index c52006d7c6..4205195dc5 100644
--- a/src/BenchmarkDotNet.Diagnostics.Windows/EtwDiagnoser.cs
+++ b/src/BenchmarkDotNet.Diagnostics.Windows/EtwDiagnoser.cs
@@ -8,6 +8,7 @@
using BenchmarkDotNet.Analysers;
using BenchmarkDotNet.Diagnosers;
using BenchmarkDotNet.Exporters;
+using BenchmarkDotNet.Helpers;
using BenchmarkDotNet.Loggers;
using Microsoft.Diagnostics.Tracing;
using Microsoft.Diagnostics.Tracing.Parsers;
@@ -15,7 +16,7 @@
namespace BenchmarkDotNet.Diagnostics.Windows
{
- public abstract class EtwDiagnoser where TStats : new()
+ public abstract class EtwDiagnoser : DisposeAtProcessTermination where TStats : new()
{
internal readonly LogCapture Logger = new LogCapture();
protected readonly Dictionary BenchmarkToProcess = new Dictionary();
@@ -39,11 +40,6 @@ protected void Start(DiagnoserActionParameters parameters)
BenchmarkToProcess.Add(parameters.BenchmarkCase, parameters.Process.Id);
StatsPerProcess.TryAdd(parameters.Process.Id, GetInitializedStats(parameters));
- // Important: Must wire-up clean-up events prior to acquiring IDisposable instance (Session property)
- // This is in effect the inverted sequence of actions in the Stop() method.
- Console.CancelKeyPress += OnConsoleCancelKeyPress;
- AppDomain.CurrentDomain.ProcessExit += OnProcessExit;
-
Session = CreateSession(parameters.BenchmarkCase);
EnableProvider();
@@ -80,11 +76,13 @@ protected virtual void EnableProvider()
protected void Stop()
{
WaitForDelayedEvents();
+ Dispose();
+ }
- Session.Dispose();
-
- Console.CancelKeyPress -= OnConsoleCancelKeyPress;
- AppDomain.CurrentDomain.ProcessExit -= OnProcessExit;
+ public override void Dispose()
+ {
+ Session?.Dispose();
+ base.Dispose();
}
private void Clear()
@@ -93,11 +91,7 @@ private void Clear()
StatsPerProcess.Clear();
}
- private void OnConsoleCancelKeyPress(object sender, ConsoleCancelEventArgs e) => Session?.Dispose();
-
- private void OnProcessExit(object sender, EventArgs e) => Session?.Dispose();
-
- private static string GetSessionName(string prefix, BenchmarkCase benchmarkCase, ParameterInstances parameters = null)
+ private static string GetSessionName(string prefix, BenchmarkCase benchmarkCase, ParameterInstances? parameters = null)
{
if (parameters != null && parameters.Items.Count > 0)
return $"{prefix}-{benchmarkCase.FolderInfo}-{parameters.FolderInfo}";
diff --git a/src/BenchmarkDotNet.Diagnostics.Windows/EtwProfiler.cs b/src/BenchmarkDotNet.Diagnostics.Windows/EtwProfiler.cs
index 6641dd5a45..57f57d9e1a 100644
--- a/src/BenchmarkDotNet.Diagnostics.Windows/EtwProfiler.cs
+++ b/src/BenchmarkDotNet.Diagnostics.Windows/EtwProfiler.cs
@@ -120,21 +120,10 @@ private void Start(DiagnoserActionParameters parameters)
private void Stop(DiagnoserActionParameters parameters)
{
WaitForDelayedEvents();
- string userSessionFile;
- try
- {
- kernelSession.Stop();
- heapSession?.Stop();
- userSession.Stop();
-
- userSessionFile = userSession.FilePath;
- }
- finally
- {
- kernelSession.Dispose();
- heapSession?.Dispose();
- userSession.Dispose();
- }
+ string userSessionFile = userSession.FilePath;
+ kernelSession.Dispose();
+ heapSession?.Dispose();
+ userSession.Dispose();
// Merge the 'primary' etl file X.etl (userSession) with any files that match .clr*.etl .user*.etl. and .kernel.etl.
TraceEventSession.MergeInPlace(userSessionFile, TextWriter.Null);
diff --git a/src/BenchmarkDotNet.Diagnostics.Windows/EtwProfilerConfig.cs b/src/BenchmarkDotNet.Diagnostics.Windows/EtwProfilerConfig.cs
index 967dcc2d03..7846edd1dd 100644
--- a/src/BenchmarkDotNet.Diagnostics.Windows/EtwProfilerConfig.cs
+++ b/src/BenchmarkDotNet.Diagnostics.Windows/EtwProfilerConfig.cs
@@ -41,8 +41,8 @@ public EtwProfilerConfig(
float cpuSampleIntervalInMilliseconds = 1.0f,
KernelTraceEventParser.Keywords kernelKeywords = KernelTraceEventParser.Keywords.ImageLoad | KernelTraceEventParser.Keywords.Profile,
KernelTraceEventParser.Keywords kernelStackKeywords = KernelTraceEventParser.Keywords.Profile,
- IReadOnlyDictionary> intervalSelectors = null,
- IReadOnlyCollection<(Guid providerGuid, TraceEventLevel providerLevel, ulong keywords, TraceEventProviderOptions options)> providers = null,
+ IReadOnlyDictionary>? intervalSelectors = null,
+ IReadOnlyCollection<(Guid providerGuid, TraceEventLevel providerLevel, ulong keywords, TraceEventProviderOptions options)>? providers = null,
bool createHeapSession = false)
{
CreateHeapSession = createHeapSession;
diff --git a/src/BenchmarkDotNet.Diagnostics.Windows/HardwareCounters.cs b/src/BenchmarkDotNet.Diagnostics.Windows/HardwareCounters.cs
index 8b3c46b4d5..d159e72a65 100644
--- a/src/BenchmarkDotNet.Diagnostics.Windows/HardwareCounters.cs
+++ b/src/BenchmarkDotNet.Diagnostics.Windows/HardwareCounters.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using BenchmarkDotNet.Detectors;
using BenchmarkDotNet.Diagnosers;
using BenchmarkDotNet.Portability;
using BenchmarkDotNet.Toolchains.InProcess.Emit;
@@ -31,7 +32,7 @@ private static readonly Dictionary EtwTranslations
public static IEnumerable Validate(ValidationParameters validationParameters, bool mandatory)
{
- if (!RuntimeInformation.IsWindows())
+ if (!OsDetector.IsWindows())
{
yield return new ValidationError(true, "Hardware Counters and EtwProfiler are supported only on Windows");
yield break;
diff --git a/src/BenchmarkDotNet.Diagnostics.Windows/InliningDiagnoser.cs b/src/BenchmarkDotNet.Diagnostics.Windows/InliningDiagnoser.cs
index 4c09957eef..a21dd1dc81 100644
--- a/src/BenchmarkDotNet.Diagnostics.Windows/InliningDiagnoser.cs
+++ b/src/BenchmarkDotNet.Diagnostics.Windows/InliningDiagnoser.cs
@@ -13,7 +13,7 @@ public class InliningDiagnoser : JitDiagnoser, IProfiler
private readonly bool logFailuresOnly = true;
private readonly bool filterByNamespace = true;
- private readonly string[] allowedNamespaces = null;
+ private readonly string[]? allowedNamespaces = null;
private string defaultNamespace;
// ReSharper disable once EmptyConstructor parameterless ctor is mandatory for DiagnosersLoader.CreateDiagnoser
@@ -35,7 +35,7 @@ public InliningDiagnoser(bool logFailuresOnly = true, bool filterByNamespace = t
///
/// only the methods that failed to get inlined. True by default.
/// list of namespaces from which inlining message should be print.
- public InliningDiagnoser(bool logFailuresOnly = true, string[] allowedNamespaces = null)
+ public InliningDiagnoser(bool logFailuresOnly = true, string[]? allowedNamespaces = null)
{
this.logFailuresOnly = logFailuresOnly;
this.allowedNamespaces = allowedNamespaces;
diff --git a/src/BenchmarkDotNet.Diagnostics.Windows/JitDiagnoser.cs b/src/BenchmarkDotNet.Diagnostics.Windows/JitDiagnoser.cs
index b189af0acb..0d06faf75b 100644
--- a/src/BenchmarkDotNet.Diagnostics.Windows/JitDiagnoser.cs
+++ b/src/BenchmarkDotNet.Diagnostics.Windows/JitDiagnoser.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using BenchmarkDotNet.Detectors;
using BenchmarkDotNet.Diagnosers;
using BenchmarkDotNet.Engines;
using BenchmarkDotNet.Loggers;
@@ -33,7 +34,7 @@ public void Handle(HostSignal signal, DiagnoserActionParameters parameters)
public IEnumerable Validate(ValidationParameters validationParameters)
{
- if (!RuntimeInformation.IsWindows())
+ if (!OsDetector.IsWindows())
{
yield return new ValidationError(true, $"{GetType().Name} is supported only on Windows");
}
diff --git a/src/BenchmarkDotNet.Diagnostics.Windows/JitStatsDiagnoser.cs b/src/BenchmarkDotNet.Diagnostics.Windows/JitStatsDiagnoser.cs
index a86989aef5..a101888a0e 100644
--- a/src/BenchmarkDotNet.Diagnostics.Windows/JitStatsDiagnoser.cs
+++ b/src/BenchmarkDotNet.Diagnostics.Windows/JitStatsDiagnoser.cs
@@ -6,6 +6,7 @@
using BenchmarkDotNet.Running;
using Microsoft.Diagnostics.Tracing.Parsers;
using Microsoft.Diagnostics.Tracing.Session;
+using Perfolizer.Metrology;
namespace BenchmarkDotNet.Diagnostics.Windows
{
@@ -97,7 +98,7 @@ private sealed class JitAllocatedMemoryDescriptor : IMetricDescriptor
public bool TheGreaterTheBetter => false;
public string NumberFormat => "N0";
public UnitType UnitType => UnitType.Size;
- public string Unit => SizeUnit.B.Name;
+ public string Unit => SizeUnit.B.Abbreviation;
public int PriorityInCategory => 0;
public bool GetIsAvailable(Metric metric) => true;
}
diff --git a/src/BenchmarkDotNet.Diagnostics.Windows/Sessions.cs b/src/BenchmarkDotNet.Diagnostics.Windows/Sessions.cs
index cf0b6fa88f..f0f5dcc475 100644
--- a/src/BenchmarkDotNet.Diagnostics.Windows/Sessions.cs
+++ b/src/BenchmarkDotNet.Diagnostics.Windows/Sessions.cs
@@ -5,13 +5,13 @@
using BenchmarkDotNet.Diagnosers;
using BenchmarkDotNet.Engines;
using BenchmarkDotNet.Exporters;
+using BenchmarkDotNet.Extensions;
using BenchmarkDotNet.Helpers;
using BenchmarkDotNet.Loggers;
-using BenchmarkDotNet.Extensions;
+using BenchmarkDotNet.Running;
using Microsoft.Diagnostics.Tracing;
using Microsoft.Diagnostics.Tracing.Parsers;
using Microsoft.Diagnostics.Tracing.Session;
-using BenchmarkDotNet.Running;
namespace BenchmarkDotNet.Diagnostics.Windows
{
@@ -90,7 +90,7 @@ internal override Session EnableProviders()
}
}
- internal abstract class Session : IDisposable
+ internal abstract class Session : DisposeAtProcessTermination
{
private const int MaxSessionNameLength = 128;
@@ -114,27 +114,16 @@ protected Session(string sessionName, DiagnoserActionParameters details, EtwProf
BufferSizeMB = config.BufferSizeInMb,
CpuSampleIntervalMSec = config.CpuSampleIntervalInMilliseconds,
};
-
- Console.CancelKeyPress += OnConsoleCancelKeyPress;
- AppDomain.CurrentDomain.ProcessExit += OnProcessExit;
}
- public void Dispose() => TraceEventSession.Dispose();
-
- internal void Stop()
+ public override void Dispose()
{
- TraceEventSession.Stop();
-
- Console.CancelKeyPress -= OnConsoleCancelKeyPress;
- AppDomain.CurrentDomain.ProcessExit -= OnProcessExit;
+ TraceEventSession.Dispose();
+ base.Dispose();
}
internal abstract Session EnableProviders();
- private void OnConsoleCancelKeyPress(object sender, ConsoleCancelEventArgs e) => Stop();
-
- private void OnProcessExit(object sender, EventArgs e) => Stop();
-
protected static string GetSessionName(BenchmarkCase benchmarkCase)
{
string benchmarkName = FullNameProvider.GetBenchmarkName(benchmarkCase);
diff --git a/src/BenchmarkDotNet.Diagnostics.Windows/Tracing/EngineEventLogParser.cs b/src/BenchmarkDotNet.Diagnostics.Windows/Tracing/EngineEventLogParser.cs
index c385c1c440..f7e3d8e6fd 100644
--- a/src/BenchmarkDotNet.Diagnostics.Windows/Tracing/EngineEventLogParser.cs
+++ b/src/BenchmarkDotNet.Diagnostics.Windows/Tracing/EngineEventLogParser.cs
@@ -8,7 +8,7 @@ namespace BenchmarkDotNet.Diagnostics.Windows.Tracing
{
public sealed class EngineEventLogParser : TraceEventParser
{
- private static volatile TraceEvent[] templates;
+ private static volatile TraceEvent[]? templates;
public EngineEventLogParser(TraceEventSource source, bool dontRegister = false) : base(source, dontRegister) { }
@@ -114,69 +114,69 @@ public event Action WorkloadActualStop
protected override string GetProviderName() { return ProviderName; }
- private static IterationEvent BenchmarkStartTemplate(Action action)
+ private static IterationEvent BenchmarkStartTemplate(Action? action)
{ // action, eventid, taskid, taskName, taskGuid, opcode, opcodeName, providerGuid, providerName
return new IterationEvent(action, EngineEventSource.BenchmarkStartEventId, (int)EngineEventSource.Tasks.Benchmark, nameof(EngineEventSource.Tasks.Benchmark), Guid.Empty, (int)EventOpcode.Start, nameof(EventOpcode.Start), ProviderGuid, ProviderName);
}
- private static IterationEvent BenchmarkStopTemplate(Action action)
+ private static IterationEvent BenchmarkStopTemplate(Action? action)
{ // action, eventid, taskid, taskName, taskGuid, opcode, opcodeName, providerGuid, providerName
return new IterationEvent(action, EngineEventSource.BenchmarkStopEventId, (int)EngineEventSource.Tasks.Benchmark, nameof(EngineEventSource.Tasks.Benchmark), Guid.Empty, (int)EventOpcode.Stop, nameof(EventOpcode.Stop), ProviderGuid, ProviderName);
}
- private static IterationEvent OverheadJittingStartTemplate(Action action)
+ private static IterationEvent OverheadJittingStartTemplate(Action? action)
=> CreateIterationStartTemplate(action, EngineEventSource.OverheadJittingStartEventId, EngineEventSource.Tasks.OverheadJitting);
- private static IterationEvent OverheadJittingStopTemplate(Action action)
+ private static IterationEvent OverheadJittingStopTemplate(Action? action)
=> CreateIterationStopTemplate(action, EngineEventSource.OverheadJittingStopEventId, EngineEventSource.Tasks.OverheadJitting);
- private static IterationEvent WorkloadJittingStartTemplate(Action action)
+ private static IterationEvent WorkloadJittingStartTemplate(Action? action)
=> CreateIterationStartTemplate(action, EngineEventSource.WorkloadJittingStartEventId, EngineEventSource.Tasks.WorkloadJitting);
- private static IterationEvent WorkloadJittingStopTemplate(Action action)
+ private static IterationEvent WorkloadJittingStopTemplate(Action? action)
=> CreateIterationStopTemplate(action, EngineEventSource.WorkloadJittingStopEventId, EngineEventSource.Tasks.WorkloadJitting);
- private static IterationEvent WorkloadPilotStartTemplate(Action action)
+ private static IterationEvent WorkloadPilotStartTemplate(Action? action)
=> CreateIterationStartTemplate(action, EngineEventSource.WorkloadPilotStartEventId, EngineEventSource.Tasks.WorkloadPilot);
- private static IterationEvent WorkloadPilotStopTemplate(Action action)
+ private static IterationEvent WorkloadPilotStopTemplate(Action? action)
=> CreateIterationStopTemplate(action, EngineEventSource.WorkloadPilotStopEventId, EngineEventSource.Tasks.WorkloadPilot);
- private static IterationEvent OverheadWarmupStartTemplate(Action action)
+ private static IterationEvent OverheadWarmupStartTemplate(Action? action)
=> CreateIterationStartTemplate(action, EngineEventSource.OverheadWarmupStartEventId, EngineEventSource.Tasks.OverheadWarmup);
- private static IterationEvent OverheadWarmupStopTemplate(Action action)
+ private static IterationEvent OverheadWarmupStopTemplate(Action? action)
=> CreateIterationStopTemplate(action, EngineEventSource.OverheadWarmupStopEventId, EngineEventSource.Tasks.OverheadWarmup);
- private static IterationEvent WorkloadWarmupStartTemplate(Action action)
+ private static IterationEvent WorkloadWarmupStartTemplate(Action? action)
=> CreateIterationStartTemplate(action, EngineEventSource.WorkloadWarmupStartEventId, EngineEventSource.Tasks.WorkloadWarmup);
- private static IterationEvent WorkloadWarmupStopTemplate(Action action)
+ private static IterationEvent WorkloadWarmupStopTemplate(Action? action)
=> CreateIterationStopTemplate(action, EngineEventSource.WorkloadWarmupStopEventId, EngineEventSource.Tasks.WorkloadWarmup);
- private static IterationEvent OverheadActualStartTemplate(Action action)
+ private static IterationEvent OverheadActualStartTemplate(Action? action)
=> CreateIterationStartTemplate(action, EngineEventSource.OverheadActualStartEventId, EngineEventSource.Tasks.OverheadActual);
- private static IterationEvent OverheadActualStopTemplate(Action action)
+ private static IterationEvent OverheadActualStopTemplate(Action? action)
=> CreateIterationStopTemplate(action, EngineEventSource.OverheadActualStopEventId, EngineEventSource.Tasks.OverheadActual);
- private static IterationEvent WorkloadActualStartTemplate(Action action)
+ private static IterationEvent WorkloadActualStartTemplate(Action? action)
=> CreateIterationStartTemplate(action, EngineEventSource.WorkloadActualStartEventId, EngineEventSource.Tasks.WorkloadActual);
- private static IterationEvent WorkloadActualStopTemplate(Action action)
+ private static IterationEvent WorkloadActualStopTemplate(Action? action)
=> CreateIterationStopTemplate(action, EngineEventSource.WorkloadActualStopEventId, EngineEventSource.Tasks.WorkloadActual);
- private static IterationEvent CreateIterationStartTemplate(Action action, int eventId, EventTask eventTask)
+ private static IterationEvent CreateIterationStartTemplate(Action? action, int eventId, EventTask eventTask)
{ // action, eventid, taskid, taskName, taskGuid, opcode, opcodeName, providerGuid, providerName
return new IterationEvent(action, eventId, (int)eventTask, eventTask.ToString(), Guid.Empty, (int)EventOpcode.Start, nameof(EventOpcode.Start), ProviderGuid, ProviderName);
}
- private static IterationEvent CreateIterationStopTemplate(Action action, int eventId, EventTask eventTask)
+ private static IterationEvent CreateIterationStopTemplate(Action? action, int eventId, EventTask eventTask)
{ // action, eventid, taskid, taskName, taskGuid, opcode, opcodeName, providerGuid, providerName
return new IterationEvent(action, eventId, (int)eventTask, eventTask.ToString(), Guid.Empty, (int)EventOpcode.Stop, nameof(EventOpcode.Stop), ProviderGuid, ProviderName);
}
- protected override void EnumerateTemplates(Func eventsToObserve, Action callback)
+ protected override void EnumerateTemplates(Func? eventsToObserve, Action callback)
{
if (templates == null)
{
diff --git a/src/BenchmarkDotNet.Diagnostics.Windows/Tracing/IterationEvent.cs b/src/BenchmarkDotNet.Diagnostics.Windows/Tracing/IterationEvent.cs
index 7908f7cf52..3367aa7c60 100644
--- a/src/BenchmarkDotNet.Diagnostics.Windows/Tracing/IterationEvent.cs
+++ b/src/BenchmarkDotNet.Diagnostics.Windows/Tracing/IterationEvent.cs
@@ -9,15 +9,15 @@ public sealed class IterationEvent : TraceEvent
{
public long TotalOperations => GetInt64At(0);
- private event Action target;
+ private event Action? target;
- internal IterationEvent(Action target, int eventID, int task, string taskName, Guid taskGuid, int opcode, string opcodeName, Guid providerGuid, string providerName)
- : base(eventID, task, taskName, taskGuid, opcode, opcodeName, providerGuid, providerName)
+ internal IterationEvent(Action? target, int eventId, int task, string taskName, Guid taskGuid, int opcode, string opcodeName, Guid providerGuid, string providerName)
+ : base(eventId, task, taskName, taskGuid, opcode, opcodeName, providerGuid, providerName)
{
this.target = target;
}
- protected override Delegate Target
+ protected override Delegate? Target
{
get => target;
set => target = (Action)value;
@@ -34,7 +34,7 @@ public override StringBuilder ToXml(StringBuilder sb)
return sb;
}
- public override object PayloadValue(int index) => index == 0 ? (object)TotalOperations : null;
+ public override object? PayloadValue(int index) => index == 0 ? TotalOperations : null;
protected override void Dispatch() => target?.Invoke(this);
diff --git a/src/BenchmarkDotNet.Diagnostics.Windows/Tracing/NativeMemoryLogParser.cs b/src/BenchmarkDotNet.Diagnostics.Windows/Tracing/NativeMemoryLogParser.cs
index 9e269f28fb..4d8b79c3d1 100644
--- a/src/BenchmarkDotNet.Diagnostics.Windows/Tracing/NativeMemoryLogParser.cs
+++ b/src/BenchmarkDotNet.Diagnostics.Windows/Tracing/NativeMemoryLogParser.cs
@@ -2,7 +2,6 @@
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
-using BenchmarkDotNet.Columns;
using BenchmarkDotNet.Diagnosers;
using BenchmarkDotNet.Engines;
using BenchmarkDotNet.Extensions;
@@ -12,6 +11,7 @@
using Microsoft.Diagnostics.Tracing.Etlx;
using Microsoft.Diagnostics.Tracing.Parsers.Kernel;
using Microsoft.Diagnostics.Tracing.Stacks;
+using Perfolizer.Metrology;
using Address = System.UInt64;
namespace BenchmarkDotNet.Diagnostics.Windows.Tracing
@@ -95,7 +95,7 @@ private IEnumerable Parse(TraceLog traceLog)
var heapParser = new HeapTraceProviderTraceEventParser(eventSource);
// We index by heap address and then within the heap we remember the allocation stack
var heaps = new Dictionary>();
- Dictionary lastHeapAllocs = null;
+ Dictionary? lastHeapAllocs = null;
Address lastHeapHandle = 0;
@@ -246,12 +246,14 @@ bool IsCallStackIn(StackSourceCallStackIndex index)
var memoryAllocatedPerOperation = totalAllocation / totalOperation;
var memoryLeakPerOperation = nativeLeakSize / totalOperation;
- logger.WriteLine($"Native memory allocated per single operation: {SizeValue.FromBytes(memoryAllocatedPerOperation).ToString(SizeUnit.B, benchmarkCase.Config.CultureInfo)}");
+ logger.WriteLine(
+ $"Native memory allocated per single operation: {SizeValue.FromBytes(memoryAllocatedPerOperation).ToString(SizeUnit.B, null, benchmarkCase.Config.CultureInfo)}");
logger.WriteLine($"Count of allocated object: {countOfAllocatedObject / totalOperation}");
if (nativeLeakSize != 0)
{
- logger.WriteLine($"Native memory leak per single operation: {SizeValue.FromBytes(memoryLeakPerOperation).ToString(SizeUnit.B, benchmarkCase.Config.CultureInfo)}");
+ logger.WriteLine(
+ $"Native memory leak per single operation: {SizeValue.FromBytes(memoryLeakPerOperation).ToString(SizeUnit.B, null, benchmarkCase.Config.CultureInfo)}");
}
var heapInfoList = heaps.Select(h => new { Address = h.Key, h.Value.Count, types = h.Value.Values });
@@ -267,7 +269,8 @@ bool IsCallStackIn(StackSourceCallStackIndex index)
};
}
- private static Dictionary CreateHeapCache(Address heapHandle, Dictionary> heaps, ref Dictionary lastHeapAllocs, ref Address lastHeapHandle)
+ private static Dictionary CreateHeapCache(Address heapHandle, Dictionary> heaps,
+ ref Dictionary lastHeapAllocs, ref Address lastHeapHandle)
{
Dictionary ret;
@@ -282,4 +285,4 @@ private static Dictionary CreateHeapCache(Address heapHandle, Dic
return ret;
}
}
-}
+}
\ 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..baf4c0383e
--- /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..7616ca468c
--- /dev/null
+++ b/src/BenchmarkDotNet.Diagnostics.dotMemory/DotMemoryDiagnoser.cs
@@ -0,0 +1,126 @@
+using System;
+using System.Reflection;
+using BenchmarkDotNet.Detectors;
+using BenchmarkDotNet.Diagnosers;
+using BenchmarkDotNet.Helpers;
+using BenchmarkDotNet.Jobs;
+using JetBrains.Profiler.SelfApi;
+
+namespace BenchmarkDotNet.Diagnostics.dotMemory;
+
+public class DotMemoryDiagnoser(Uri? nugetUrl = null, string? downloadTo = null) : SnapshotProfilerBase
+{
+ public override string ShortName => "dotMemory";
+
+ protected override void InitTool(Progress progress)
+ {
+ DotMemory.InitAsync(progress, nugetUrl, NuGetApi.V3, downloadTo).Wait();
+ }
+
+ protected override void AttachToCurrentProcess(string snapshotFile)
+ {
+ DotMemory.Attach(new DotMemory.Config().SaveToFile(snapshotFile));
+ }
+
+ protected override void AttachToProcessByPid(int pid, string snapshotFile)
+ {
+ DotMemory.Attach(new DotMemory.Config().ProfileExternalProcess(pid).SaveToFile(snapshotFile));
+ }
+
+ protected override void TakeSnapshot()
+ {
+ DotMemory.GetSnapshot();
+ }
+
+ protected override void Detach()
+ {
+ DotMemory.Detach();
+ }
+
+ protected override string CreateSnapshotFilePath(DiagnoserActionParameters parameters)
+ {
+ return ArtifactFileNameHelper.GetFilePath(parameters, "snapshots", DateTime.Now, "dmw", ".0000".Length);
+ }
+
+ protected override 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;
+ }
+
+ internal override 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:
+ case RuntimeMoniker.Net10_0:
+ return true;
+ case RuntimeMoniker.NotRecognized:
+ case RuntimeMoniker.Mono:
+ case RuntimeMoniker.NativeAot60:
+ case RuntimeMoniker.NativeAot70:
+ case RuntimeMoniker.NativeAot80:
+ case RuntimeMoniker.NativeAot90:
+ case RuntimeMoniker.NativeAot10_0:
+ case RuntimeMoniker.Wasm:
+ case RuntimeMoniker.WasmNet50:
+ case RuntimeMoniker.WasmNet60:
+ case RuntimeMoniker.WasmNet70:
+ case RuntimeMoniker.WasmNet80:
+ case RuntimeMoniker.WasmNet90:
+ case RuntimeMoniker.WasmNet10_0:
+ case RuntimeMoniker.MonoAOTLLVM:
+ case RuntimeMoniker.MonoAOTLLVMNet60:
+ case RuntimeMoniker.MonoAOTLLVMNet70:
+ case RuntimeMoniker.MonoAOTLLVMNet80:
+ case RuntimeMoniker.MonoAOTLLVMNet90:
+ case RuntimeMoniker.MonoAOTLLVMNet10_0:
+ case RuntimeMoniker.Mono60:
+ case RuntimeMoniker.Mono70:
+ case RuntimeMoniker.Mono80:
+ case RuntimeMoniker.Mono90:
+ case RuntimeMoniker.Mono10_0:
+#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 OsDetector.IsWindows();
+ case RuntimeMoniker.NetCoreApp30:
+ case RuntimeMoniker.NetCoreApp31:
+ return OsDetector.IsWindows() || OsDetector.IsLinux();
+ default:
+ throw new ArgumentOutOfRangeException(nameof(runtimeMoniker), runtimeMoniker, $"Runtime moniker {runtimeMoniker} is not supported");
+ }
+ }
+}
diff --git a/src/BenchmarkDotNet.Diagnostics.dotMemory/DotMemoryDiagnoserAttribute.cs b/src/BenchmarkDotNet.Diagnostics.dotMemory/DotMemoryDiagnoserAttribute.cs
new file mode 100644
index 0000000000..c0fb55d4f1
--- /dev/null
+++ b/src/BenchmarkDotNet.Diagnostics.dotMemory/DotMemoryDiagnoserAttribute.cs
@@ -0,0 +1,22 @@
+using System;
+using BenchmarkDotNet.Configs;
+
+namespace BenchmarkDotNet.Diagnostics.dotMemory;
+
+[AttributeUsage(AttributeTargets.Class)]
+public class DotMemoryDiagnoserAttribute : Attribute, IConfigSource
+{
+ public IConfig Config { get; }
+
+ public DotMemoryDiagnoserAttribute()
+ {
+ var diagnoser = new DotMemoryDiagnoser();
+ Config = ManualConfig.CreateEmpty().AddDiagnoser(diagnoser);
+ }
+
+ public DotMemoryDiagnoserAttribute(Uri? nugetUrl, string? downloadTo = null)
+ {
+ var diagnoser = new DotMemoryDiagnoser(nugetUrl, downloadTo);
+ Config = ManualConfig.CreateEmpty().AddDiagnoser(diagnoser);
+ }
+}
\ 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/BenchmarkDotNet.Diagnostics.dotTrace.csproj b/src/BenchmarkDotNet.Diagnostics.dotTrace/BenchmarkDotNet.Diagnostics.dotTrace.csproj
index e4037e5bcf..42c838e20f 100644
--- a/src/BenchmarkDotNet.Diagnostics.dotTrace/BenchmarkDotNet.Diagnostics.dotTrace.csproj
+++ b/src/BenchmarkDotNet.Diagnostics.dotTrace/BenchmarkDotNet.Diagnostics.dotTrace.csproj
@@ -6,6 +6,7 @@
BenchmarkDotNet.Diagnostics.dotTrace
BenchmarkDotNet.Diagnostics.dotTrace
BenchmarkDotNet.Diagnostics.dotTrace
+ enable
@@ -13,7 +14,7 @@
-
+
diff --git a/src/BenchmarkDotNet.Diagnostics.dotTrace/DotTraceDiagnoser.cs b/src/BenchmarkDotNet.Diagnostics.dotTrace/DotTraceDiagnoser.cs
index a53c305938..8b5d3a858f 100644
--- a/src/BenchmarkDotNet.Diagnostics.dotTrace/DotTraceDiagnoser.cs
+++ b/src/BenchmarkDotNet.Diagnostics.dotTrace/DotTraceDiagnoser.cs
@@ -1,146 +1,129 @@
using System;
-using System.Collections.Generic;
-using System.Collections.Immutable;
-using System.Linq;
-using BenchmarkDotNet.Analysers;
+using System.Reflection;
+using BenchmarkDotNet.Detectors;
using BenchmarkDotNet.Diagnosers;
-using BenchmarkDotNet.Engines;
-using BenchmarkDotNet.Exporters;
+using BenchmarkDotNet.Helpers;
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;
+using JetBrains.Profiler.SelfApi;
-namespace BenchmarkDotNet.Diagnostics.dotTrace
+namespace BenchmarkDotNet.Diagnostics.dotTrace;
+
+public class DotTraceDiagnoser(Uri? nugetUrl = null, string? downloadTo = null) : SnapshotProfilerBase
{
- public class DotTraceDiagnoser : IProfiler
+ public override string ShortName => "dotTrace";
+
+ protected override void InitTool(Progress progress)
{
- private readonly Uri nugetUrl;
- private readonly string toolsDownloadFolder;
+ DotTrace.InitAsync(progress, nugetUrl, NuGetApi.V3, downloadTo).Wait();
+ }
- public DotTraceDiagnoser(Uri nugetUrl = null, string toolsDownloadFolder = null)
- {
- this.nugetUrl = nugetUrl;
- this.toolsDownloadFolder = toolsDownloadFolder;
- }
+ protected override void AttachToCurrentProcess(string snapshotFile)
+ {
+ DotTrace.Attach(new DotTrace.Config().SaveToFile(snapshotFile));
+ DotTrace.StartCollectingData();
+ }
- public IEnumerable Ids => new[] { "DotTrace" };
- public string ShortName => "dotTrace";
+ protected override void AttachToProcessByPid(int pid, string snapshotFile)
+ {
+ DotTrace.Attach(new DotTrace.Config().ProfileExternalProcess(pid).SaveToFile(snapshotFile));
+ DotTrace.StartCollectingData();
+ }
- public RunMode GetRunMode(BenchmarkCase benchmarkCase)
- {
- return IsSupported(benchmarkCase.Job.Environment.GetRuntime().RuntimeMoniker) ? RunMode.ExtraRun : RunMode.None;
- }
+ protected override void TakeSnapshot()
+ {
+ DotTrace.StopCollectingData();
+ DotTrace.SaveData();
+ }
- private readonly List snapshotFilePaths = new ();
+ protected override void Detach()
+ {
+ DotTrace.Detach();
+ }
- 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);
+ protected override string CreateSnapshotFilePath(DiagnoserActionParameters parameters)
+ {
+ return ArtifactFileNameHelper.GetFilePath(parameters, "snapshots", DateTime.Now, "dtp", ".0000".Length);
+ }
- var runtimeMoniker = job.Environment.GetRuntime().RuntimeMoniker;
- if (!IsSupported(runtimeMoniker))
- {
- logger.WriteLineError($"Runtime '{runtimeMoniker}' is not supported by dotTrace");
- return;
- }
+ protected override string GetRunnerPath()
+ {
+ var consoleRunnerPackageField = typeof(DotTrace).GetField("ConsoleRunnerPackage", BindingFlags.NonPublic | BindingFlags.Static);
+ if (consoleRunnerPackageField == null)
+ throw new InvalidOperationException("Field 'ConsoleRunnerPackage' not found.");
- 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;
- }
- }
+ object? consoleRunnerPackage = consoleRunnerPackageField.GetValue(null);
+ if (consoleRunnerPackage == null)
+ throw new InvalidOperationException("Unable to get value of 'ConsoleRunnerPackage'.");
- public IEnumerable Exporters => Enumerable.Empty();
- public IEnumerable Analysers => Enumerable.Empty();
+ var consoleRunnerPackageType = consoleRunnerPackage.GetType();
+ var getRunnerPathMethod = consoleRunnerPackageType.GetMethod("GetRunnerPath");
+ if (getRunnerPathMethod == null)
+ throw new InvalidOperationException("Method 'GetRunnerPath' not found.");
- 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");
- }
- }
+ string? runnerPath = getRunnerPathMethod.Invoke(consoleRunnerPackage, null) as string;
+ if (runnerPath == null)
+ throw new InvalidOperationException("Unable to invoke 'GetRunnerPath'.");
- internal static bool IsSupported(RuntimeMoniker runtimeMoniker)
+ return runnerPath;
+ }
+
+ internal override bool IsSupported(RuntimeMoniker runtimeMoniker)
+ {
+ switch (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:
+ 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:
+ case RuntimeMoniker.Net10_0:
+ return true;
+ case RuntimeMoniker.NotRecognized:
+ case RuntimeMoniker.Mono:
+ case RuntimeMoniker.NativeAot60:
+ case RuntimeMoniker.NativeAot70:
+ case RuntimeMoniker.NativeAot80:
+ case RuntimeMoniker.NativeAot90:
+ case RuntimeMoniker.NativeAot10_0:
+ case RuntimeMoniker.Wasm:
+ case RuntimeMoniker.WasmNet50:
+ case RuntimeMoniker.WasmNet60:
+ case RuntimeMoniker.WasmNet70:
+ case RuntimeMoniker.WasmNet80:
+ case RuntimeMoniker.WasmNet90:
+ case RuntimeMoniker.WasmNet10_0:
+ case RuntimeMoniker.MonoAOTLLVM:
+ case RuntimeMoniker.MonoAOTLLVMNet60:
+ case RuntimeMoniker.MonoAOTLLVMNet70:
+ case RuntimeMoniker.MonoAOTLLVMNet80:
+ case RuntimeMoniker.MonoAOTLLVMNet90:
+ case RuntimeMoniker.MonoAOTLLVMNet10_0:
+ case RuntimeMoniker.Mono60:
+ case RuntimeMoniker.Mono70:
+ case RuntimeMoniker.Mono80:
+ case RuntimeMoniker.Mono90:
+ case RuntimeMoniker.Mono10_0:
#pragma warning disable CS0618 // Type or member is obsolete
- case RuntimeMoniker.NetCoreApp50:
+ 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}");
- }
+ return false;
+ case RuntimeMoniker.NetCoreApp20:
+ case RuntimeMoniker.NetCoreApp21:
+ case RuntimeMoniker.NetCoreApp22:
+ return OsDetector.IsWindows();
+ case RuntimeMoniker.NetCoreApp30:
+ case RuntimeMoniker.NetCoreApp31:
+ return OsDetector.IsWindows() || OsDetector.IsLinux();
+ default:
+ throw new ArgumentOutOfRangeException(nameof(runtimeMoniker), runtimeMoniker, $"Runtime moniker {runtimeMoniker} is not supported");
}
}
-}
\ No newline at end of file
+}
diff --git a/src/BenchmarkDotNet.Diagnostics.dotTrace/DotTraceDiagnoserAttribute.cs b/src/BenchmarkDotNet.Diagnostics.dotTrace/DotTraceDiagnoserAttribute.cs
index de803e6443..f056a98cbd 100644
--- a/src/BenchmarkDotNet.Diagnostics.dotTrace/DotTraceDiagnoserAttribute.cs
+++ b/src/BenchmarkDotNet.Diagnostics.dotTrace/DotTraceDiagnoserAttribute.cs
@@ -1,21 +1,22 @@
using System;
using BenchmarkDotNet.Configs;
-namespace BenchmarkDotNet.Diagnostics.dotTrace
+namespace BenchmarkDotNet.Diagnostics.dotTrace;
+
+[AttributeUsage(AttributeTargets.Class)]
+public class DotTraceDiagnoserAttribute : Attribute, IConfigSource
{
- [AttributeUsage(AttributeTargets.Class)]
- public class DotTraceDiagnoserAttribute : Attribute, IConfigSource
- {
- public IConfig Config { get; }
+ public IConfig Config { get; }
- public DotTraceDiagnoserAttribute()
- {
- Config = ManualConfig.CreateEmpty().AddDiagnoser(new DotTraceDiagnoser());
- }
+ public DotTraceDiagnoserAttribute()
+ {
+ var diagnoser = new DotTraceDiagnoser();
+ Config = ManualConfig.CreateEmpty().AddDiagnoser(diagnoser);
+ }
- public DotTraceDiagnoserAttribute(Uri nugetUrl = null, string toolsDownloadFolder = null)
- {
- Config = ManualConfig.CreateEmpty().AddDiagnoser(new DotTraceDiagnoser(nugetUrl, toolsDownloadFolder));
- }
+ public DotTraceDiagnoserAttribute(Uri? nugetUrl, string? downloadTo = null)
+ {
+ var diagnoser = new DotTraceDiagnoser(nugetUrl, downloadTo);
+ Config = ManualConfig.CreateEmpty().AddDiagnoser(diagnoser);
}
}
\ No newline at end of file
diff --git a/src/BenchmarkDotNet.Diagnostics.dotTrace/DotTraceToolBase.cs b/src/BenchmarkDotNet.Diagnostics.dotTrace/DotTraceToolBase.cs
deleted file mode 100644
index c41ffc53e5..0000000000
--- a/src/BenchmarkDotNet.Diagnostics.dotTrace/DotTraceToolBase.cs
+++ /dev/null
@@ -1,145 +0,0 @@
-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
deleted file mode 100644
index 9c5161b309..0000000000
--- a/src/BenchmarkDotNet.Diagnostics.dotTrace/ExternalDotTraceTool.cs
+++ /dev/null
@@ -1,84 +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.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
deleted file mode 100644
index a124e3e495..0000000000
--- a/src/BenchmarkDotNet.Diagnostics.dotTrace/InProcessDotTraceTool.cs
+++ /dev/null
@@ -1,28 +0,0 @@
-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
deleted file mode 100644
index 1d8249f31e..0000000000
--- a/src/BenchmarkDotNet.Diagnostics.dotTrace/Progress.cs
+++ /dev/null
@@ -1,38 +0,0 @@
-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.Disassembler.x64/ClrMdV1Disassembler.cs b/src/BenchmarkDotNet.Disassembler.x64/ClrMdV1Disassembler.cs
index 29ee184df3..734aa470e4 100644
--- a/src/BenchmarkDotNet.Disassembler.x64/ClrMdV1Disassembler.cs
+++ b/src/BenchmarkDotNet.Disassembler.x64/ClrMdV1Disassembler.cs
@@ -93,6 +93,7 @@ private static DisassembledMethod[] Disassemble(Settings settings, State state)
{
var result = new List();
+ using var sourceCodeProvider = new SourceCodeProvider();
while (state.Todo.Count != 0)
{
var methodInfo = state.Todo.Dequeue();
@@ -101,7 +102,7 @@ private static DisassembledMethod[] Disassemble(Settings settings, State state)
continue; // already handled
if (settings.MaxDepth >= methodInfo.Depth)
- result.Add(DisassembleMethod(methodInfo, state, settings));
+ result.Add(DisassembleMethod(methodInfo, state, settings, sourceCodeProvider));
}
return result.ToArray();
@@ -110,7 +111,7 @@ private static DisassembledMethod[] Disassemble(Settings settings, State state)
private static bool CanBeDisassembled(ClrMethod method)
=> !((method.ILOffsetMap is null || method.ILOffsetMap.Length == 0) && (method.HotColdInfo is null || method.HotColdInfo.HotStart == 0 || method.HotColdInfo.HotSize == 0));
- private static DisassembledMethod DisassembleMethod(MethodInfo methodInfo, State state, Settings settings)
+ private static DisassembledMethod DisassembleMethod(MethodInfo methodInfo, State state, Settings settings, SourceCodeProvider sourceCodeProvider)
{
var method = methodInfo.Method;
@@ -133,7 +134,7 @@ private static DisassembledMethod DisassembleMethod(MethodInfo methodInfo, State
var uniqueSourceCodeLines = new HashSet(new SharpComparer());
// for getting C# code we always use the original ILOffsetMap
foreach (var map in method.ILOffsetMap.Where(map => map.StartAddress < map.EndAddress && map.ILOffset >= 0).OrderBy(map => map.StartAddress))
- foreach (var sharp in SourceCodeProvider.GetSource(method, map))
+ foreach (var sharp in sourceCodeProvider.GetSource(method, map))
uniqueSourceCodeLines.Add(sharp);
codes.AddRange(uniqueSourceCodeLines);
diff --git a/src/BenchmarkDotNet.Disassembler.x64/SourceCodeProvider.cs b/src/BenchmarkDotNet.Disassembler.x64/SourceCodeProvider.cs
index 7cdb00a6d9..f827369775 100644
--- a/src/BenchmarkDotNet.Disassembler.x64/SourceCodeProvider.cs
+++ b/src/BenchmarkDotNet.Disassembler.x64/SourceCodeProvider.cs
@@ -8,13 +8,34 @@
namespace BenchmarkDotNet.Disassemblers
{
- internal static class SourceCodeProvider
+ // This is taken from the Samples\FileAndLineNumbers projects from microsoft/clrmd,
+ // and replaces the previously-available SourceLocation functionality.
+
+ internal class SourceLocation
{
- private static readonly Dictionary SourceFileCache = new Dictionary();
+ public string FilePath;
+ public int LineNumber;
+ public int LineNumberEnd;
+ public int ColStart;
+ public int ColEnd;
+ }
+
+ internal class SourceCodeProvider : IDisposable
+ {
+ private readonly Dictionary sourceFileCache = new Dictionary();
+ private readonly Dictionary pdbReaders = new Dictionary();
+
+ public void Dispose()
+ {
+ foreach (var reader in pdbReaders.Values)
+ {
+ reader?.Dispose();
+ }
+ }
- internal static IEnumerable GetSource(ClrMethod method, ILToNativeMap map)
+ internal IEnumerable GetSource(ClrMethod method, ILToNativeMap map)
{
- var sourceLocation = method.GetSourceLocation(map.ILOffset);
+ var sourceLocation = GetSourceLocation(method, map.ILOffset);
if (sourceLocation == null)
yield break;
@@ -39,16 +60,16 @@ internal static IEnumerable GetSource(ClrMethod method, ILToNativeMap map
}
}
- private static string ReadSourceLine(string file, int line)
+ private string ReadSourceLine(string file, int line)
{
- if (!SourceFileCache.TryGetValue(file, out string[] contents))
+ if (!sourceFileCache.TryGetValue(file, out string[] contents))
{
// sometimes the symbols report some disk location from MS CI machine like "E:\A\_work\308\s\src\mscorlib\shared\System\Random.cs" for .NET Core 2.0
if (!File.Exists(file))
return null;
contents = File.ReadAllLines(file);
- SourceFileCache.Add(file, contents);
+ sourceFileCache.Add(file, contents);
}
return line - 1 < contents.Length
@@ -84,29 +105,8 @@ private static string GetSmartPointer(string sourceLine, int? start, int? end)
return new string(prefix);
}
- }
-
-
- // This is taken from the Samples\FileAndLineNumbers projects from microsoft/clrmd,
- // and replaces the previously-available SourceLocation functionality.
-
- internal class SourceLocation
- {
- public string FilePath;
- public int LineNumber;
- public int LineNumberEnd;
- public int ColStart;
- public int ColEnd;
- }
-
- internal static class ClrSourceExtensions
- {
- // TODO Not sure we want this to be a shared dictionary, especially without
- // any synchronization. Probably want to put this hanging off the Context
- // somewhere, or inside SymbolCache.
- private static readonly Dictionary s_pdbReaders = new Dictionary();
- internal static SourceLocation GetSourceLocation(this ClrMethod method, int ilOffset)
+ internal SourceLocation GetSourceLocation(ClrMethod method, int ilOffset)
{
PdbReader reader = GetReaderForMethod(method);
if (reader == null)
@@ -116,7 +116,7 @@ internal static SourceLocation GetSourceLocation(this ClrMethod method, int ilOf
return FindNearestLine(function, ilOffset);
}
- internal static SourceLocation GetSourceLocation(this ClrStackFrame frame)
+ internal SourceLocation GetSourceLocation(ClrStackFrame frame)
{
PdbReader reader = GetReaderForMethod(frame.Method);
if (reader == null)
@@ -134,7 +134,7 @@ private static SourceLocation FindNearestLine(PdbFunction function, int ilOffset
return null;
int distance = int.MaxValue;
- SourceLocation nearest = null;
+ SourceLocation? nearest = null;
foreach (PdbSequencePointCollection sequenceCollection in function.SequencePoints)
{
@@ -178,15 +178,15 @@ private static int FindIlOffset(ClrStackFrame frame)
return last;
}
- private static PdbReader GetReaderForMethod(ClrMethod method)
+ private PdbReader GetReaderForMethod(ClrMethod method)
{
ClrModule module = method?.Type?.Module;
PdbInfo info = module?.Pdb;
- PdbReader reader = null;
+ PdbReader? reader = null;
if (info != null)
{
- if (!s_pdbReaders.TryGetValue(info, out reader))
+ if (!pdbReaders.TryGetValue(info, out reader))
{
SymbolLocator locator = GetSymbolLocator(module);
string pdbPath = locator.FindPdb(info);
@@ -207,7 +207,7 @@ private static PdbReader GetReaderForMethod(ClrMethod method)
}
}
- s_pdbReaders[info] = reader;
+ pdbReaders[info] = reader;
}
}
diff --git a/src/BenchmarkDotNet.Exporters.Plotting/BenchmarkDotNet.Exporters.Plotting.csproj b/src/BenchmarkDotNet.Exporters.Plotting/BenchmarkDotNet.Exporters.Plotting.csproj
new file mode 100644
index 0000000000..5a53f980fb
--- /dev/null
+++ b/src/BenchmarkDotNet.Exporters.Plotting/BenchmarkDotNet.Exporters.Plotting.csproj
@@ -0,0 +1,19 @@
+
+
+
+ BenchmarkDotNet plotting export support.
+ netstandard2.0
+ BenchmarkDotNet.Exporters.Plotting
+ BenchmarkDotNet.Exporters.Plotting
+ BenchmarkDotNet.Exporters.Plotting
+
+ True
+ enable
+
+
+
+
+
+
+
+
diff --git a/src/BenchmarkDotNet.Exporters.Plotting/ScottPlotExporter.cs b/src/BenchmarkDotNet.Exporters.Plotting/ScottPlotExporter.cs
new file mode 100644
index 0000000000..77695d4ae7
--- /dev/null
+++ b/src/BenchmarkDotNet.Exporters.Plotting/ScottPlotExporter.cs
@@ -0,0 +1,438 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using BenchmarkDotNet.Engines;
+using BenchmarkDotNet.Loggers;
+using BenchmarkDotNet.Properties;
+using BenchmarkDotNet.Reports;
+using ScottPlot;
+using ScottPlot.Plottables;
+
+namespace BenchmarkDotNet.Exporters.Plotting
+{
+ ///
+ /// Provides plot exports as .png files.
+ ///
+ public class ScottPlotExporter : IExporter
+ {
+ ///
+ /// Default instance of the exporter with default configuration.
+ ///
+ public static readonly IExporter Default = new ScottPlotExporter();
+
+ ///
+ /// Gets the name of the Exporter type.
+ ///
+ public string Name => nameof(ScottPlotExporter);
+
+ ///
+ /// Initializes a new instance of ScottPlotExporter.
+ ///
+ /// The width of all plots in pixels (optional). Defaults to 1920.
+ /// The height of all plots in pixels (optional). Defaults to 1080.
+ public ScottPlotExporter(int width = 1920, int height = 1080)
+ {
+ this.Width = width;
+ this.Height = height;
+ this.IncludeBarPlot = true;
+ this.IncludeBoxPlot = true;
+ this.RotateLabels = true;
+ }
+
+ ///
+ /// Gets or sets the width of all plots in pixels.
+ ///
+ public int Width { get; set; }
+
+ ///
+ /// Gets or sets the height of all plots in pixels.
+ ///
+ public int Height { get; set; }
+
+ ///
+ /// Gets or sets the common font size for ticks, labels etc. (defaults to 14).
+ ///
+ public int FontSize { get; set; } = 14;
+
+ ///
+ /// Gets or sets the font size for the chart title. (defaults to 28).
+ ///
+ public int TitleFontSize { get; set; } = 28;
+
+ ///
+ /// Gets or sets a value indicating whether labels for Plot X-axis should be rotated.
+ /// This allows for longer labels at the expense of chart height.
+ ///
+ public bool RotateLabels { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether a bar plot for time-per-op
+ /// measurement values should be exported.
+ ///
+ public bool IncludeBarPlot { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether a box plot or whisker plot for time-per-op
+ /// measurement values should be exported.
+ ///
+ public bool IncludeBoxPlot { get; set; }
+
+ ///
+ /// Not supported.
+ ///
+ /// This parameter is not used.
+ /// This parameter is not used.
+ ///
+ public void ExportToLog(Summary summary, ILogger logger)
+ {
+ throw new NotSupportedException();
+ }
+
+ ///
+ /// Exports plots to .png file.
+ ///
+ /// The summary to be exported.
+ /// Logger to output to.
+ /// The file paths of every plot exported.
+ public IEnumerable ExportToFiles(Summary summary, ILogger consoleLogger)
+ {
+ var title = summary.Title;
+ var version = BenchmarkDotNetInfo.Instance.BrandTitle;
+ var annotations = GetAnnotations(version);
+
+ var (timeUnit, timeScale) = GetTimeUnit(summary.Reports
+ .SelectMany(m => m.AllMeasurements.Where(m => m.Is(IterationMode.Workload, IterationStage.Result))));
+
+ foreach (var benchmark in summary.Reports.GroupBy(r => r.BenchmarkCase.Descriptor.Type.Name))
+ {
+ var benchmarkName = benchmark.Key;
+
+ // Get the measurement nanoseconds per op, divided by time scale, grouped by target and Job [param].
+ var timeStats = from report in benchmark
+ let jobId = report.BenchmarkCase.DisplayInfo.Replace(report.BenchmarkCase.Descriptor.DisplayInfo + ": ", string.Empty)
+ from measurement in report.AllMeasurements
+ where measurement.Is(IterationMode.Workload, IterationStage.Result)
+ let measurementValue = measurement.Nanoseconds / measurement.Operations
+ group measurementValue / timeScale by (Target: report.BenchmarkCase.Descriptor.WorkloadMethodDisplayInfo, JobId: jobId) into g
+ select new ChartStats(g.Key.Target, g.Key.JobId, g.ToList());
+
+ if (this.IncludeBarPlot)
+ {
+ // -barplot.png
+ yield return CreateBarPlot(
+ $"{title} - {benchmarkName}",
+ Path.Combine(summary.ResultsDirectoryPath, $"{title}-{benchmarkName}-barplot.png"),
+ $"Time ({timeUnit})",
+ "Target",
+ timeStats,
+ annotations);
+ }
+
+ if (this.IncludeBoxPlot)
+ {
+ // -boxplot.png
+ yield return CreateBoxPlot(
+ $"{title} - {benchmarkName}",
+ Path.Combine(summary.ResultsDirectoryPath, $"{title}-{benchmarkName}-boxplot.png"),
+ $"Time ({timeUnit})",
+ "Target",
+ timeStats,
+ annotations);
+ }
+
+ /* TODO: Rest of the RPlotExporter plots.
+ --density.png
+ --facetTimeline.png
+ --facetTimelineSmooth.png
+ ---timelineSmooth.png
+ ---timelineSmooth.png*/
+ }
+ }
+
+ ///
+ /// Calculate Standard Deviation.
+ ///
+ /// Values to calculate from.
+ /// Standard deviation of values.
+ private static double StandardError(IReadOnlyList values)
+ {
+ double average = values.Average();
+ double sumOfSquaresOfDifferences = values.Select(val => (val - average) * (val - average)).Sum();
+ double standardDeviation = Math.Sqrt(sumOfSquaresOfDifferences / values.Count);
+ return standardDeviation / Math.Sqrt(values.Count);
+ }
+
+ ///
+ /// Gets the lowest appropriate time scale across all measurements.
+ ///
+ /// All measurements
+ /// A unit and scaling factor to convert from nanoseconds.
+ private (string Unit, double ScaleFactor) GetTimeUnit(IEnumerable values)
+ {
+ var minValue = values.Select(m => m.Nanoseconds / m.Operations).DefaultIfEmpty(0d).Min();
+ if (minValue > 1000000000d)
+ {
+ return ("sec", 1000000000d);
+ }
+
+ if (minValue > 1000000d)
+ {
+ return ("ms", 1000000d);
+ }
+
+ if (minValue > 1000d)
+ {
+ return ("us", 1000d);
+ }
+
+ return ("ns", 1d);
+ }
+
+ private string CreateBarPlot(string title, string fileName, string yLabel, string xLabel, IEnumerable data, IReadOnlyList annotations)
+ {
+ Plot plt = new Plot();
+ plt.Title(title, this.TitleFontSize);
+ plt.YLabel(yLabel, this.FontSize);
+ plt.XLabel(xLabel, this.FontSize);
+
+ var palette = new ScottPlot.Palettes.Category10();
+
+ var legendPalette = data.Select(d => d.JobId)
+ .Distinct()
+ .Select((jobId, index) => (jobId, index))
+ .ToDictionary(t => t.jobId, t => palette.GetColor(t.index));
+
+ plt.Legend.IsVisible = true;
+ plt.Legend.Alignment = Alignment.UpperRight;
+ plt.Legend.FontSize = this.FontSize;
+ var legend = data.Select(d => d.JobId)
+ .Distinct()
+ .Select((label, index) => new LegendItem()
+ {
+ LabelText = label,
+ FillColor = legendPalette[label]
+ })
+ .ToList();
+
+ plt.Legend.ManualItems.AddRange(legend);
+
+ var jobCount = plt.Legend.ManualItems.Count;
+ var ticks = data
+ .Select((d, index) => new Tick(index, d.Target))
+ .ToArray();
+
+ plt.Axes.Left.TickLabelStyle.FontSize = this.FontSize;
+ plt.Axes.Bottom.TickGenerator = new ScottPlot.TickGenerators.NumericManual(ticks);
+ plt.Axes.Bottom.MajorTickStyle.Length = 0;
+ plt.Axes.Bottom.TickLabelStyle.FontSize = this.FontSize;
+
+ if (this.RotateLabels)
+ {
+ plt.Axes.Bottom.TickLabelStyle.Rotation = 45;
+ plt.Axes.Bottom.TickLabelStyle.Alignment = Alignment.MiddleLeft;
+
+ // determine the width of the largest tick label
+ float largestLabelWidth = 0;
+ foreach (Tick tick in ticks)
+ {
+ PixelSize size = plt.Axes.Bottom.TickLabelStyle.Measure(tick.Label).Size;
+ largestLabelWidth = Math.Max(largestLabelWidth, size.Width);
+ }
+
+ // ensure axis panels do not get smaller than the largest label
+ plt.Axes.Bottom.MinimumSize = largestLabelWidth * 2;
+ plt.Axes.Right.MinimumSize = largestLabelWidth;
+ }
+
+ var bars = data
+ .Select((d, index) => new Bar()
+ {
+ Position = ticks[index].Position,
+ Value = d.Mean,
+ Error = d.StdError,
+ FillColor = legendPalette[d.JobId]
+ });
+ plt.Add.Bars(bars.ToList());
+
+ // Tell the plot to autoscale with no padding beneath the bars
+ plt.Axes.Margins(bottom: 0, right: .2);
+
+ plt.PlottableList.AddRange(annotations);
+
+ plt.SavePng(fileName, this.Width, this.Height);
+ return Path.GetFullPath(fileName);
+ }
+
+ private string CreateBoxPlot(string title, string fileName, string yLabel, string xLabel, IEnumerable data, IReadOnlyList annotations)
+ {
+ Plot plt = new Plot();
+ plt.Title(title, this.TitleFontSize);
+ plt.YLabel(yLabel, this.FontSize);
+ plt.XLabel(xLabel, this.FontSize);
+
+ var palette = new ScottPlot.Palettes.Category10();
+
+ var legendPalette = data.Select(d => d.JobId)
+ .Distinct()
+ .Select((jobId, index) => (jobId, index))
+ .ToDictionary(t => t.jobId, t => palette.GetColor(t.index));
+
+ plt.Legend.IsVisible = true;
+ plt.Legend.Alignment = Alignment.UpperRight;
+ plt.Legend.FontSize = this.FontSize;
+ var legend = data.Select(d => d.JobId)
+ .Distinct()
+ .Select((label, index) => new LegendItem()
+ {
+ LabelText = label,
+ FillColor = legendPalette[label]
+ })
+ .ToList();
+
+ plt.Legend.ManualItems.AddRange(legend);
+
+ var jobCount = plt.Legend.ManualItems.Count;
+ var ticks = data
+ .Select((d, index) => new Tick(index, d.Target))
+ .ToArray();
+
+ plt.Axes.Left.TickLabelStyle.FontSize = this.FontSize;
+ plt.Axes.Bottom.TickGenerator = new ScottPlot.TickGenerators.NumericManual(ticks);
+ plt.Axes.Bottom.MajorTickStyle.Length = 0;
+ plt.Axes.Bottom.TickLabelStyle.FontSize = this.FontSize;
+
+ if (this.RotateLabels)
+ {
+ plt.Axes.Bottom.TickLabelStyle.Rotation = 45;
+ plt.Axes.Bottom.TickLabelStyle.Alignment = Alignment.MiddleLeft;
+
+ // determine the width of the largest tick label
+ float largestLabelWidth = 0;
+ foreach (Tick tick in ticks)
+ {
+ PixelSize size = plt.Axes.Bottom.TickLabelStyle.Measure(tick.Label).Size;
+ largestLabelWidth = Math.Max(largestLabelWidth, size.Width);
+ }
+
+ // ensure axis panels do not get smaller than the largest label
+ plt.Axes.Bottom.MinimumSize = largestLabelWidth * 2;
+ plt.Axes.Right.MinimumSize = largestLabelWidth;
+ }
+
+ int globalIndex = 0;
+ foreach (var (targetGroup, targetGroupIndex) in data.GroupBy(s => s.Target).Select((targetGroup, index) => (targetGroup, index)))
+ {
+ var boxes = targetGroup.Select(job => (job.JobId, Stats: job.CalculateBoxPlotStatistics())).Select((j, jobIndex) => new Box()
+ {
+ Position = ticks[globalIndex++].Position,
+ FillStyle = new FillStyle() { Color = legendPalette[j.JobId] },
+ LineStyle = new LineStyle() { Color = Colors.Black },
+ BoxMin = j.Stats.Q1,
+ BoxMax = j.Stats.Q3,
+ WhiskerMin = j.Stats.Min,
+ WhiskerMax = j.Stats.Max,
+ BoxMiddle = j.Stats.Median
+ })
+ .ToList();
+ plt.Add.Boxes(boxes);
+ }
+
+ // Tell the plot to autoscale with a small padding below the boxes.
+ plt.Axes.Margins(bottom: 0.05, right: .2);
+
+ plt.PlottableList.AddRange(annotations);
+
+ plt.SavePng(fileName, this.Width, this.Height);
+ return Path.GetFullPath(fileName);
+ }
+
+ ///
+ /// Provides a list of annotations to put over the data area.
+ ///
+ /// The version to be displayed.
+ /// A list of annotations for every plot.
+ private IReadOnlyList GetAnnotations(string version)
+ {
+ var versionAnnotation = new Annotation()
+ {
+ LabelStyle =
+ {
+ Text = version,
+ FontSize = 14,
+ ForeColor = new Color(0, 0, 0, 100)
+ },
+ OffsetY = 10,
+ OffsetX = 20,
+ Alignment = Alignment.LowerRight
+ };
+
+
+ return new[] { versionAnnotation };
+ }
+
+ private class ChartStats
+ {
+ public ChartStats(string Target, string JobId, IReadOnlyList Values)
+ {
+ this.Target = Target;
+ this.JobId = JobId;
+ this.Values = Values;
+ }
+
+ public string Target { get; }
+
+ public string JobId { get; }
+
+ public IReadOnlyList Values { get; }
+
+ public double Min => this.Values.DefaultIfEmpty(0d).Min();
+
+ public double Max => this.Values.DefaultIfEmpty(0d).Max();
+
+ public double Mean => this.Values.DefaultIfEmpty(0d).Average();
+
+ public double StdError => StandardError(this.Values);
+
+
+ private static (int MidPoint, double Median) CalculateMedian(ReadOnlySpan values)
+ {
+ int n = values.Length;
+ var midPoint = n / 2;
+
+ // Check if count is even, if so use average of the two middle values,
+ // otherwise take the middle value.
+ var median = n % 2 == 0 ? (values[midPoint - 1] + values[midPoint]) / 2d : values[midPoint];
+ return (midPoint, median);
+ }
+
+ ///
+ /// Calculate the mid points.
+ ///
+ ///
+ public (double Min, double Q1, double Median, double Q3, double Max, double[] Outliers) CalculateBoxPlotStatistics()
+ {
+ var values = this.Values.ToArray();
+ Array.Sort(values);
+ var s = values.AsSpan();
+ var (midPoint, median) = CalculateMedian(s);
+
+ var (q1Index, q1) = midPoint > 0 ? CalculateMedian(s.Slice(0, midPoint)) : (midPoint, median);
+ var (q3Index, q3) = midPoint + 1 < s.Length ? CalculateMedian(s.Slice(midPoint + 1)) : (midPoint, median);
+ var iqr = q3 - q1;
+ var lowerFence = q1 - 1.5d * iqr;
+ var upperFence = q3 + 1.5d * iqr;
+ var outliers = values.Where(v => v < lowerFence || v > upperFence).ToArray();
+ var nonOutliers = values.Where(v => v >= lowerFence && v <= upperFence).ToArray();
+ return (
+ nonOutliers.FirstOrDefault(),
+ q1,
+ median,
+ q3,
+ nonOutliers.LastOrDefault(),
+ outliers
+ );
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/BenchmarkDotNet.TestAdapter/BenchmarkCaseExtensions.cs b/src/BenchmarkDotNet.TestAdapter/BenchmarkCaseExtensions.cs
new file mode 100644
index 0000000000..82e6d4b6ec
--- /dev/null
+++ b/src/BenchmarkDotNet.TestAdapter/BenchmarkCaseExtensions.cs
@@ -0,0 +1,96 @@
+using BenchmarkDotNet.Attributes;
+using BenchmarkDotNet.Characteristics;
+using BenchmarkDotNet.Exporters;
+using BenchmarkDotNet.Extensions;
+using BenchmarkDotNet.Running;
+using Microsoft.TestPlatform.AdapterUtilities;
+using Microsoft.VisualStudio.TestPlatform.ObjectModel;
+using System;
+
+namespace BenchmarkDotNet.TestAdapter
+{
+ ///
+ /// A set of extensions for BenchmarkCase to support converting to VSTest TestCase objects.
+ ///
+ internal static class BenchmarkCaseExtensions
+ {
+ ///
+ /// Converts a BDN BenchmarkCase to a VSTest TestCase.
+ ///
+ /// The BenchmarkCase to convert.
+ /// The dll or exe of the benchmark project.
+ /// Whether or not the display name should include the job name.
+ /// The VSTest TestCase.
+ internal static TestCase ToVsTestCase(this BenchmarkCase benchmarkCase, string assemblyPath, bool includeJobInName = false)
+ {
+ var benchmarkMethod = benchmarkCase.Descriptor.WorkloadMethod;
+ var fullClassName = benchmarkCase.Descriptor.Type.GetCorrectCSharpTypeName();
+ var parametrizedMethodName = FullNameProvider.GetMethodName(benchmarkCase);
+
+ var displayJobInfo = benchmarkCase.GetUnrandomizedJobDisplayInfo();
+ var displayMethodName = parametrizedMethodName + (includeJobInName ? $" [{displayJobInfo}]" : "");
+ var displayName = $"{fullClassName}.{displayMethodName}";
+
+ // We use displayName as FQN to workaround the Rider/R# problem with FQNs processing
+ // See: https://github.com/dotnet/BenchmarkDotNet/issues/2494
+ var fullyQualifiedName = displayName;
+
+ var vsTestCase = new TestCase(fullyQualifiedName, VsTestAdapter.ExecutorUri, assemblyPath)
+ {
+ DisplayName = displayName,
+ Id = GetTestCaseId(benchmarkCase)
+ };
+
+ var benchmarkAttribute = benchmarkMethod.ResolveAttribute();
+ if (benchmarkAttribute != null)
+ {
+ vsTestCase.CodeFilePath = benchmarkAttribute.SourceCodeFile;
+ vsTestCase.LineNumber = benchmarkAttribute.SourceCodeLineNumber;
+ }
+
+ var categories = DefaultCategoryDiscoverer.Instance.GetCategories(benchmarkMethod);
+ foreach (var category in categories)
+ vsTestCase.Traits.Add("Category", category);
+
+ vsTestCase.Traits.Add("", "BenchmarkDotNet");
+
+ return vsTestCase;
+ }
+
+ ///
+ /// If an ID is not provided, a random string is used for the ID. This method will identify if randomness was
+ /// used for the ID and return the Job's DisplayInfo with that randomness removed so that the same benchmark
+ /// can be referenced across multiple processes.
+ ///
+ /// The benchmark case.
+ /// The benchmark case' job's DisplayInfo without randomness.
+ internal static string GetUnrandomizedJobDisplayInfo(this BenchmarkCase benchmarkCase)
+ {
+ var jobDisplayInfo = benchmarkCase.Job.DisplayInfo;
+ if (!benchmarkCase.Job.HasValue(CharacteristicObject.IdCharacteristic) &&
+ benchmarkCase.Job.ResolvedId.StartsWith("Job-", StringComparison.OrdinalIgnoreCase))
+ {
+ // Replace Job-ABCDEF with Job
+ jobDisplayInfo = "Job" + jobDisplayInfo.Substring(benchmarkCase.Job.ResolvedId.Length);
+ }
+
+ return jobDisplayInfo;
+ }
+
+ ///
+ /// Gets an ID for a given BenchmarkCase that is uniquely identifiable from discovery to execution phase.
+ ///
+ /// The benchmark case.
+ /// The test case ID.
+ internal static Guid GetTestCaseId(this BenchmarkCase benchmarkCase)
+ {
+ var testIdProvider = new TestIdProvider();
+ testIdProvider.AppendString(VsTestAdapter.ExecutorUriString);
+ testIdProvider.AppendString(benchmarkCase.Descriptor.Type.Namespace ?? string.Empty);
+ testIdProvider.AppendString(benchmarkCase.Descriptor.DisplayInfo);
+ testIdProvider.AppendString(benchmarkCase.GetUnrandomizedJobDisplayInfo());
+ testIdProvider.AppendString(benchmarkCase.Parameters.DisplayInfo);
+ return testIdProvider.GetId();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/BenchmarkDotNet.TestAdapter/BenchmarkDotNet.TestAdapter.csproj b/src/BenchmarkDotNet.TestAdapter/BenchmarkDotNet.TestAdapter.csproj
new file mode 100644
index 0000000000..bd6c053bab
--- /dev/null
+++ b/src/BenchmarkDotNet.TestAdapter/BenchmarkDotNet.TestAdapter.csproj
@@ -0,0 +1,28 @@
+
+
+
+ netstandard2.0;net462
+ BenchmarkDotNet.TestAdapter
+ BenchmarkDotNet.TestAdapter
+ BenchmarkDotNet.TestAdapter
+ True
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/BenchmarkDotNet.TestAdapter/BenchmarkEnumerator.cs b/src/BenchmarkDotNet.TestAdapter/BenchmarkEnumerator.cs
new file mode 100644
index 0000000000..daf3e2222e
--- /dev/null
+++ b/src/BenchmarkDotNet.TestAdapter/BenchmarkEnumerator.cs
@@ -0,0 +1,48 @@
+using BenchmarkDotNet.Extensions;
+using BenchmarkDotNet.Helpers;
+using BenchmarkDotNet.Running;
+using BenchmarkDotNet.Toolchains;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+
+namespace BenchmarkDotNet.TestAdapter
+{
+ ///
+ /// A class used for enumerating all the benchmarks in an assembly.
+ ///
+ internal static class BenchmarkEnumerator
+ {
+ ///
+ /// Returns all the BenchmarkRunInfo objects from a given assembly.
+ ///
+ /// The dll or exe of the benchmark project.
+ /// The benchmarks inside the assembly.
+ public static BenchmarkRunInfo[] GetBenchmarksFromAssemblyPath(string assemblyPath)
+ {
+ var assembly = Assembly.LoadFrom(assemblyPath);
+
+ var isDebugAssembly = assembly.IsJitOptimizationDisabled() ?? false;
+
+ return GenericBenchmarksBuilder.GetRunnableBenchmarks(assembly.GetRunnableBenchmarks())
+ .Select(type =>
+ {
+ var benchmarkRunInfo = BenchmarkConverter.TypeToBenchmarks(type);
+ if (isDebugAssembly)
+ {
+ // If the assembly is a debug assembly, then only display them if they will run in-process
+ // This will allow people to debug their benchmarks using VSTest if they wish.
+ benchmarkRunInfo = new BenchmarkRunInfo(
+ benchmarkRunInfo.BenchmarksCases.Where(c => c.GetToolchain().IsInProcess).ToArray(),
+ benchmarkRunInfo.Type,
+ benchmarkRunInfo.Config);
+ }
+
+ return benchmarkRunInfo;
+ })
+ .Where(runInfo => runInfo.BenchmarksCases.Length > 0)
+ .ToArray();
+ }
+ }
+}
diff --git a/src/BenchmarkDotNet.TestAdapter/BenchmarkExecutor.cs b/src/BenchmarkDotNet.TestAdapter/BenchmarkExecutor.cs
new file mode 100644
index 0000000000..f1cd64f34e
--- /dev/null
+++ b/src/BenchmarkDotNet.TestAdapter/BenchmarkExecutor.cs
@@ -0,0 +1,86 @@
+using BenchmarkDotNet.Configs;
+using BenchmarkDotNet.Running;
+using BenchmarkDotNet.TestAdapter.Remoting;
+using Microsoft.VisualStudio.TestPlatform.ObjectModel;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+
+namespace BenchmarkDotNet.TestAdapter
+{
+ ///
+ /// A class used for executing benchmarks
+ ///
+ internal class BenchmarkExecutor
+ {
+ private readonly CancellationTokenSource cts = new ();
+
+ ///
+ /// Runs all the benchmarks in the given assembly, updating the TestExecutionRecorder as they get run.
+ ///
+ /// The dll or exe of the benchmark project.
+ /// The interface used to record the current test execution progress.
+ ///
+ /// An optional list of benchmark IDs specifying which benchmarks to run.
+ /// These IDs are the same as the ones generated for the VSTest TestCase.
+ ///
+ public void RunBenchmarks(string assemblyPath, TestExecutionRecorderWrapper recorder, HashSet? benchmarkIds = null)
+ {
+ var benchmarks = BenchmarkEnumerator.GetBenchmarksFromAssemblyPath(assemblyPath);
+ var testCases = new List();
+
+ var filteredBenchmarks = new List();
+ foreach (var benchmark in benchmarks)
+ {
+ var needsJobInfo = benchmark.BenchmarksCases.Select(c => c.Job.DisplayInfo).Distinct().Count() > 1;
+ var filteredCases = new List();
+ foreach (var benchmarkCase in benchmark.BenchmarksCases)
+ {
+ var testId = benchmarkCase.GetTestCaseId();
+ if (benchmarkIds == null || benchmarkIds.Contains(testId))
+ {
+ filteredCases.Add(benchmarkCase);
+ testCases.Add(benchmarkCase.ToVsTestCase(assemblyPath, needsJobInfo));
+ }
+ }
+
+ if (filteredCases.Count > 0)
+ {
+ filteredBenchmarks.Add(new BenchmarkRunInfo(filteredCases.ToArray(), benchmark.Type, benchmark.Config));
+ }
+ }
+
+ benchmarks = filteredBenchmarks.ToArray();
+
+ if (benchmarks.Length == 0)
+ return;
+
+ // Create an event processor which will subscribe to events and push them to VSTest
+ var eventProcessor = new VsTestEventProcessor(testCases, recorder, cts.Token);
+
+ // Create a logger which will forward all log messages in BDN to the VSTest logger.
+ var logger = new VsTestLogger(recorder.GetLogger());
+
+ // Modify all the benchmarks so that the event process and logger is added.
+ benchmarks = benchmarks
+ .Select(b => new BenchmarkRunInfo(
+ b.BenchmarksCases,
+ b.Type,
+ b.Config.AddEventProcessor(eventProcessor).AddLogger(logger).CreateImmutableConfig()))
+ .ToArray();
+
+ // Run all the benchmarks, and ensure that any tests that don't have a result yet are sent.
+ BenchmarkRunner.Run(benchmarks);
+ eventProcessor.SendUnsentTestResults();
+ }
+
+ ///
+ /// Stop the benchmarks when next able.
+ ///
+ public void Cancel()
+ {
+ cts.Cancel();
+ }
+ }
+}
diff --git a/src/BenchmarkDotNet.TestAdapter/Remoting/BenchmarkEnumeratorWrapper.cs b/src/BenchmarkDotNet.TestAdapter/Remoting/BenchmarkEnumeratorWrapper.cs
new file mode 100644
index 0000000000..b3ad68bb23
--- /dev/null
+++ b/src/BenchmarkDotNet.TestAdapter/Remoting/BenchmarkEnumeratorWrapper.cs
@@ -0,0 +1,35 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace BenchmarkDotNet.TestAdapter.Remoting
+{
+ ///
+ /// A wrapper around the BenchmarkEnumerator for passing data across AppDomain boundaries.
+ ///
+ internal class BenchmarkEnumeratorWrapper : MarshalByRefObject
+ {
+ ///
+ /// Gets a list of VSTest TestCases from the given assembly.
+ /// Each test case is serialized into a string so that it can be used across AppDomain boundaries.
+ ///
+ /// The dll or exe of the benchmark project.
+ /// The serialized test cases.
+ public List GetTestCasesFromAssemblyPathSerialized(string assemblyPath)
+ {
+ var serializedTestCases = new List();
+ foreach (var runInfo in BenchmarkEnumerator.GetBenchmarksFromAssemblyPath(assemblyPath))
+ {
+ // If all the benchmarks have the same job, then no need to include job info.
+ var needsJobInfo = runInfo.BenchmarksCases.Select(c => c.Job.DisplayInfo).Distinct().Count() > 1;
+ foreach (var benchmarkCase in runInfo.BenchmarksCases)
+ {
+ var testCase = benchmarkCase.ToVsTestCase(assemblyPath, needsJobInfo);
+ serializedTestCases.Add(SerializationHelpers.Serialize(testCase));
+ }
+ }
+
+ return serializedTestCases;
+ }
+ }
+}
diff --git a/src/BenchmarkDotNet.TestAdapter/Remoting/BenchmarkExecutorWrapper.cs b/src/BenchmarkDotNet.TestAdapter/Remoting/BenchmarkExecutorWrapper.cs
new file mode 100644
index 0000000000..646ae2f8be
--- /dev/null
+++ b/src/BenchmarkDotNet.TestAdapter/Remoting/BenchmarkExecutorWrapper.cs
@@ -0,0 +1,23 @@
+using System;
+using System.Collections.Generic;
+
+namespace BenchmarkDotNet.TestAdapter.Remoting
+{
+ ///
+ /// A wrapper around the BenchmarkExecutor that works across AppDomain boundaries.
+ ///
+ internal class BenchmarkExecutorWrapper : MarshalByRefObject
+ {
+ private readonly BenchmarkExecutor benchmarkExecutor = new ();
+
+ public void RunBenchmarks(string assemblyPath, TestExecutionRecorderWrapper recorder, HashSet? benchmarkIds = null)
+ {
+ benchmarkExecutor.RunBenchmarks(assemblyPath, recorder, benchmarkIds);
+ }
+
+ public void Cancel()
+ {
+ benchmarkExecutor.Cancel();
+ }
+ }
+}
diff --git a/src/BenchmarkDotNet.TestAdapter/Remoting/MessageLoggerWrapper.cs b/src/BenchmarkDotNet.TestAdapter/Remoting/MessageLoggerWrapper.cs
new file mode 100644
index 0000000000..00c4f5325f
--- /dev/null
+++ b/src/BenchmarkDotNet.TestAdapter/Remoting/MessageLoggerWrapper.cs
@@ -0,0 +1,23 @@
+using Microsoft.VisualStudio.TestPlatform.ObjectModel.Logging;
+using System;
+
+namespace BenchmarkDotNet.TestAdapter.Remoting
+{
+ ///
+ /// A wrapper around an IMessageLogger that works across AppDomain boundaries.
+ ///
+ internal class MessageLoggerWrapper : MarshalByRefObject, IMessageLogger
+ {
+ private readonly IMessageLogger logger;
+
+ public MessageLoggerWrapper(IMessageLogger logger)
+ {
+ this.logger = logger;
+ }
+
+ public void SendMessage(TestMessageLevel testMessageLevel, string message)
+ {
+ logger.SendMessage(testMessageLevel, message);
+ }
+ }
+}
diff --git a/src/BenchmarkDotNet.TestAdapter/Remoting/SerializationHelpers.cs b/src/BenchmarkDotNet.TestAdapter/Remoting/SerializationHelpers.cs
new file mode 100644
index 0000000000..5b13bd5175
--- /dev/null
+++ b/src/BenchmarkDotNet.TestAdapter/Remoting/SerializationHelpers.cs
@@ -0,0 +1,26 @@
+using Microsoft.VisualStudio.TestPlatform.CommunicationUtilities;
+
+namespace BenchmarkDotNet.TestAdapter.Remoting
+{
+ ///
+ /// A set of helper methods for serializing and deserializing the VSTest TestCases and TestReports.
+ ///
+ internal static class SerializationHelpers
+ {
+ // Version number of the VSTest protocol that the adapter supports. Only needs to be updated when
+ // the VSTest protocol has a change and this test adapter wishes to take a dependency on it.
+ // A list of protocol versions and a summary of the changes that were made in them can be found here:
+ // https://github.com/microsoft/vstest/blob/main/docs/Overview.md#protocolversion-request
+ private const int VsTestProtocolVersion = 7;
+
+ public static string Serialize(T data)
+ {
+ return JsonDataSerializer.Instance.Serialize(data, version: VsTestProtocolVersion);
+ }
+
+ public static T Deserialize(string data)
+ {
+ return JsonDataSerializer.Instance.Deserialize(data, version: VsTestProtocolVersion)!;
+ }
+ }
+}
diff --git a/src/BenchmarkDotNet.TestAdapter/Remoting/TestExecutionRecorderWrapper.cs b/src/BenchmarkDotNet.TestAdapter/Remoting/TestExecutionRecorderWrapper.cs
new file mode 100644
index 0000000000..0669e79019
--- /dev/null
+++ b/src/BenchmarkDotNet.TestAdapter/Remoting/TestExecutionRecorderWrapper.cs
@@ -0,0 +1,39 @@
+using Microsoft.VisualStudio.TestPlatform.ObjectModel;
+using Microsoft.VisualStudio.TestPlatform.ObjectModel.Adapter;
+using System;
+
+namespace BenchmarkDotNet.TestAdapter.Remoting
+{
+ ///
+ /// A wrapper around the ITestExecutionRecorder which works across AppDomain boundaries.
+ ///
+ internal class TestExecutionRecorderWrapper : MarshalByRefObject
+ {
+ private readonly ITestExecutionRecorder testExecutionRecorder;
+
+ public TestExecutionRecorderWrapper(ITestExecutionRecorder testExecutionRecorder)
+ {
+ this.testExecutionRecorder = testExecutionRecorder;
+ }
+
+ public MessageLoggerWrapper GetLogger()
+ {
+ return new MessageLoggerWrapper(testExecutionRecorder);
+ }
+
+ internal void RecordStart(string serializedTestCase)
+ {
+ testExecutionRecorder.RecordStart(SerializationHelpers.Deserialize(serializedTestCase));
+ }
+
+ internal void RecordEnd(string serializedTestCase, TestOutcome testOutcome)
+ {
+ testExecutionRecorder.RecordEnd(SerializationHelpers.Deserialize(serializedTestCase), testOutcome);
+ }
+
+ internal void RecordResult(string serializedTestResult)
+ {
+ testExecutionRecorder.RecordResult(SerializationHelpers.Deserialize(serializedTestResult));
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/BenchmarkDotNet.TestAdapter/VSTestAdapter.cs b/src/BenchmarkDotNet.TestAdapter/VSTestAdapter.cs
new file mode 100644
index 0000000000..eb6695de1b
--- /dev/null
+++ b/src/BenchmarkDotNet.TestAdapter/VSTestAdapter.cs
@@ -0,0 +1,213 @@
+using BenchmarkDotNet.TestAdapter.Remoting;
+using Microsoft.VisualStudio.TestPlatform.ObjectModel;
+using Microsoft.VisualStudio.TestPlatform.ObjectModel.Adapter;
+using Microsoft.VisualStudio.TestPlatform.ObjectModel.Logging;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using System.Threading;
+
+namespace BenchmarkDotNet.TestAdapter
+{
+ ///
+ /// Discovers and executes benchmarks using the VSTest protocol.
+ ///
+ [ExtensionUri(ExecutorUriString)]
+ [DefaultExecutorUri(ExecutorUriString)]
+ [FileExtension(".dll")]
+ [FileExtension(".exe")]
+ public class VsTestAdapter : ITestExecutor, ITestDiscoverer
+ {
+ // This URI is used to identify the adapter.
+ internal const string ExecutorUriString = "executor://BenchmarkDotNet.TestAdapter";
+ internal static readonly Uri ExecutorUri = new Uri(ExecutorUriString);
+
+ ///
+ /// Cancellation token used to stop any benchmarks that are currently running.
+ ///
+ private CancellationTokenSource? cts = null;
+
+ ///
+ /// Discovers the benchmarks.
+ ///
+ /// List of assemblies to search for benchmarks in.
+ /// A context that the discovery is performed in.
+ /// Logger that sends messages back to VSTest host.
+ /// Interface that provides methods for sending discovered benchmarks back to the host.
+ public void DiscoverTests(
+ IEnumerable sources,
+ IDiscoveryContext discoveryContext,
+ IMessageLogger logger,
+ ITestCaseDiscoverySink discoverySink)
+ {
+ foreach (var source in sources)
+ {
+ ValidateSourceIsAssemblyOrThrow(source);
+ foreach (var testCase in GetVsTestCasesFromAssembly(source, logger))
+ {
+ discoverySink.SendTestCase(testCase);
+ }
+ }
+ }
+
+ ///
+ /// Runs a given set of test cases that represent benchmarks.
+ ///
+ /// The tests to run.
+ /// A context that the run is performed in.
+ /// Interface used for communicating with the VSTest host.
+ public void RunTests(IEnumerable? tests, IRunContext? runContext, IFrameworkHandle? frameworkHandle)
+ {
+ if (tests == null)
+ throw new ArgumentNullException(nameof(tests));
+ if (frameworkHandle == null)
+ throw new ArgumentNullException(nameof(frameworkHandle));
+
+ cts ??= new CancellationTokenSource();
+
+ foreach (var testsPerAssembly in tests.GroupBy(t => t.Source))
+ RunBenchmarks(testsPerAssembly.Key, frameworkHandle, testsPerAssembly);
+
+ cts = null;
+ }
+
+ ///
+ /// Runs all benchmarks in the given set of sources (assemblies).
+ ///
+ /// The assemblies to run.
+ /// A context that the run is performed in.
+ /// Interface used for communicating with the VSTest host.
+ public void RunTests(IEnumerable? sources, IRunContext? runContext, IFrameworkHandle? frameworkHandle)
+ {
+ if (sources == null)
+ throw new ArgumentNullException(nameof(sources));
+ if (frameworkHandle == null)
+ throw new ArgumentNullException(nameof(frameworkHandle));
+
+ cts ??= new CancellationTokenSource();
+
+ foreach (var source in sources)
+ RunBenchmarks(source, frameworkHandle);
+
+ cts = null;
+ }
+
+ ///
+ /// Stops any currently running benchmarks.
+ ///
+ public void Cancel()
+ {
+ cts?.Cancel();
+ }
+
+ ///
+ /// Gets the VSTest test cases in the given assembly.
+ ///
+ /// The dll or exe of the benchmark project.
+ /// A logger that sends logs to VSTest.
+ /// The VSTest test cases inside the given assembly.
+ private static List GetVsTestCasesFromAssembly(string assemblyPath, IMessageLogger logger)
+ {
+ try
+ {
+ // Ensure that the test enumeration is done inside the context of the source directory.
+ var enumerator = (BenchmarkEnumeratorWrapper)CreateIsolatedType(typeof(BenchmarkEnumeratorWrapper), assemblyPath);
+ var testCases = enumerator
+ .GetTestCasesFromAssemblyPathSerialized(assemblyPath)
+ .Select(SerializationHelpers.Deserialize)
+ .ToList();
+
+ // Validate that all test ids are unique
+ var idLookup = new Dictionary();
+ foreach (var testCase in testCases)
+ {
+ if (idLookup.TryGetValue(testCase.Id, out var matchingCase))
+ throw new Exception($"Encountered Duplicate Test ID: '{testCase.DisplayName}' and '{matchingCase}'");
+
+ idLookup[testCase.Id] = testCase.DisplayName;
+ }
+
+ return testCases;
+ }
+ catch (Exception ex)
+ {
+ logger.SendMessage(TestMessageLevel.Error, $"Failed to load benchmarks from assembly\n{ex}");
+ throw;
+ }
+ }
+
+ ///
+ /// Runs the benchmarks in the given source.
+ ///
+ /// The dll or exe of the benchmark project.
+ /// An interface used to communicate with the VSTest host.
+ ///
+ /// The specific test cases to be run if specified.
+ /// If unspecified, runs all the test cases in the source.
+ ///
+ private void RunBenchmarks(string source, IFrameworkHandle frameworkHandle, IEnumerable? testCases = null)
+ {
+ ValidateSourceIsAssemblyOrThrow(source);
+
+ // Create a HashSet of all the TestCase IDs to be run if specified.
+ var caseIds = testCases == null ? null : new HashSet(testCases.Select(c => c.Id));
+
+ try
+ {
+ // Ensure that test execution is done inside the context of the source directory.
+ var executor = (BenchmarkExecutorWrapper)CreateIsolatedType(typeof(BenchmarkExecutorWrapper), source);
+ cts?.Token.Register(executor.Cancel);
+
+ executor.RunBenchmarks(source, new TestExecutionRecorderWrapper(frameworkHandle), caseIds);
+ }
+ catch (Exception ex)
+ {
+ frameworkHandle.SendMessage(TestMessageLevel.Error, $"Failed to run benchmarks in assembly\n{ex}");
+ throw;
+ }
+ }
+
+ ///
+ /// This will create the given type in a child AppDomain when used in .NET Framework.
+ /// If not in the .NET Framework, it will use the current AppDomain.
+ ///
+ /// The type to create.
+ /// The dll or exe of the benchmark project.
+ /// The created object.
+ private static object CreateIsolatedType(Type type, string assemblyPath)
+ {
+ // .NET Framework runs require a custom AppDomain to be set up to run the benchmarks in because otherwise,
+ // all the assemblies will be loaded from the VSTest console rather than from the directory that the BDN
+ // program under test lives in. .NET Core assembly resolution is smarter and will correctly load the right
+ // assembly versions as needed and does not require a custom AppDomain. Unfortunately, the APIs needed to
+ // create the AppDomain for .NET Framework are not part of .NET Standard, and so a multi-targeting solution
+ // such as this is required to get this to work. This same approach is also used by other .NET unit testing
+ // libraries as well, further justifying this approach to solving how to get the correct assemblies loaded.
+#if NETFRAMEWORK
+ var appBase = Path.GetDirectoryName(assemblyPath);
+ var setup = new AppDomainSetup { ApplicationBase = appBase };
+ var domainName = $"Isolated Domain for {type.Name}";
+ var appDomain = AppDomain.CreateDomain(domainName, null, setup);
+ return appDomain.CreateInstanceAndUnwrap(
+ type.Assembly.FullName, type.FullName, false, BindingFlags.Default, null, null, null, null);
+#else
+ return Activator.CreateInstance(type);
+#endif
+ }
+
+ private static void ValidateSourceIsAssemblyOrThrow(string source)
+ {
+ if (string.IsNullOrEmpty(source))
+ throw new ArgumentException($"'{nameof(source)}' cannot be null or whitespace.", nameof(source));
+
+ if (!Path.HasExtension(source))
+ throw new NotSupportedException($"Missing extension on source '{source}', must have the extension '.dll' or '.exe'.");
+
+ var extension = Path.GetExtension(source);
+ if (!string.Equals(extension, ".dll", StringComparison.OrdinalIgnoreCase) && !string.Equals(extension, ".exe", StringComparison.OrdinalIgnoreCase))
+ throw new NotSupportedException($"Unsupported extension on source '{source}', must have the extension '.dll' or '.exe'.");
+ }
+ }
+}
diff --git a/src/BenchmarkDotNet.TestAdapter/VSTestEventProcessor.cs b/src/BenchmarkDotNet.TestAdapter/VSTestEventProcessor.cs
new file mode 100644
index 0000000000..35438d4d80
--- /dev/null
+++ b/src/BenchmarkDotNet.TestAdapter/VSTestEventProcessor.cs
@@ -0,0 +1,198 @@
+using BenchmarkDotNet.EventProcessors;
+using BenchmarkDotNet.Extensions;
+using BenchmarkDotNet.Reports;
+using BenchmarkDotNet.Running;
+using BenchmarkDotNet.TestAdapter.Remoting;
+using BenchmarkDotNet.Toolchains.Results;
+using BenchmarkDotNet.Validators;
+using Microsoft.VisualStudio.TestPlatform.ObjectModel;
+using Perfolizer.Mathematics.Histograms;
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Globalization;
+using System.Linq;
+using System.Text;
+using System.Threading;
+
+namespace BenchmarkDotNet.TestAdapter
+{
+ ///
+ /// An event processor which will pass on benchmark execution information to VSTest.
+ ///
+ internal class VsTestEventProcessor : EventProcessor
+ {
+ private readonly Dictionary cases;
+ private readonly TestExecutionRecorderWrapper recorder;
+ private readonly CancellationToken cancellationToken;
+ private readonly Stopwatch runTimerStopwatch = new ();
+ private readonly Dictionary testResults = new ();
+ private readonly HashSet sentTestResults = new ();
+
+ public VsTestEventProcessor(
+ List cases,
+ TestExecutionRecorderWrapper recorder,
+ CancellationToken cancellationToken)
+ {
+ this.cases = cases.ToDictionary(c => c.Id);
+ this.recorder = recorder;
+ this.cancellationToken = cancellationToken;
+ }
+
+ public override void OnValidationError(ValidationError validationError)
+ {
+ // If the error is not linked to a benchmark case, then set the error on all benchmarks
+ var errorCases = validationError.BenchmarkCase == null
+ ? cases.Values.ToList()
+ : new List { cases[validationError.BenchmarkCase.GetTestCaseId()] };
+ foreach (var testCase in errorCases)
+ {
+ var testResult = GetOrCreateTestResult(testCase);
+
+ if (validationError.IsCritical)
+ {
+ // Fail if there is a critical validation error
+ testResult.Outcome = TestOutcome.Failed;
+
+ // Append validation error message to end of test case error message
+ testResult.ErrorMessage = testResult.ErrorMessage == null
+ ? validationError.Message
+ : $"{testResult.ErrorMessage}\n{validationError.Message}";
+
+ // The test result is not sent yet, in case there are multiple validation errors that need to be sent.
+ }
+ else
+ {
+ // If the validation error is not critical, append it as a message
+ testResult.Messages.Add(new TestResultMessage(TestResultMessage.StandardOutCategory, $"WARNING: {validationError.Message}\n"));
+ }
+ }
+ }
+
+ public override void OnBuildComplete(BuildPartition buildPartition, BuildResult buildResult)
+ {
+ // Only need to handle build failures
+ if (!buildResult.IsBuildSuccess)
+ {
+ foreach (var benchmarkBuildInfo in buildPartition.Benchmarks)
+ {
+ var testCase = cases[benchmarkBuildInfo.BenchmarkCase.GetTestCaseId()];
+ var testResult = GetOrCreateTestResult(testCase);
+
+ if (buildResult.GenerateException != null)
+ testResult.ErrorMessage = $"// Generate Exception: {buildResult.GenerateException.Message}";
+ else if (!buildResult.IsBuildSuccess && buildResult.TryToExplainFailureReason(out string reason))
+ testResult.ErrorMessage = $"// Build Error: {reason}";
+ else if (buildResult.ErrorMessage != null)
+ testResult.ErrorMessage = $"// Build Error: {buildResult.ErrorMessage}";
+ testResult.Outcome = TestOutcome.Failed;
+
+ // Send the result immediately
+ RecordStart(testCase);
+ RecordEnd(testCase, testResult.Outcome);
+ RecordResult(testResult);
+ sentTestResults.Add(testCase.Id);
+ }
+ }
+ }
+
+ public override void OnStartRunBenchmark(BenchmarkCase benchmarkCase)
+ {
+ // TODO: add proper cancellation support to BDN so that we don't need to do cancellation through the event processor
+ cancellationToken.ThrowIfCancellationRequested();
+
+ var testCase = cases[benchmarkCase.GetTestCaseId()];
+ var testResult = GetOrCreateTestResult(testCase);
+ testResult.StartTime = DateTimeOffset.UtcNow;
+
+ RecordStart(testCase);
+ runTimerStopwatch.Restart();
+ }
+
+ public override void OnEndRunBenchmark(BenchmarkCase benchmarkCase, BenchmarkReport report)
+ {
+ var testCase = cases[benchmarkCase.GetTestCaseId()];
+ var testResult = GetOrCreateTestResult(testCase);
+ testResult.EndTime = DateTimeOffset.UtcNow;
+ testResult.Duration = runTimerStopwatch.Elapsed;
+ testResult.Outcome = report.Success ? TestOutcome.Passed : TestOutcome.Failed;
+
+ var resultRuns = report.GetResultRuns();
+
+ // Provide the raw result runs data.
+ testResult.SetPropertyValue(VsTestProperties.Measurement, resultRuns.Select(m => m.Nanoseconds.ToString()).ToArray());
+
+ // Add a message to the TestResult which contains the results summary.
+ testResult.Messages.Add(new TestResultMessage(TestResultMessage.StandardOutCategory, report.BenchmarkCase.DisplayInfo + "\n"));
+ testResult.Messages.Add(new TestResultMessage(TestResultMessage.StandardOutCategory, $"Runtime = {report.GetRuntimeInfo()}; GC = {report.GetGcInfo()}\n"));
+
+ var statistics = resultRuns.GetStatistics();
+ var cultureInfo = CultureInfo.InvariantCulture;
+ var formatter = statistics.CreateNanosecondFormatter(cultureInfo);
+
+ var builder = new StringBuilder();
+ var histogram = HistogramBuilder.Adaptive.Build(statistics.Sample.Values);
+ builder.AppendLine("-------------------- Histogram --------------------");
+ builder.AppendLine(histogram.ToString(formatter));
+ builder.AppendLine("---------------------------------------------------");
+
+ var statisticsOutput = statistics.ToString(cultureInfo, formatter, calcHistogram: false);
+ builder.AppendLine(statisticsOutput);
+
+ testResult.Messages.Add(new TestResultMessage(TestResultMessage.StandardOutCategory, builder.ToString()));
+
+ RecordEnd(testResult.TestCase, testResult.Outcome);
+ RecordResult(testResult);
+ sentTestResults.Add(testCase.Id);
+ }
+
+ ///
+ /// Iterate through all the benchmarks that were scheduled to run, and if they haven't been sent yet, send the result through.
+ ///
+ public void SendUnsentTestResults()
+ {
+ foreach (var testCase in cases.Values)
+ {
+ if (!sentTestResults.Contains(testCase.Id))
+ {
+ var testResult = GetOrCreateTestResult(testCase);
+ if (testResult.Outcome == TestOutcome.None)
+ testResult.Outcome = TestOutcome.Skipped;
+ RecordStart(testCase);
+ RecordEnd(testCase, testResult.Outcome);
+ RecordResult(testResult);
+ }
+ }
+ }
+
+ private TestResult GetOrCreateTestResult(TestCase testCase)
+ {
+ if (testResults.TryGetValue(testCase.Id, out var testResult))
+ return testResult;
+
+ var newResult = new TestResult(testCase)
+ {
+ ComputerName = Environment.MachineName,
+ DisplayName = testCase.DisplayName
+ };
+
+ testResults[testCase.Id] = newResult;
+ return newResult;
+ }
+
+ private void RecordStart(TestCase testCase)
+ {
+ recorder.RecordStart(SerializationHelpers.Serialize(testCase));
+ }
+
+ private void RecordEnd(TestCase testCase, TestOutcome testOutcome)
+ {
+ recorder.RecordEnd(SerializationHelpers.Serialize(testCase), testOutcome);
+ }
+
+ private void RecordResult(TestResult testResult)
+ {
+ recorder.RecordResult(SerializationHelpers.Serialize(testResult));
+ }
+ }
+}
diff --git a/src/BenchmarkDotNet.TestAdapter/VSTestLogger.cs b/src/BenchmarkDotNet.TestAdapter/VSTestLogger.cs
new file mode 100644
index 0000000000..c9a8de620f
--- /dev/null
+++ b/src/BenchmarkDotNet.TestAdapter/VSTestLogger.cs
@@ -0,0 +1,62 @@
+using BenchmarkDotNet.Loggers;
+using Microsoft.VisualStudio.TestPlatform.ObjectModel.Logging;
+using System.Text;
+
+namespace BenchmarkDotNet.TestAdapter
+{
+ ///
+ /// A class to send logs from BDN to the VSTest output log.
+ ///
+ internal sealed class VsTestLogger : ILogger
+ {
+ private readonly IMessageLogger messageLogger;
+ private readonly StringBuilder currentLine = new StringBuilder();
+ private TestMessageLevel currentLevel = TestMessageLevel.Informational;
+
+ public VsTestLogger(IMessageLogger logger)
+ {
+ messageLogger = logger;
+ }
+
+ public string Id => nameof(VsTestLogger);
+
+ public int Priority => 0;
+
+ public void Flush()
+ {
+ WriteLine();
+ }
+
+ public void Write(LogKind logKind, string text)
+ {
+ currentLine.Append(text);
+
+ // Assume that if the log kind is an error, that the whole line is treated as an error
+ // The level will be reset to Informational when WriteLine() is called.
+ currentLevel = logKind switch
+ {
+ LogKind.Error => TestMessageLevel.Error,
+ LogKind.Warning => TestMessageLevel.Warning,
+ _ => currentLevel
+ };
+ }
+
+ public void WriteLine()
+ {
+ // The VSTest logger throws an error on logging empty or whitespace strings, so skip them.
+ if (currentLine.Length == 0)
+ return;
+
+ messageLogger.SendMessage(currentLevel, currentLine.ToString());
+
+ currentLevel = TestMessageLevel.Informational;
+ currentLine.Clear();
+ }
+
+ public void WriteLine(LogKind logKind, string text)
+ {
+ Write(logKind, text);
+ WriteLine();
+ }
+ }
+}
diff --git a/src/BenchmarkDotNet.TestAdapter/VSTestProperties.cs b/src/BenchmarkDotNet.TestAdapter/VSTestProperties.cs
new file mode 100644
index 0000000000..6bcbdcf299
--- /dev/null
+++ b/src/BenchmarkDotNet.TestAdapter/VSTestProperties.cs
@@ -0,0 +1,22 @@
+using Microsoft.VisualStudio.TestPlatform.ObjectModel;
+
+namespace BenchmarkDotNet.TestAdapter
+{
+ ///
+ /// A class that contains all the custom properties that can be set on VSTest TestCase and TestResults.
+ /// Some of these properties are well known as they are also used by VSTest adapters for other test libraries.
+ ///
+ internal static class VsTestProperties
+ {
+ ///
+ /// A test property used for storing the test results so that they could be accessed
+ /// programmatically from a custom VSTest runner.
+ ///
+ internal static readonly TestProperty Measurement = TestProperty.Register(
+ "BenchmarkDotNet.TestAdapter.Measurements",
+ "Measurements",
+ typeof(string[]),
+ TestPropertyAttributes.Hidden,
+ typeof(TestResult));
+ }
+}
diff --git a/src/BenchmarkDotNet.TestAdapter/build/BenchmarkDotNet.TestAdapter.props b/src/BenchmarkDotNet.TestAdapter/build/BenchmarkDotNet.TestAdapter.props
new file mode 100644
index 0000000000..a9e8340b30
--- /dev/null
+++ b/src/BenchmarkDotNet.TestAdapter/build/BenchmarkDotNet.TestAdapter.props
@@ -0,0 +1,21 @@
+
+
+ $(MSBuildThisFileDirectory)..\entrypoints\
+
+
+
+
+
+
+
+
+
+
+ false
+
+
+
\ No newline at end of file
diff --git a/src/BenchmarkDotNet.TestAdapter/entrypoints/EntryPoint.cs b/src/BenchmarkDotNet.TestAdapter/entrypoints/EntryPoint.cs
new file mode 100644
index 0000000000..4f9036a00c
--- /dev/null
+++ b/src/BenchmarkDotNet.TestAdapter/entrypoints/EntryPoint.cs
@@ -0,0 +1,11 @@
+//
+// Exclude this file from StyleCop analysis. This file isn't generated but is added to projects.
+//
+
+using BenchmarkDotNet.Running;
+using System.Reflection;
+
+public class __AutoGeneratedEntryPointClass
+{
+ public static void Main(string[] args) => BenchmarkSwitcher.FromAssembly(typeof(__AutoGeneratedEntryPointClass).Assembly).Run(args);
+}
diff --git a/src/BenchmarkDotNet.TestAdapter/entrypoints/EntryPoint.fs b/src/BenchmarkDotNet.TestAdapter/entrypoints/EntryPoint.fs
new file mode 100644
index 0000000000..7e305320b8
--- /dev/null
+++ b/src/BenchmarkDotNet.TestAdapter/entrypoints/EntryPoint.fs
@@ -0,0 +1,13 @@
+//
+// Exclude this file from StyleCop analysis. This file isn't generated but is added to projects.
+//
+
+module __AutoGeneratedEntryPointClass
+open System.Reflection;
+open BenchmarkDotNet.Running
+
+type internal __Marker = interface end // Used to help locale current assembly
+[]
+let main argv =
+ BenchmarkSwitcher.FromAssembly(typeof<__Marker>.Assembly).Run(argv) |> ignore
+ 0 // return an integer exit code
diff --git a/src/BenchmarkDotNet.TestAdapter/entrypoints/EntryPoint.vb b/src/BenchmarkDotNet.TestAdapter/entrypoints/EntryPoint.vb
new file mode 100644
index 0000000000..24458cafca
--- /dev/null
+++ b/src/BenchmarkDotNet.TestAdapter/entrypoints/EntryPoint.vb
@@ -0,0 +1,14 @@
+REM
+REM Exclude this file from StyleCop analysis. This file isn't generated but is added to projects.
+REM
+
+Imports System.Reflection
+Imports BenchmarkDotNet.Running
+
+Namespace Global
+ Module __AutoGeneratedEntryPointClass
+ Sub Main(args As String())
+ Dim summary = BenchmarkSwitcher.FromAssembly(MethodBase.GetCurrentMethod().Module.Assembly).Run(args)
+ End Sub
+ End Module
+End Namespace
\ No newline at end of file
diff --git a/src/BenchmarkDotNet/Analysers/BaselineCustomAnalyzer.cs b/src/BenchmarkDotNet/Analysers/BaselineCustomAnalyzer.cs
index 51663e4085..9f2ff0028c 100644
--- a/src/BenchmarkDotNet/Analysers/BaselineCustomAnalyzer.cs
+++ b/src/BenchmarkDotNet/Analysers/BaselineCustomAnalyzer.cs
@@ -25,13 +25,14 @@ protected override IEnumerable AnalyseSummary(Summary summary)
foreach (var benchmarkCase in summary.BenchmarksCases)
{
- string logicalGroupKey = summary.GetLogicalGroupKey(benchmarkCase);
+ string? logicalGroupKey = summary.GetLogicalGroupKey(benchmarkCase);
var baseline = summary.GetBaseline(logicalGroupKey);
if (BaselineCustomColumn.ResultsAreInvalid(summary, benchmarkCase, baseline) == false)
continue;
var message = "A question mark '?' symbol indicates that it was not possible to compute the " +
- $"({columnNames}) column(s) because the baseline value is too close to zero.";
+ $"({columnNames}) column(s) because the baseline or benchmark could not be found, or " +
+ $"the baseline value is too close to zero.";
yield return Conclusion.CreateWarning(Id, message);
}
diff --git a/src/BenchmarkDotNet/Analysers/ConclusionHelper.cs b/src/BenchmarkDotNet/Analysers/ConclusionHelper.cs
index 9542230606..af872414f2 100644
--- a/src/BenchmarkDotNet/Analysers/ConclusionHelper.cs
+++ b/src/BenchmarkDotNet/Analysers/ConclusionHelper.cs
@@ -10,7 +10,7 @@ public static class ConclusionHelper
public static void Print(ILogger logger, IEnumerable conclusions)
{
PrintFiltered(conclusions, ConclusionKind.Error, "Errors", logger.WriteLineError);
- PrintFiltered(conclusions, ConclusionKind.Warning, "Warnings", logger.WriteLineError);
+ PrintFiltered(conclusions, ConclusionKind.Warning, "Warnings", logger.WriteLineWarning);
PrintFiltered(conclusions, ConclusionKind.Hint, "Hints", logger.WriteLineHint);
}
diff --git a/src/BenchmarkDotNet/Analysers/MultimodalDistributionAnalyzer.cs b/src/BenchmarkDotNet/Analysers/MultimodalDistributionAnalyzer.cs
index 151ac70be2..e1e4465ef6 100644
--- a/src/BenchmarkDotNet/Analysers/MultimodalDistributionAnalyzer.cs
+++ b/src/BenchmarkDotNet/Analysers/MultimodalDistributionAnalyzer.cs
@@ -20,7 +20,7 @@ protected override IEnumerable AnalyseReport(BenchmarkReport report,
if (statistics == null || statistics.N < EngineResolver.DefaultMinWorkloadIterationCount)
yield break;
- double mValue = MValueCalculator.Calculate(statistics.OriginalValues);
+ double mValue = MValueCalculator.Calculate(statistics.Sample.Values);
if (mValue > 4.2)
yield return Create("is multimodal", mValue, report, summary.GetCultureInfo());
else if (mValue > 3.2)
diff --git a/src/BenchmarkDotNet/Analysers/OutliersAnalyser.cs b/src/BenchmarkDotNet/Analysers/OutliersAnalyser.cs
index 6da2088e86..570ea7806e 100644
--- a/src/BenchmarkDotNet/Analysers/OutliersAnalyser.cs
+++ b/src/BenchmarkDotNet/Analysers/OutliersAnalyser.cs
@@ -3,6 +3,7 @@
using System.Linq;
using BenchmarkDotNet.Engines;
using BenchmarkDotNet.Extensions;
+using BenchmarkDotNet.Helpers;
using BenchmarkDotNet.Jobs;
using BenchmarkDotNet.Reports;
using JetBrains.Annotations;
@@ -22,25 +23,11 @@ protected override IEnumerable AnalyseReport(BenchmarkReport report,
var workloadActual = report.AllMeasurements.Where(m => m.Is(IterationMode.Workload, IterationStage.Actual)).ToArray();
if (workloadActual.IsEmpty())
yield break;
- var result = report.AllMeasurements.Where(m => m.Is(IterationMode.Workload, IterationStage.Result)).ToArray();
var outlierMode = report.BenchmarkCase.Job.ResolveValue(AccuracyMode.OutlierModeCharacteristic, EngineResolver.Instance); // TODO: improve
var statistics = workloadActual.GetStatistics();
var allOutliers = statistics.AllOutliers;
var actualOutliers = statistics.GetActualOutliers(outlierMode);
- if (result.Length + actualOutliers.Length != workloadActual.Length)
- {
- // This should never happen
- yield return CreateHint(
- "Something went wrong with outliers: " +
- $"Size(WorkloadActual) = {workloadActual.Length}, " +
- $"Size(WorkloadActual/Outliers) = {actualOutliers.Length}, " +
- $"Size(Result) = {result.Length}), " +
- $"OutlierMode = {outlierMode}",
- report);
- yield break;
- }
-
var cultureInfo = summary.GetCultureInfo();
if (allOutliers.Any())
yield return CreateHint(GetMessage(actualOutliers, allOutliers, statistics.LowerOutliers, statistics.UpperOutliers, cultureInfo), report);
@@ -67,7 +54,7 @@ string Format(int n, string verb)
return $"{n} {words} {verb}";
}
- var rangeMessages = new List { GetRangeMessage(lowerOutliers, cultureInfo), GetRangeMessage(upperOutliers, cultureInfo) };
+ var rangeMessages = new List { GetRangeMessage(lowerOutliers), GetRangeMessage(upperOutliers) };
rangeMessages.RemoveAll(string.IsNullOrEmpty);
string rangeMessage = rangeMessages.Any()
? " (" + string.Join(", ", rangeMessages) + ")"
@@ -80,20 +67,17 @@ string Format(int n, string verb)
return Format(actualOutliers.Length, "removed") + ", " + Format(allOutliers.Length, "detected") + rangeMessage;
}
- private static string? GetRangeMessage(double[] values, CultureInfo cultureInfo)
+ private static string? GetRangeMessage(double[] values)
{
- string Format(double value) => TimeInterval.FromNanoseconds(value).ToString(cultureInfo, "N2");
+ string Format(double value) => TimeInterval.FromNanoseconds(value).ToDefaultString("N2");
- switch (values.Length) {
- case 0:
- return null;
- case 1:
- return Format(values.First());
- case 2:
- return Format(values.Min()) + ", " + Format(values.Max());
- default:
- return Format(values.Min()) + ".." + Format(values.Max());
- }
+ return values.Length switch
+ {
+ 0 => null,
+ 1 => Format(values.First()),
+ 2 => Format(values.Min()) + ", " + Format(values.Max()),
+ _ => Format(values.Min()) + ".." + Format(values.Max())
+ };
}
}
}
\ No newline at end of file
diff --git a/src/BenchmarkDotNet/Analysers/ZeroMeasurementAnalyser.cs b/src/BenchmarkDotNet/Analysers/ZeroMeasurementAnalyser.cs
index 240f2e8475..4d83a5d154 100644
--- a/src/BenchmarkDotNet/Analysers/ZeroMeasurementAnalyser.cs
+++ b/src/BenchmarkDotNet/Analysers/ZeroMeasurementAnalyser.cs
@@ -19,7 +19,7 @@ private ZeroMeasurementAnalyser() { }
protected override IEnumerable AnalyseReport(BenchmarkReport report, Summary summary)
{
- var currentFrequency = summary.HostEnvironmentInfo.CpuInfo.Value.MaxFrequency;
+ var currentFrequency = summary.HostEnvironmentInfo.Cpu.Value.MaxFrequency();
if (!currentFrequency.HasValue || currentFrequency <= 0)
currentFrequency = FallbackCpuResolutionValue.ToFrequency();
@@ -28,17 +28,17 @@ protected override IEnumerable AnalyseReport(BenchmarkReport report,
var workloadMeasurements = entire.Where(m => m.Is(IterationMode.Workload, IterationStage.Actual)).ToArray();
if (workloadMeasurements.IsEmpty())
yield break;
- var workload = workloadMeasurements.GetStatistics();
+ var workloadSample = workloadMeasurements.GetStatistics().Sample;
var threshold = currentFrequency.Value.ToResolution().Nanoseconds / 2;
var zeroMeasurement = overheadMeasurements.Any()
- ? ZeroMeasurementHelper.CheckZeroMeasurementTwoSamples(workload.WithoutOutliers(), overheadMeasurements.GetStatistics().WithoutOutliers())
- : ZeroMeasurementHelper.CheckZeroMeasurementOneSample(workload.WithoutOutliers(), threshold);
+ ? ZeroMeasurementHelper.AreIndistinguishable(workloadSample, overheadMeasurements.GetStatistics().Sample)
+ : ZeroMeasurementHelper.IsNegligible(workloadSample, threshold);
if (zeroMeasurement)
yield return CreateWarning("The method duration is indistinguishable from the empty method duration",
- report, false);
+ report, false);
}
}
}
\ No newline at end of file
diff --git a/src/BenchmarkDotNet/Analysers/ZeroMeasurementHelper.cs b/src/BenchmarkDotNet/Analysers/ZeroMeasurementHelper.cs
index 5e3f650167..9079bc54d9 100644
--- a/src/BenchmarkDotNet/Analysers/ZeroMeasurementHelper.cs
+++ b/src/BenchmarkDotNet/Analysers/ZeroMeasurementHelper.cs
@@ -1,30 +1,39 @@
+using BenchmarkDotNet.Mathematics;
+using Perfolizer;
+using Perfolizer.Horology;
+using Perfolizer.Mathematics.Common;
+using Perfolizer.Mathematics.GenericEstimators;
using Perfolizer.Mathematics.SignificanceTesting;
-using Perfolizer.Mathematics.Thresholds;
+using Perfolizer.Mathematics.SignificanceTesting.MannWhitney;
+using Perfolizer.Metrology;
namespace BenchmarkDotNet.Analysers
{
- public static class ZeroMeasurementHelper
+ internal static class ZeroMeasurementHelper
{
- ///
- /// Checks distribution against Zero Measurement hypothesis in case of known threshold
- ///
- /// True if measurement is ZeroMeasurement
- public static bool CheckZeroMeasurementOneSample(double[] results, double threshold)
+ public static bool IsNegligible(Sample results, double threshold) => HodgesLehmannEstimator.Instance.Median(results) < threshold;
+ public static bool IsNoticeable(Sample results, double threshold) => !IsNegligible(results, threshold);
+
+ public static bool AreIndistinguishable(double[] workload, double[] overhead, Threshold? threshold = null)
{
- if (results.Length < 3)
- return false;
- return !StudentTest.Instance.IsGreater(results, threshold).NullHypothesisIsRejected;
+ var workloadSample = new Sample(workload, TimeUnit.Nanosecond);
+ var overheadSample = new Sample(overhead, TimeUnit.Nanosecond);
+ return AreIndistinguishable(workloadSample, overheadSample, threshold);
}
- ///
- /// Checks distribution against Zero Measurement hypothesis in case of two samples
- ///
- /// True if measurement is ZeroMeasurement
- public static bool CheckZeroMeasurementTwoSamples(double[] workload, double[] overhead, Threshold? threshold = null)
+ public static bool AreIndistinguishable(Sample workload, Sample overhead, Threshold? threshold = null)
{
- if (workload.Length < 3 || overhead.Length < 3)
+ threshold ??= MathHelper.DefaultThreshold;
+ var tost = new SimpleEquivalenceTest(MannWhitneyTest.Instance);
+ if (workload.Size == 1 || overhead.Size == 1)
return false;
- return !WelchTest.Instance.IsGreater(workload, overhead, threshold).NullHypothesisIsRejected;
+ return tost.Perform(workload, overhead, threshold, SignificanceLevel.P1E5) == ComparisonResult.Indistinguishable;
}
+
+ public static bool AreDistinguishable(double[] workload, double[] overhead, Threshold? threshold = null) =>
+ !AreIndistinguishable(workload, overhead, threshold);
+
+ public static bool AreDistinguishable(Sample workload, Sample overhead, Threshold? threshold = null) =>
+ !AreIndistinguishable(workload, overhead, threshold);
}
}
\ No newline at end of file
diff --git a/src/BenchmarkDotNet/Attributes/Columns/ConfidenceIntervalErrorColumnAttribute.cs b/src/BenchmarkDotNet/Attributes/Columns/ConfidenceIntervalErrorColumnAttribute.cs
index d0d1f90faf..b1b87d5241 100644
--- a/src/BenchmarkDotNet/Attributes/Columns/ConfidenceIntervalErrorColumnAttribute.cs
+++ b/src/BenchmarkDotNet/Attributes/Columns/ConfidenceIntervalErrorColumnAttribute.cs
@@ -7,8 +7,7 @@ namespace BenchmarkDotNet.Attributes
[PublicAPI]
public class ConfidenceIntervalErrorColumnAttribute : ColumnConfigBaseAttribute
{
- public ConfidenceIntervalErrorColumnAttribute(ConfidenceLevel level = ConfidenceLevel.L999) : base(StatisticColumn.CiError(level))
- {
- }
+ public ConfidenceIntervalErrorColumnAttribute() : base(StatisticColumn.CiError(ConfidenceLevel.L999)) { }
+ public ConfidenceIntervalErrorColumnAttribute(ConfidenceLevel level) : base(StatisticColumn.CiError(level)) { }
}
}
\ No newline at end of file
diff --git a/src/BenchmarkDotNet/Attributes/Columns/OperationsPerSecondAttribute.cs b/src/BenchmarkDotNet/Attributes/Columns/OperationsPerSecondAttribute.cs
new file mode 100644
index 0000000000..6315dc3694
--- /dev/null
+++ b/src/BenchmarkDotNet/Attributes/Columns/OperationsPerSecondAttribute.cs
@@ -0,0 +1,10 @@
+using BenchmarkDotNet.Columns;
+using JetBrains.Annotations;
+
+namespace BenchmarkDotNet.Attributes
+{
+ public class OperationsPerSecondAttribute : ColumnConfigBaseAttribute
+ {
+ public OperationsPerSecondAttribute() : base(StatisticColumn.OperationsPerSecond) { }
+ }
+}
diff --git a/src/BenchmarkDotNet/Attributes/Columns/StdErrorColumnAttribute.cs b/src/BenchmarkDotNet/Attributes/Columns/StdErrorColumnAttribute.cs
index 27a22b1e1a..23327e187e 100644
--- a/src/BenchmarkDotNet/Attributes/Columns/StdErrorColumnAttribute.cs
+++ b/src/BenchmarkDotNet/Attributes/Columns/StdErrorColumnAttribute.cs
@@ -4,10 +4,5 @@
namespace BenchmarkDotNet.Attributes
{
[PublicAPI]
- public class StdErrorColumnAttribute : ColumnConfigBaseAttribute
- {
- public StdErrorColumnAttribute() : base(StatisticColumn.StdErr)
- {
- }
- }
+ public class StdErrorColumnAttribute() : ColumnConfigBaseAttribute(StatisticColumn.StdErr) { }
}
\ No newline at end of file
diff --git a/src/BenchmarkDotNet/Attributes/Columns/WelchTTestPValueColumnAttribute.cs b/src/BenchmarkDotNet/Attributes/Columns/WelchTTestPValueColumnAttribute.cs
index 797ab57a9d..607f2c4fe2 100644
--- a/src/BenchmarkDotNet/Attributes/Columns/WelchTTestPValueColumnAttribute.cs
+++ b/src/BenchmarkDotNet/Attributes/Columns/WelchTTestPValueColumnAttribute.cs
@@ -1,8 +1,7 @@
using System;
using BenchmarkDotNet.Columns;
using JetBrains.Annotations;
-using Perfolizer.Mathematics.SignificanceTesting;
-using Perfolizer.Mathematics.Thresholds;
+using Perfolizer.Mathematics.Common;
namespace BenchmarkDotNet.Attributes
{
@@ -10,17 +9,11 @@ namespace BenchmarkDotNet.Attributes
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Assembly, AllowMultiple = true)]
public class StatisticalTestColumnAttribute : ColumnConfigBaseAttribute
{
- public StatisticalTestColumnAttribute(StatisticalTestKind testKind, ThresholdUnit thresholdUnit, double value, bool showPValues = false)
- : base(StatisticalTestColumn.Create(testKind, Threshold.Create(thresholdUnit, value), showPValues)) { }
+ public StatisticalTestColumnAttribute() : base(StatisticalTestColumn.Create("10%", null)) { }
- public StatisticalTestColumnAttribute(StatisticalTestKind testKind, bool showPValues = false) : this(testKind, ThresholdUnit.Ratio, 0.1, showPValues) { }
+ public StatisticalTestColumnAttribute(string threshold) : base(StatisticalTestColumn.Create(threshold, null)) { }
- public StatisticalTestColumnAttribute(bool showPValues = false) : this(StatisticalTestKind.MannWhitney, showPValues) {}
- }
-
- [Obsolete("Use StatisticalTestAttribute")]
- public class WelchTTestPValueColumnAttribute : StatisticalTestColumnAttribute
- {
- public WelchTTestPValueColumnAttribute() : base(StatisticalTestKind.Welch) { }
+ public StatisticalTestColumnAttribute(string threshold, SignificanceLevel significanceLevel)
+ : base(StatisticalTestColumn.Create(threshold, significanceLevel)) { }
}
}
\ No newline at end of file
diff --git a/src/BenchmarkDotNet/Attributes/ExceptionDiagnoserAttribute.cs b/src/BenchmarkDotNet/Attributes/ExceptionDiagnoserAttribute.cs
index bff956e968..21f2903124 100644
--- a/src/BenchmarkDotNet/Attributes/ExceptionDiagnoserAttribute.cs
+++ b/src/BenchmarkDotNet/Attributes/ExceptionDiagnoserAttribute.cs
@@ -9,6 +9,10 @@ public class ExceptionDiagnoserAttribute : Attribute, IConfigSource
{
public IConfig Config { get; }
- public ExceptionDiagnoserAttribute() => Config = ManualConfig.CreateEmpty().AddDiagnoser(ExceptionDiagnoser.Default);
+ /// Display Exceptions column. True by default.
+ public ExceptionDiagnoserAttribute(bool displayExceptionsIfZeroValue = true)
+ {
+ Config = ManualConfig.CreateEmpty().AddDiagnoser(new ExceptionDiagnoser(new ExceptionDiagnoserConfig(displayExceptionsIfZeroValue)));
+ }
}
}
diff --git a/src/BenchmarkDotNet/Attributes/ExceptionDiagnoserConfig.cs b/src/BenchmarkDotNet/Attributes/ExceptionDiagnoserConfig.cs
new file mode 100644
index 0000000000..86f8d99be4
--- /dev/null
+++ b/src/BenchmarkDotNet/Attributes/ExceptionDiagnoserConfig.cs
@@ -0,0 +1,20 @@
+using JetBrains.Annotations;
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace BenchmarkDotNet.Attributes
+{
+ public class ExceptionDiagnoserConfig
+ {
+ /// Determines whether the Exceptions column is displayed when its value is not calculated. True by default.
+
+ [PublicAPI]
+ public ExceptionDiagnoserConfig(bool displayExceptionsIfZeroValue = true)
+ {
+ DisplayExceptionsIfZeroValue = displayExceptionsIfZeroValue;
+ }
+
+ public bool DisplayExceptionsIfZeroValue { get; }
+ }
+}
diff --git a/src/BenchmarkDotNet/Attributes/Exporters/PhdExporterAttribute.cs b/src/BenchmarkDotNet/Attributes/Exporters/PhdExporterAttribute.cs
new file mode 100644
index 0000000000..9169c82084
--- /dev/null
+++ b/src/BenchmarkDotNet/Attributes/Exporters/PhdExporterAttribute.cs
@@ -0,0 +1,10 @@
+using System;
+using BenchmarkDotNet.Exporters;
+
+namespace BenchmarkDotNet.Attributes;
+
+///
+/// IMPORTANT: Not fully implemented yet
+///
+[AttributeUsage(AttributeTargets.Class | AttributeTargets.Assembly, AllowMultiple = true)]
+public class PhdExporterAttribute() : ExporterConfigBaseAttribute(new PhdJsonExporter(), new PhdMdExporter());
\ No newline at end of file
diff --git a/src/BenchmarkDotNet/Attributes/ThreadingDiagnoserAttribute.cs b/src/BenchmarkDotNet/Attributes/ThreadingDiagnoserAttribute.cs
index 7627170b8b..4ad7651bfc 100644
--- a/src/BenchmarkDotNet/Attributes/ThreadingDiagnoserAttribute.cs
+++ b/src/BenchmarkDotNet/Attributes/ThreadingDiagnoserAttribute.cs
@@ -1,6 +1,7 @@
using System;
using BenchmarkDotNet.Configs;
using BenchmarkDotNet.Diagnosers;
+using JetBrains.Annotations;
namespace BenchmarkDotNet.Attributes
{
@@ -9,6 +10,15 @@ public class ThreadingDiagnoserAttribute : Attribute, IConfigSource
{
public IConfig Config { get; }
- public ThreadingDiagnoserAttribute() => Config = ManualConfig.CreateEmpty().AddDiagnoser(ThreadingDiagnoser.Default);
+ //public ThreadingDiagnoserAttribute() => Config = ManualConfig.CreateEmpty().AddDiagnoser(ThreadingDiagnoser.Default);
+
+ /// Display configuration for 'LockContentionCount' when it is empty. True (displayed) by default.
+ /// Display configuration for 'CompletedWorkItemCount' when it is empty. True (displayed) by default.
+
+ [PublicAPI]
+ public ThreadingDiagnoserAttribute(bool displayLockContentionWhenZero = true, bool displayCompletedWorkItemCountWhenZero = true)
+ {
+ Config = ManualConfig.CreateEmpty().AddDiagnoser(new ThreadingDiagnoser(new ThreadingDiagnoserConfig(displayLockContentionWhenZero, displayCompletedWorkItemCountWhenZero)));
+ }
}
}
\ No newline at end of file
diff --git a/src/BenchmarkDotNet/Attributes/WakeLockAttribute.cs b/src/BenchmarkDotNet/Attributes/WakeLockAttribute.cs
new file mode 100644
index 0000000000..1243ac8f8f
--- /dev/null
+++ b/src/BenchmarkDotNet/Attributes/WakeLockAttribute.cs
@@ -0,0 +1,18 @@
+using BenchmarkDotNet.Configs;
+using System;
+
+namespace BenchmarkDotNet.Attributes
+{
+ ///
+ /// Placing a on your assembly or class controls whether the
+ /// Windows system enters sleep or turns off the display while benchmarks run.
+ ///
+ [AttributeUsage(AttributeTargets.Assembly | AttributeTargets.Class)]
+ public sealed class WakeLockAttribute : Attribute, IConfigSource
+ {
+ public WakeLockAttribute(WakeLockType wakeLockType) =>
+ Config = ManualConfig.CreateEmpty().WithWakeLock(wakeLockType);
+
+ public IConfig Config { get; }
+ }
+}
diff --git a/src/BenchmarkDotNet/BenchmarkDotNet.csproj b/src/BenchmarkDotNet/BenchmarkDotNet.csproj
index 50e6733592..1b72486e30 100644
--- a/src/BenchmarkDotNet/BenchmarkDotNet.csproj
+++ b/src/BenchmarkDotNet/BenchmarkDotNet.csproj
@@ -2,7 +2,7 @@
BenchmarkDotNet
- netstandard2.0;net6.0
+ netstandard2.0;net6.0;net8.0
true
$(NoWarn);1701;1702;1705;1591;3005;NU1702;CS3001;CS3003
BenchmarkDotNet
@@ -13,19 +13,17 @@
-
-
-
-
+
+
+
-
-
-
+
+
diff --git a/src/BenchmarkDotNet/Characteristics/Characteristic.cs b/src/BenchmarkDotNet/Characteristics/Characteristic.cs
index 6ca88f5f75..d6e5815b4e 100644
--- a/src/BenchmarkDotNet/Characteristics/Characteristic.cs
+++ b/src/BenchmarkDotNet/Characteristics/Characteristic.cs
@@ -86,7 +86,7 @@ protected Characteristic(
private object FallbackValue { get; }
- public object this[CharacteristicObject obj]
+ public object? this[CharacteristicObject obj]
{
get { return obj.GetValue(this); }
set { obj.SetValue(this, value); }
diff --git a/src/BenchmarkDotNet/Characteristics/CharacteristicObject.cs b/src/BenchmarkDotNet/Characteristics/CharacteristicObject.cs
index a6d7b37b81..6cb4a983c3 100644
--- a/src/BenchmarkDotNet/Characteristics/CharacteristicObject.cs
+++ b/src/BenchmarkDotNet/Characteristics/CharacteristicObject.cs
@@ -402,6 +402,12 @@ protected CharacteristicObject UnfreezeCopyCore()
var newRoot = (CharacteristicObject)Activator.CreateInstance(GetType());
newRoot.ApplyCore(this);
+ // Preserve the IdCharacteristic of the original object
+ if (this.HasValue(IdCharacteristic))
+ {
+ newRoot.SetValue(IdCharacteristic, this.GetValue(IdCharacteristic));
+ }
+
return newRoot;
}
#endregion
diff --git a/src/BenchmarkDotNet/Code/CodeGenerator.cs b/src/BenchmarkDotNet/Code/CodeGenerator.cs
index 005564b77c..9a6228dd88 100644
--- a/src/BenchmarkDotNet/Code/CodeGenerator.cs
+++ b/src/BenchmarkDotNet/Code/CodeGenerator.cs
@@ -56,6 +56,7 @@ internal static string Generate(BuildPartition buildPartition)
.Replace("$OverheadImplementation$", provider.OverheadImplementation)
.Replace("$ConsumeField$", provider.ConsumeField)
.Replace("$JobSetDefinition$", GetJobsSetDefinition(benchmark))
+ .Replace("$ParamsInitializer$", GetParamsInitializer(benchmark))
.Replace("$ParamsContent$", GetParamsContent(benchmark))
.Replace("$ArgumentsDefinition$", GetArgumentsDefinition(benchmark))
.Replace("$DeclareArgumentFields$", GetDeclareArgumentFields(benchmark))
@@ -186,7 +187,15 @@ private static DeclarationsProvider GetDeclarationsProvider(Descriptor descripto
return new NonVoidDeclarationsProvider(descriptor);
}
+ private static string GetParamsInitializer(BenchmarkCase benchmarkCase)
+ => string.Join(
+ ", ",
+ benchmarkCase.Parameters.Items
+ .Where(parameter => !parameter.IsArgument && !parameter.IsStatic)
+ .Select(parameter => $"{parameter.Name} = default"));
+
// internal for tests
+
internal static string GetParamsContent(BenchmarkCase benchmarkCase)
=> string.Join(
string.Empty,
diff --git a/src/BenchmarkDotNet/Code/DeclarationsProvider.cs b/src/BenchmarkDotNet/Code/DeclarationsProvider.cs
index ddf78eb572..7528e8ed62 100644
--- a/src/BenchmarkDotNet/Code/DeclarationsProvider.cs
+++ b/src/BenchmarkDotNet/Code/DeclarationsProvider.cs
@@ -63,7 +63,7 @@ private string GetMethodName(MethodInfo method)
(method.ReturnType.GetGenericTypeDefinition() == typeof(Task<>) ||
method.ReturnType.GetGenericTypeDefinition() == typeof(ValueTask<>))))
{
- return $"() => {method.Name}().GetAwaiter().GetResult()";
+ return $"() => BenchmarkDotNet.Helpers.AwaitHelper.GetResult({method.Name}())";
}
return method.Name;
@@ -149,12 +149,10 @@ internal class TaskDeclarationsProvider : VoidDeclarationsProvider
{
public TaskDeclarationsProvider(Descriptor descriptor) : base(descriptor) { }
- // we use GetAwaiter().GetResult() because it's fastest way to obtain the result in blocking way,
- // and will eventually throw actual exception, not aggregated one
public override string WorkloadMethodDelegate(string passArguments)
- => $"({passArguments}) => {{ {Descriptor.WorkloadMethod.Name}({passArguments}).GetAwaiter().GetResult(); }}";
+ => $"({passArguments}) => {{ BenchmarkDotNet.Helpers.AwaitHelper.GetResult({Descriptor.WorkloadMethod.Name}({passArguments})); }}";
- public override string GetWorkloadMethodCall(string passArguments) => $"{Descriptor.WorkloadMethod.Name}({passArguments}).GetAwaiter().GetResult()";
+ public override string GetWorkloadMethodCall(string passArguments) => $"BenchmarkDotNet.Helpers.AwaitHelper.GetResult({Descriptor.WorkloadMethod.Name}({passArguments}))";
protected override Type WorkloadMethodReturnType => typeof(void);
}
@@ -168,11 +166,9 @@ public GenericTaskDeclarationsProvider(Descriptor descriptor) : base(descriptor)
protected override Type WorkloadMethodReturnType => Descriptor.WorkloadMethod.ReturnType.GetTypeInfo().GetGenericArguments().Single();
- // we use GetAwaiter().GetResult() because it's fastest way to obtain the result in blocking way,
- // and will eventually throw actual exception, not aggregated one
public override string WorkloadMethodDelegate(string passArguments)
- => $"({passArguments}) => {{ return {Descriptor.WorkloadMethod.Name}({passArguments}).GetAwaiter().GetResult(); }}";
+ => $"({passArguments}) => {{ return BenchmarkDotNet.Helpers.AwaitHelper.GetResult({Descriptor.WorkloadMethod.Name}({passArguments})); }}";
- public override string GetWorkloadMethodCall(string passArguments) => $"{Descriptor.WorkloadMethod.Name}({passArguments}).GetAwaiter().GetResult()";
+ public override string GetWorkloadMethodCall(string passArguments) => $"BenchmarkDotNet.Helpers.AwaitHelper.GetResult({Descriptor.WorkloadMethod.Name}({passArguments}))";
}
}
\ No newline at end of file
diff --git a/src/BenchmarkDotNet/Code/EnumParam.cs b/src/BenchmarkDotNet/Code/EnumParam.cs
index 9d0ac58950..5c0ecf6afd 100644
--- a/src/BenchmarkDotNet/Code/EnumParam.cs
+++ b/src/BenchmarkDotNet/Code/EnumParam.cs
@@ -24,7 +24,7 @@ private EnumParam(object value, Type type)
public string ToSourceCode() =>
$"({type.GetCorrectCSharpTypeName()})({ToInvariantCultureString()})";
- internal static IParam FromObject(object value, Type type = null)
+ internal static IParam FromObject(object value, Type? type = null)
{
type = type ?? value.GetType();
if (!type.IsEnum)
diff --git a/src/BenchmarkDotNet/Columns/BaselineCustomColumn.cs b/src/BenchmarkDotNet/Columns/BaselineCustomColumn.cs
index b808d988d8..4dabe8e502 100644
--- a/src/BenchmarkDotNet/Columns/BaselineCustomColumn.cs
+++ b/src/BenchmarkDotNet/Columns/BaselineCustomColumn.cs
@@ -43,7 +43,7 @@ public abstract string GetValue(Summary summary, BenchmarkCase benchmarkCase, St
public override string ToString() => ColumnName;
public bool IsDefault(Summary summary, BenchmarkCase benchmarkCase) => false;
- internal static bool ResultsAreInvalid(Summary summary, BenchmarkCase benchmarkCase, BenchmarkCase baseline)
+ internal static bool ResultsAreInvalid(Summary summary, BenchmarkCase benchmarkCase, BenchmarkCase? baseline)
{
return baseline == null ||
summary[baseline] == null ||
diff --git a/src/BenchmarkDotNet/Columns/DefaultColumnProvider.cs b/src/BenchmarkDotNet/Columns/DefaultColumnProvider.cs
index 0677f73c77..60bb629ee8 100644
--- a/src/BenchmarkDotNet/Columns/DefaultColumnProvider.cs
+++ b/src/BenchmarkDotNet/Columns/DefaultColumnProvider.cs
@@ -46,7 +46,7 @@ public IEnumerable GetColumns(Summary summary)
if (NeedToShow(summary, s => s.Percentiles.P95 > s.Mean + 3 * s.StandardDeviation))
yield return StatisticColumn.P95;
if (NeedToShow(summary, s => s.N >= 3 &&
- (!s.GetConfidenceInterval(ConfidenceLevel.L99, s.N).Contains(s.Median) ||
+ (!s.GetConfidenceInterval(ConfidenceLevel.L99).Contains(s.Median) ||
Math.Abs(s.Median - s.Mean) > s.Mean * 0.2)))
yield return StatisticColumn.Median;
if (NeedToShow(summary, s => s.StandardDeviation > 1e-9))
diff --git a/src/BenchmarkDotNet/Columns/MetricColumn.cs b/src/BenchmarkDotNet/Columns/MetricColumn.cs
index 645891502a..feb615bfb0 100644
--- a/src/BenchmarkDotNet/Columns/MetricColumn.cs
+++ b/src/BenchmarkDotNet/Columns/MetricColumn.cs
@@ -1,8 +1,9 @@
using System.Linq;
+using BenchmarkDotNet.Extensions;
using BenchmarkDotNet.Reports;
using BenchmarkDotNet.Running;
-using Perfolizer.Common;
using Perfolizer.Horology;
+using Perfolizer.Metrology;
namespace BenchmarkDotNet.Columns
{
@@ -26,8 +27,8 @@ public class MetricColumn : IColumn
public bool IsDefault(Summary summary, BenchmarkCase benchmarkCase) => false;
public bool IsAvailable(Summary summary) => summary.Reports.Any(report =>
- report.Metrics.TryGetValue(descriptor.Id, out var metric)
- && metric.Descriptor.GetIsAvailable(metric));
+ report.Metrics.TryGetValue(descriptor.Id, out var metric)
+ && metric.Descriptor.GetIsAvailable(metric));
public string GetValue(Summary summary, BenchmarkCase benchmarkCase) => GetValue(summary, benchmarkCase, SummaryStyle.Default);
@@ -43,18 +44,23 @@ public string GetValue(Summary summary, BenchmarkCase benchmarkCase, SummaryStyl
var cultureInfo = summary.GetCultureInfo();
bool printUnits = style.PrintUnitsInContent || style.PrintUnitsInHeader;
- UnitPresentation unitPresentation = UnitPresentation.FromVisibility(style.PrintUnitsInContent);
+ var unitPresentation = new UnitPresentation(style.PrintUnitsInContent, minUnitWidth: 0, gap: true);
+ string numberFormat = descriptor.NumberFormat;
if (printUnits && descriptor.UnitType == UnitType.CodeSize)
- return SizeValue.FromBytes((long) metric.Value).ToString(style.CodeSizeUnit, cultureInfo, descriptor.NumberFormat, unitPresentation);
+ return SizeValue.FromBytes((long)metric.Value).ToString(style.CodeSizeUnit, numberFormat, cultureInfo, unitPresentation);
if (printUnits && descriptor.UnitType == UnitType.Size)
- return SizeValue.FromBytes((long) metric.Value).ToString(style.SizeUnit, cultureInfo, descriptor.NumberFormat, unitPresentation);
+ return SizeValue.FromBytes((long)metric.Value).ToString(style.SizeUnit, numberFormat, cultureInfo, unitPresentation);
if (printUnits && descriptor.UnitType == UnitType.Time)
- return TimeInterval.FromNanoseconds(metric.Value).ToString(style.TimeUnit, cultureInfo, descriptor.NumberFormat, unitPresentation);
+ {
+ if (numberFormat.IsBlank())
+ numberFormat = "N4";
+ return TimeInterval.FromNanoseconds(metric.Value).ToString(style.TimeUnit, numberFormat, cultureInfo, unitPresentation);
+ }
- return metric.Value.ToString(descriptor.NumberFormat, cultureInfo);
+ return metric.Value.ToString(numberFormat, cultureInfo);
}
public override string ToString() => descriptor.DisplayName;
}
-}
+}
\ No newline at end of file
diff --git a/src/BenchmarkDotNet/Columns/SizeUnit.cs b/src/BenchmarkDotNet/Columns/SizeUnit.cs
deleted file mode 100644
index b919fb7fa0..0000000000
--- a/src/BenchmarkDotNet/Columns/SizeUnit.cs
+++ /dev/null
@@ -1,75 +0,0 @@
-using System;
-using System.Diagnostics.CodeAnalysis;
-using System.Linq;
-using JetBrains.Annotations;
-
-namespace BenchmarkDotNet.Columns
-{
- [SuppressMessage("ReSharper", "InconsistentNaming")] // We want to use "KB", "MB", "GB", "TB"
- public class SizeUnit : IEquatable
- {
- [PublicAPI] public string Name { get; }
- [PublicAPI] public string Description { get; }
- [PublicAPI] public long ByteAmount { get; }
-
- public SizeUnit(string name, string description, long byteAmount)
- {
- Name = name;
- Description = description;
- ByteAmount = byteAmount;
- }
-
- private const long BytesInKiloByte = 1024L; // this value MUST NOT be changed
-
- public SizeValue ToValue(long value = 1) => new SizeValue(value, this);
-
- [PublicAPI] public static readonly SizeUnit B = new SizeUnit("B", "Byte", 1L);
- [PublicAPI] public static readonly SizeUnit KB = new SizeUnit("KB", "Kilobyte", BytesInKiloByte);
- [PublicAPI] public static readonly SizeUnit MB = new SizeUnit("MB", "Megabyte", BytesInKiloByte * BytesInKiloByte);
- [PublicAPI] public static readonly SizeUnit GB = new SizeUnit("GB", "Gigabyte", BytesInKiloByte * BytesInKiloByte * BytesInKiloByte);
- [PublicAPI] public static readonly SizeUnit TB = new SizeUnit("TB", "Terabyte", BytesInKiloByte * BytesInKiloByte * BytesInKiloByte * BytesInKiloByte);
- [PublicAPI] public static readonly SizeUnit[] All = { B, KB, MB, GB, TB };
-
- public static SizeUnit GetBestSizeUnit(params long[] values)
- {
- if (!values.Any())
- return B;
- // Use the largest unit to display the smallest recorded measurement without loss of precision.
- long minValue = values.Min();
- foreach (var sizeUnit in All)
- {
- if (minValue < sizeUnit.ByteAmount * BytesInKiloByte)
- return sizeUnit;
- }
- return All.Last();
- }
-
- public static double Convert(long value, SizeUnit from, SizeUnit to) => value * (double)from.ByteAmount / (to ?? GetBestSizeUnit(value)).ByteAmount;
-
- public bool Equals(SizeUnit other)
- {
- if (ReferenceEquals(null, other))
- return false;
- if (ReferenceEquals(this, other))
- return true;
- return string.Equals(Name, other.Name) && string.Equals(Description, other.Description) && ByteAmount == other.ByteAmount;
- }
-
- public override bool Equals(object obj)
- {
- if (ReferenceEquals(null, obj))
- return false;
- if (ReferenceEquals(this, obj))
- return true;
- if (obj.GetType() != this.GetType())
- return false;
- return Equals((SizeUnit) obj);
- }
-
- public override int GetHashCode() => HashCode.Combine(Name, Description, ByteAmount);
-
- public static bool operator ==(SizeUnit left, SizeUnit right) => Equals(left, right);
-
- public static bool operator !=(SizeUnit left, SizeUnit right) => !Equals(left, right);
- }
-}
\ No newline at end of file
diff --git a/src/BenchmarkDotNet/Columns/SizeValue.cs b/src/BenchmarkDotNet/Columns/SizeValue.cs
deleted file mode 100644
index 9478989eb1..0000000000
--- a/src/BenchmarkDotNet/Columns/SizeValue.cs
+++ /dev/null
@@ -1,61 +0,0 @@
-using System.Globalization;
-using BenchmarkDotNet.Helpers;
-using JetBrains.Annotations;
-using Perfolizer.Common;
-
-namespace BenchmarkDotNet.Columns
-{
- public struct SizeValue
- {
- public long Bytes { get; }
-
- public SizeValue(long bytes) => Bytes = bytes;
-
- public SizeValue(long bytes, SizeUnit unit) : this(bytes * unit.ByteAmount) { }
-
- public static readonly SizeValue B = SizeUnit.B.ToValue();
- public static readonly SizeValue KB = SizeUnit.KB.ToValue();
- public static readonly SizeValue MB = SizeUnit.MB.ToValue();
- public static readonly SizeValue GB = SizeUnit.GB.ToValue();
- public static readonly SizeValue TB = SizeUnit.TB.ToValue();
-
- [Pure] public static SizeValue FromBytes(long value) => value * B;
- [Pure] public static SizeValue FromKilobytes(long value) => value * KB;
- [Pure] public static SizeValue FromMegabytes(long value) => value * MB;
- [Pure] public static SizeValue FromGigabytes(long value) => value * GB;
- [Pure] public static SizeValue FromTerabytes(long value) => value * TB;
-
- [Pure] public static SizeValue operator *(SizeValue value, long k) => new SizeValue(value.Bytes * k);
- [Pure] public static SizeValue operator *(long k, SizeValue value) => new SizeValue(value.Bytes * k);
-
- [Pure]
- public string ToString(
- CultureInfo? cultureInfo,
- string? format = "0.##",
- UnitPresentation? unitPresentation = null)
- {
- return ToString(null, cultureInfo, format, unitPresentation);
- }
-
- [Pure]
- public string ToString(
- SizeUnit? sizeUnit,
- CultureInfo? cultureInfo,
- string? format = "0.##",
- UnitPresentation? unitPresentation = null)
- {
- sizeUnit = sizeUnit ?? SizeUnit.GetBestSizeUnit(Bytes);
- cultureInfo = cultureInfo ?? DefaultCultureInfo.Instance;
- format = format ?? "0.##";
- unitPresentation = unitPresentation ?? UnitPresentation.Default;
- double unitValue = SizeUnit.Convert(Bytes, SizeUnit.B, sizeUnit);
- if (unitPresentation.IsVisible)
- {
- string unitName = sizeUnit.Name.PadLeft(unitPresentation.MinUnitWidth);
- return $"{unitValue.ToString(format, cultureInfo)} {unitName}";
- }
-
- return unitValue.ToString(format, cultureInfo);
- }
- }
-}
\ No newline at end of file
diff --git a/src/BenchmarkDotNet/Columns/StatisticColumn.cs b/src/BenchmarkDotNet/Columns/StatisticColumn.cs
index 31e72f2446..1e1fd63b35 100644
--- a/src/BenchmarkDotNet/Columns/StatisticColumn.cs
+++ b/src/BenchmarkDotNet/Columns/StatisticColumn.cs
@@ -6,10 +6,10 @@
using BenchmarkDotNet.Reports;
using BenchmarkDotNet.Running;
using JetBrains.Annotations;
-using Perfolizer.Common;
using Perfolizer.Horology;
using Perfolizer.Mathematics.Common;
using Perfolizer.Mathematics.Multimodality;
+using Perfolizer.Metrology;
namespace BenchmarkDotNet.Columns
{
@@ -38,7 +38,7 @@ private enum Priority
s => s.StandardDeviation, Priority.Main, parentColumn: Mean);
public static readonly IColumn Error = new StatisticColumn(Column.Error, "Half of 99.9% confidence interval",
- s => new ConfidenceInterval(s.Mean, s.StandardError, s.N, ConfidenceLevel.L999).Margin, Priority.Main, parentColumn: Mean);
+ s => s.GetConfidenceInterval(ConfidenceLevel.L999).Margin, Priority.Main, parentColumn: Mean);
public static readonly IColumn OperationsPerSecond = new StatisticColumn(Column.OperationPerSecond, "Operation per second",
s => 1.0 * 1000 * 1000 * 1000 / s.Mean, Priority.Additional, UnitType.Dimensionless);
@@ -67,7 +67,7 @@ private enum Priority
/// See http://www.brendangregg.com/FrequencyTrails/modes.html
///
public static readonly IColumn MValue = new StatisticColumn(Column.MValue, "Modal value, see http://www.brendangregg.com/FrequencyTrails/modes.html",
- s => MValueCalculator.Calculate(s.OriginalValues), Priority.Additional, UnitType.Dimensionless);
+ s => MValueCalculator.Calculate(s.Sample.Values), Priority.Additional, UnitType.Dimensionless);
public static readonly IColumn Iterations = new StatisticColumn(Column.Iterations, "Number of target iterations",
s => s.N, Priority.Additional, UnitType.Dimensionless);
@@ -84,17 +84,17 @@ private enum Priority
[PublicAPI]
public static IColumn CiLower(ConfidenceLevel level) => new StatisticColumn(
- $"CI{level.ToPercentStr()} Lower", $"Lower bound of {level.ToPercentStr()} confidence interval",
+ $"CI{level} Lower", $"Lower bound of {level} confidence interval",
s => new ConfidenceInterval(s.Mean, s.StandardError, s.N, level).Lower, Priority.Additional);
[PublicAPI]
public static IColumn CiUpper(ConfidenceLevel level) => new StatisticColumn(
- $"CI{level.ToPercentStr()} Upper", $"Upper bound of {level.ToPercentStr()} confidence interval",
+ $"CI{level} Upper", $"Upper bound of {level} confidence interval",
s => new ConfidenceInterval(s.Mean, s.StandardError, s.N, level).Upper, Priority.Additional);
[PublicAPI]
public static IColumn CiError(ConfidenceLevel level) => new StatisticColumn(
- $"CI{level.ToPercentStr()} Margin", $"Half of {level.ToPercentStr()} confidence interval",
+ $"CI{level} Margin", $"Half of {level} confidence interval",
s => new ConfidenceInterval(s.Mean, s.StandardError, s.N, level).Margin, Priority.Additional);
@@ -107,7 +107,7 @@ private enum Priority
private readonly IStatisticColumn parentColumn;
private StatisticColumn(string columnName, string legend, Func calc, Priority priority, UnitType type = UnitType.Time,
- IStatisticColumn parentColumn = null)
+ IStatisticColumn? parentColumn = null)
{
this.calc = calc;
this.priority = priority;
@@ -118,33 +118,34 @@ private StatisticColumn(string columnName, string legend, Func Format(summary, benchmarkCase.Config, summary[benchmarkCase].ResultStatistics, SummaryStyle.Default);
+ => Format(summary, benchmarkCase.Config, summary[benchmarkCase]?.ResultStatistics, SummaryStyle.Default);
public string GetValue(Summary summary, BenchmarkCase benchmarkCase, SummaryStyle style)
- => Format(summary, benchmarkCase.Config, summary[benchmarkCase].ResultStatistics, style);
+ => Format(summary, benchmarkCase.Config, summary[benchmarkCase]?.ResultStatistics, style);
public bool IsAvailable(Summary summary) => true;
public bool AlwaysShow => true;
public ColumnCategory Category => ColumnCategory.Statistics;
- public int PriorityInCategory => (int) priority;
+ public int PriorityInCategory => (int)priority;
public bool IsNumeric => true;
public UnitType UnitType { get; }
public string Legend { get; }
public List GetAllValues(Summary summary, SummaryStyle style)
- => summary.Reports
+ {
+ return summary.Reports
.Where(r => r.ResultStatistics != null)
.Select(r => calc(r.ResultStatistics))
.Where(v => !double.IsNaN(v) && !double.IsInfinity(v))
- .Select(v => UnitType == UnitType.Time ? v / style.TimeUnit.NanosecondAmount : v)
+ .Select(v => UnitType == UnitType.Time && style.TimeUnit != null ? v / style.TimeUnit.BaseUnits : v)
.ToList();
+ }
- private string Format(Summary summary, ImmutableConfig config, Statistics statistics, SummaryStyle style)
+ private string Format(Summary summary, ImmutableConfig config, Statistics? statistics, SummaryStyle style)
{
if (statistics == null)
return "NA";
-
int precision = summary.DisplayPrecisionManager.GetPrecision(style, this, parentColumn);
string format = "N" + precision;
@@ -155,9 +156,9 @@ private string Format(Summary summary, ImmutableConfig config, Statistics statis
? TimeInterval.FromNanoseconds(value)
.ToString(
style.TimeUnit,
- style.CultureInfo,
format,
- UnitPresentation.FromVisibility(style.PrintUnitsInContent))
+ style.CultureInfo,
+ new UnitPresentation(style.PrintUnitsInContent, minUnitWidth: 0, gap: true))
: value.ToString(format, style.CultureInfo);
}
diff --git a/src/BenchmarkDotNet/Columns/StatisticalTestColumn.cs b/src/BenchmarkDotNet/Columns/StatisticalTestColumn.cs
index 0316b9c5f4..51edbb40f9 100644
--- a/src/BenchmarkDotNet/Columns/StatisticalTestColumn.cs
+++ b/src/BenchmarkDotNet/Columns/StatisticalTestColumn.cs
@@ -4,50 +4,57 @@
using BenchmarkDotNet.Mathematics;
using BenchmarkDotNet.Reports;
using BenchmarkDotNet.Running;
+using Perfolizer.Mathematics.Common;
using Perfolizer.Mathematics.SignificanceTesting;
-using Perfolizer.Mathematics.Thresholds;
+using Perfolizer.Mathematics.SignificanceTesting.MannWhitney;
+using Perfolizer.Metrology;
namespace BenchmarkDotNet.Columns
{
- public class StatisticalTestColumn : BaselineCustomColumn
+ public class StatisticalTestColumn(Threshold threshold, SignificanceLevel? significanceLevel = null) : BaselineCustomColumn
{
- public static StatisticalTestColumn Create(StatisticalTestKind kind, Threshold threshold, bool showPValues = false)
- => new StatisticalTestColumn(kind, threshold, showPValues);
+ private static readonly SignificanceLevel DefaultSignificanceLevel = SignificanceLevel.P1E5;
- public StatisticalTestKind Kind { get; }
- public Threshold Threshold { get; }
- public bool ShowPValues { get; }
+ public static StatisticalTestColumn CreateDefault() => new (new PercentValue(10).ToThreshold());
- public StatisticalTestColumn(StatisticalTestKind kind, Threshold threshold, bool showPValues = false)
+ public static StatisticalTestColumn Create(Threshold threshold, SignificanceLevel? significanceLevel = null) => new (threshold, significanceLevel);
+
+ public static StatisticalTestColumn Create(string threshold, SignificanceLevel? significanceLevel = null)
{
- Kind = kind;
- Threshold = threshold;
- ShowPValues = showPValues;
+ if (!Threshold.TryParse(threshold, out var parsedThreshold))
+ throw new ArgumentException($"Can't parse threshold '{threshold}'");
+ return new StatisticalTestColumn(parsedThreshold, significanceLevel);
}
- public override string Id => nameof(StatisticalTestColumn) + "." + Kind + "." + Threshold + "." + (ShowPValues ? "WithDetails" : "WithoutDetails");
- public override string ColumnName => $"{Kind}({Threshold.ToString().Replace(" ", "")}){(ShowPValues ? "/p-values" : "")}";
+ public Threshold Threshold { get; } = threshold;
+ public SignificanceLevel SignificanceLevel { get; } = significanceLevel ?? DefaultSignificanceLevel;
+
+ public override string Id => $"{nameof(StatisticalTestColumn)}/{Threshold}";
+ public override string ColumnName => $"MannWhitney({Threshold})";
public override string GetValue(Summary summary, BenchmarkCase benchmarkCase, Statistics baseline, IReadOnlyDictionary baselineMetrics,
Statistics current, IReadOnlyDictionary currentMetrics, bool isBaseline)
{
- var x = baseline.OriginalValues.ToArray();
- var y = current.OriginalValues.ToArray();
- switch (Kind)
+ if (baseline.Sample.Values.SequenceEqual(current.Sample.Values))
+ return "Baseline";
+ if (current.Sample.Size == 1 && baseline.Sample.Size == 1)
+ return "?";
+
+ var test = new SimpleEquivalenceTest(MannWhitneyTest.Instance);
+ var comparisonResult = test.Perform(current.Sample, baseline.Sample, Threshold, SignificanceLevel);
+ return comparisonResult switch
{
- case StatisticalTestKind.Welch:
- return StatisticalTestHelper.CalculateTost(WelchTest.Instance, x, y, Threshold).ToString(ShowPValues);
- case StatisticalTestKind.MannWhitney:
- return StatisticalTestHelper.CalculateTost(MannWhitneyTest.Instance, x, y, Threshold).ToString(ShowPValues);
- default:
- throw new ArgumentOutOfRangeException();
- }
+ ComparisonResult.Greater => "Slower",
+ ComparisonResult.Indistinguishable => "Same",
+ ComparisonResult.Lesser => "Faster",
+ _ => throw new ArgumentOutOfRangeException()
+ };
}
- public override int PriorityInCategory => (int) Kind;
+ public override int PriorityInCategory => 0;
public override bool IsNumeric => false;
public override UnitType UnitType => UnitType.Dimensionless;
- public override string Legend => $"{Kind}-based TOST equivalence test with {Threshold} threshold{(ShowPValues ? ". Format: 'Result: p-value(Slower)|p-value(Faster)'" : "")}";
+ public override string Legend => $"MannWhitney-based equivalence test (threshold={Threshold}, alpha = {SignificanceLevel})";
}
}
\ No newline at end of file
diff --git a/src/BenchmarkDotNet/Columns/TagColumn.cs b/src/BenchmarkDotNet/Columns/TagColumn.cs
index e4300f037c..425b4d2ad8 100644
--- a/src/BenchmarkDotNet/Columns/TagColumn.cs
+++ b/src/BenchmarkDotNet/Columns/TagColumn.cs
@@ -19,7 +19,7 @@ public TagColumn(string columnName, Func getTag)
}
public bool IsDefault(Summary summary, BenchmarkCase benchmarkCase) => false;
- public string GetValue(Summary summary, BenchmarkCase benchmarkCase) => getTag(benchmarkCase.Descriptor.WorkloadMethod.Name);
+ public string GetValue(Summary summary, BenchmarkCase benchmarkCase) => getTag(benchmarkCase.Descriptor?.WorkloadMethod?.Name ?? "");
public bool IsAvailable(Summary summary) => true;
public bool AlwaysShow => true;
diff --git a/src/BenchmarkDotNet/Columns/UnitType.cs b/src/BenchmarkDotNet/Columns/UnitType.cs
index 606a68bd2d..f9e7815fc4 100644
--- a/src/BenchmarkDotNet/Columns/UnitType.cs
+++ b/src/BenchmarkDotNet/Columns/UnitType.cs
@@ -1,5 +1,6 @@
namespace BenchmarkDotNet.Columns
{
+ // TODO: migrate to Perfolizer.Metrology
public enum UnitType
{
Dimensionless,
diff --git a/src/BenchmarkDotNet/Configs/ConfigExtensions.cs b/src/BenchmarkDotNet/Configs/ConfigExtensions.cs
index fa0920c7f9..5177227c2a 100644
--- a/src/BenchmarkDotNet/Configs/ConfigExtensions.cs
+++ b/src/BenchmarkDotNet/Configs/ConfigExtensions.cs
@@ -7,6 +7,7 @@
using BenchmarkDotNet.Analysers;
using BenchmarkDotNet.Columns;
using BenchmarkDotNet.Diagnosers;
+using BenchmarkDotNet.EventProcessors;
using BenchmarkDotNet.Exporters;
using BenchmarkDotNet.Filters;
using BenchmarkDotNet.Jobs;
@@ -110,6 +111,7 @@ public static class ConfigExtensions
[Obsolete("This method will soon be removed, please start using .AddLogicalGroupRules() instead.")]
[EditorBrowsable(EditorBrowsableState.Never)] public static IConfig With(this IConfig config, params BenchmarkLogicalGroupRule[] rules) => config.AddLogicalGroupRules(rules);
[PublicAPI] public static ManualConfig AddLogicalGroupRules(this IConfig config, params BenchmarkLogicalGroupRule[] rules) => config.With(c => c.AddLogicalGroupRules(rules));
+ [PublicAPI] public static ManualConfig AddEventProcessor(this IConfig config, params EventProcessor[] eventProcessors) => config.With(c => c.AddEventProcessor(eventProcessors));
[PublicAPI] public static ManualConfig HideColumns(this IConfig config, params string[] columnNames) => config.With(c => c.HideColumns(columnNames));
[PublicAPI] public static ManualConfig HideColumns(this IConfig config, params IColumn[] columns) => config.With(c => c.HideColumns(columns));
diff --git a/src/BenchmarkDotNet/Configs/ConfigOptions.cs b/src/BenchmarkDotNet/Configs/ConfigOptions.cs
index c1ad75d642..06bc7768c7 100644
--- a/src/BenchmarkDotNet/Configs/ConfigOptions.cs
+++ b/src/BenchmarkDotNet/Configs/ConfigOptions.cs
@@ -48,7 +48,11 @@ public enum ConfigOptions
///
/// Continue the execution if the last run was stopped.
///
- Resume = 1 << 9
+ Resume = 1 << 9,
+ ///
+ /// Determines whether parallel build of benchmark projects should be disabled.
+ ///
+ DisableParallelBuild = 1 << 10,
}
internal static class ConfigOptionsExtensions
diff --git a/src/BenchmarkDotNet/Configs/DebugConfig.cs b/src/BenchmarkDotNet/Configs/DebugConfig.cs
index 0d66540bb1..551c38fa09 100644
--- a/src/BenchmarkDotNet/Configs/DebugConfig.cs
+++ b/src/BenchmarkDotNet/Configs/DebugConfig.cs
@@ -4,6 +4,7 @@
using BenchmarkDotNet.Analysers;
using BenchmarkDotNet.Columns;
using BenchmarkDotNet.Diagnosers;
+using BenchmarkDotNet.EventProcessors;
using BenchmarkDotNet.Exporters;
using BenchmarkDotNet.Filters;
using BenchmarkDotNet.Jobs;
@@ -61,6 +62,7 @@ public abstract class DebugConfig : IConfig
public IEnumerable GetDiagnosers() => Array.Empty();
public IEnumerable GetAnalysers() => Array.Empty();
public IEnumerable GetHardwareCounters() => Array.Empty();
+ public IEnumerable GetEventProcessors() => Array.Empty();
public IEnumerable GetFilters() => Array.Empty();
public IEnumerable GetColumnHidingRules() => Array.Empty();
@@ -69,6 +71,7 @@ public abstract class DebugConfig : IConfig
public SummaryStyle SummaryStyle => SummaryStyle.Default;
public ConfigUnionRule UnionRule => ConfigUnionRule.Union;
public TimeSpan BuildTimeout => DefaultConfig.Instance.BuildTimeout;
+ public WakeLockType WakeLock => WakeLockType.None;
public string ArtifactsPath => null; // DefaultConfig.ArtifactsPath will be used if the user does not specify it in explicit way
diff --git a/src/BenchmarkDotNet/Configs/DefaultConfig.cs b/src/BenchmarkDotNet/Configs/DefaultConfig.cs
index 8d1df6285b..8585a77797 100644
--- a/src/BenchmarkDotNet/Configs/DefaultConfig.cs
+++ b/src/BenchmarkDotNet/Configs/DefaultConfig.cs
@@ -4,7 +4,9 @@
using System.IO;
using BenchmarkDotNet.Analysers;
using BenchmarkDotNet.Columns;
+using BenchmarkDotNet.Detectors;
using BenchmarkDotNet.Diagnosers;
+using BenchmarkDotNet.EventProcessors;
using BenchmarkDotNet.Exporters;
using BenchmarkDotNet.Exporters.Csv;
using BenchmarkDotNet.Filters;
@@ -85,11 +87,13 @@ public IEnumerable GetValidators()
public TimeSpan BuildTimeout => TimeSpan.FromSeconds(120);
+ public WakeLockType WakeLock => WakeLockType.System;
+
public string ArtifactsPath
{
get
{
- var root = RuntimeInformation.IsAndroid() ?
+ var root = OsDetector.IsAndroid() ?
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) :
Directory.GetCurrentDirectory();
return Path.Combine(root, "BenchmarkDotNet.Artifacts");
@@ -108,6 +112,8 @@ public string ArtifactsPath
public IEnumerable GetFilters() => Array.Empty();
+ public IEnumerable GetEventProcessors() => Array.Empty();
+
public IEnumerable GetColumnHidingRules() => Array.Empty();
}
}
\ No newline at end of file
diff --git a/src/BenchmarkDotNet/Configs/IConfig.cs b/src/BenchmarkDotNet/Configs/IConfig.cs
index ba34cfeddf..9e3bf50ce6 100644
--- a/src/BenchmarkDotNet/Configs/IConfig.cs
+++ b/src/BenchmarkDotNet/Configs/IConfig.cs
@@ -4,6 +4,7 @@
using BenchmarkDotNet.Analysers;
using BenchmarkDotNet.Columns;
using BenchmarkDotNet.Diagnosers;
+using BenchmarkDotNet.EventProcessors;
using BenchmarkDotNet.Exporters;
using BenchmarkDotNet.Filters;
using BenchmarkDotNet.Jobs;
@@ -27,6 +28,7 @@ public interface IConfig
IEnumerable GetHardwareCounters();
IEnumerable GetFilters();
IEnumerable GetLogicalGroupRules();
+ IEnumerable GetEventProcessors();
IEnumerable GetColumnHidingRules();
IOrderer? Orderer { get; }
@@ -53,6 +55,8 @@ public interface IConfig
///
TimeSpan BuildTimeout { get; }
+ public WakeLockType WakeLock { get; }
+
///
/// Collect any errors or warnings when composing the configuration
///
diff --git a/src/BenchmarkDotNet/Configs/ImmutableConfig.cs b/src/BenchmarkDotNet/Configs/ImmutableConfig.cs
index b5d84eaf56..0043ecbe4b 100644
--- a/src/BenchmarkDotNet/Configs/ImmutableConfig.cs
+++ b/src/BenchmarkDotNet/Configs/ImmutableConfig.cs
@@ -6,6 +6,7 @@
using BenchmarkDotNet.Analysers;
using BenchmarkDotNet.Columns;
using BenchmarkDotNet.Diagnosers;
+using BenchmarkDotNet.EventProcessors;
using BenchmarkDotNet.Exporters;
using BenchmarkDotNet.Filters;
using BenchmarkDotNet.Jobs;
@@ -31,6 +32,7 @@ public sealed class ImmutableConfig : IConfig
private readonly ImmutableHashSet hardwareCounters;
private readonly ImmutableHashSet filters;
private readonly ImmutableArray rules;
+ private readonly ImmutableHashSet eventProcessors;
private readonly ImmutableArray columnHidingRules;
internal ImmutableConfig(
@@ -45,6 +47,7 @@ internal ImmutableConfig(
ImmutableArray uniqueRules,
ImmutableArray uniqueColumnHidingRules,
ImmutableHashSet uniqueRunnableJobs,
+ ImmutableHashSet uniqueEventProcessors,
ConfigUnionRule unionRule,
string artifactsPath,
CultureInfo cultureInfo,
@@ -53,6 +56,7 @@ internal ImmutableConfig(
SummaryStyle summaryStyle,
ConfigOptions options,
TimeSpan buildTimeout,
+ WakeLockType wakeLock,
IReadOnlyList configAnalysisConclusion)
{
columnProviders = uniqueColumnProviders;
@@ -66,6 +70,7 @@ internal ImmutableConfig(
rules = uniqueRules;
columnHidingRules = uniqueColumnHidingRules;
jobs = uniqueRunnableJobs;
+ eventProcessors = uniqueEventProcessors;
UnionRule = unionRule;
ArtifactsPath = artifactsPath;
CultureInfo = cultureInfo;
@@ -74,6 +79,7 @@ internal ImmutableConfig(
SummaryStyle = summaryStyle;
Options = options;
BuildTimeout = buildTimeout;
+ WakeLock = wakeLock;
ConfigAnalysisConclusion = configAnalysisConclusion;
}
@@ -85,6 +91,7 @@ internal ImmutableConfig(
public ICategoryDiscoverer CategoryDiscoverer { get; }
public SummaryStyle SummaryStyle { get; }
public TimeSpan BuildTimeout { get; }
+ public WakeLockType WakeLock { get; }
public IEnumerable GetColumnProviders() => columnProviders;
public IEnumerable GetExporters() => exporters;
@@ -96,6 +103,7 @@ internal ImmutableConfig(
public IEnumerable GetHardwareCounters() => hardwareCounters;
public IEnumerable GetFilters() => filters;
public IEnumerable GetLogicalGroupRules() => rules;
+ public IEnumerable GetEventProcessors() => eventProcessors;
public IEnumerable GetColumnHidingRules() => columnHidingRules;
public ILogger GetCompositeLogger() => new CompositeLogger(loggers);
@@ -114,7 +122,7 @@ internal ImmutableConfig(
public bool HasExtraStatsDiagnoser() => HasMemoryDiagnoser() || HasThreadingDiagnoser() || HasExceptionDiagnoser();
- public IDiagnoser GetCompositeDiagnoser(BenchmarkCase benchmarkCase, RunMode runMode)
+ public IDiagnoser? GetCompositeDiagnoser(BenchmarkCase benchmarkCase, RunMode runMode)
{
var diagnosersForGivenMode = diagnosers.Where(diagnoser => diagnoser.GetRunMode(benchmarkCase) == runMode).ToImmutableHashSet();
diff --git a/src/BenchmarkDotNet/Configs/ImmutableConfigBuilder.cs b/src/BenchmarkDotNet/Configs/ImmutableConfigBuilder.cs
index 0540dd1426..f93e5590d0 100644
--- a/src/BenchmarkDotNet/Configs/ImmutableConfigBuilder.cs
+++ b/src/BenchmarkDotNet/Configs/ImmutableConfigBuilder.cs
@@ -53,6 +53,7 @@ public static ImmutableConfig Create(IConfig source)
var uniqueHidingRules = source.GetColumnHidingRules().ToImmutableArray();
var uniqueRunnableJobs = GetRunnableJobs(source.GetJobs()).ToImmutableHashSet();
+ var uniqueEventProcessors = source.GetEventProcessors().ToImmutableHashSet();
return new ImmutableConfig(
uniqueColumnProviders,
@@ -66,6 +67,7 @@ public static ImmutableConfig Create(IConfig source)
uniqueRules,
uniqueHidingRules,
uniqueRunnableJobs,
+ uniqueEventProcessors,
source.UnionRule,
source.ArtifactsPath ?? DefaultConfig.Instance.ArtifactsPath,
source.CultureInfo,
@@ -74,6 +76,7 @@ public static ImmutableConfig Create(IConfig source)
source.SummaryStyle ?? SummaryStyle.Default,
source.Options,
source.BuildTimeout,
+ source.WakeLock,
configAnalyse.AsReadOnly()
);
}
diff --git a/src/BenchmarkDotNet/Configs/ManualConfig.cs b/src/BenchmarkDotNet/Configs/ManualConfig.cs
index 27eca81863..cdfb64a234 100644
--- a/src/BenchmarkDotNet/Configs/ManualConfig.cs
+++ b/src/BenchmarkDotNet/Configs/ManualConfig.cs
@@ -6,6 +6,7 @@
using BenchmarkDotNet.Analysers;
using BenchmarkDotNet.Columns;
using BenchmarkDotNet.Diagnosers;
+using BenchmarkDotNet.EventProcessors;
using BenchmarkDotNet.Exporters;
using BenchmarkDotNet.Extensions;
using BenchmarkDotNet.Filters;
@@ -33,6 +34,7 @@ public class ManualConfig : IConfig
private readonly HashSet hardwareCounters = new HashSet();
private readonly List filters = new List();
private readonly List logicalGroupRules = new List();
+ private readonly List eventProcessors = new List();
private readonly List columnHidingRules = new List();
public IEnumerable GetColumnProviders() => columnProviders;
@@ -45,6 +47,7 @@ public class ManualConfig : IConfig
public IEnumerable GetHardwareCounters() => hardwareCounters;
public IEnumerable GetFilters() => filters;
public IEnumerable GetLogicalGroupRules() => logicalGroupRules;
+ public IEnumerable GetEventProcessors() => eventProcessors;
public IEnumerable GetColumnHidingRules() => columnHidingRules;
[PublicAPI] public ConfigOptions Options { get; set; }
@@ -55,6 +58,7 @@ public class ManualConfig : IConfig
[PublicAPI] public ICategoryDiscoverer CategoryDiscoverer { get; set; }
[PublicAPI] public SummaryStyle SummaryStyle { get; set; }
[PublicAPI] public TimeSpan BuildTimeout { get; set; } = DefaultConfig.Instance.BuildTimeout;
+ [PublicAPI] public WakeLockType WakeLock { get; set; } = DefaultConfig.Instance.WakeLock;
public IReadOnlyList ConfigAnalysisConclusion => emptyConclusion;
@@ -106,6 +110,12 @@ public ManualConfig WithBuildTimeout(TimeSpan buildTimeout)
return this;
}
+ public ManualConfig WithWakeLock(WakeLockType wakeLockType)
+ {
+ WakeLock = wakeLockType;
+ return this;
+ }
+
[EditorBrowsable(EditorBrowsableState.Never)]
[Obsolete("This method will soon be removed, please start using .AddColumn() instead.")]
public void Add(params IColumn[] newColumns) => AddColumn(newColumns);
@@ -221,6 +231,12 @@ public ManualConfig AddLogicalGroupRules(params BenchmarkLogicalGroupRule[] rule
return this;
}
+ public ManualConfig AddEventProcessor(params EventProcessor[] newEventProcessors)
+ {
+ this.eventProcessors.AddRange(newEventProcessors);
+ return this;
+ }
+
[PublicAPI]
public ManualConfig HideColumns(params string[] columnNames)
{
@@ -254,6 +270,7 @@ public void Add(IConfig config)
validators.AddRange(config.GetValidators());
hardwareCounters.AddRange(config.GetHardwareCounters());
filters.AddRange(config.GetFilters());
+ eventProcessors.AddRange(config.GetEventProcessors());
Orderer = config.Orderer ?? Orderer;
CategoryDiscoverer = config.CategoryDiscoverer ?? CategoryDiscoverer;
ArtifactsPath = config.ArtifactsPath ?? ArtifactsPath;
@@ -263,6 +280,7 @@ public void Add(IConfig config)
columnHidingRules.AddRange(config.GetColumnHidingRules());
Options |= config.Options;
BuildTimeout = GetBuildTimeout(BuildTimeout, config.BuildTimeout);
+ WakeLock = GetWakeLock(WakeLock, config.WakeLock);
}
///
@@ -317,5 +335,12 @@ private static TimeSpan GetBuildTimeout(TimeSpan current, TimeSpan other)
=> current == DefaultConfig.Instance.BuildTimeout
? other
: TimeSpan.FromMilliseconds(Math.Max(current.TotalMilliseconds, other.TotalMilliseconds));
+
+ private static WakeLockType GetWakeLock(WakeLockType current, WakeLockType other)
+ {
+ if (current == DefaultConfig.Instance.WakeLock) { return other; }
+ if (other == DefaultConfig.Instance.WakeLock) { return current; }
+ return current.CompareTo(other) > 0 ? current : other;
+ }
}
}
diff --git a/src/BenchmarkDotNet/Configs/WakeLockType.cs b/src/BenchmarkDotNet/Configs/WakeLockType.cs
new file mode 100644
index 0000000000..f547e44764
--- /dev/null
+++ b/src/BenchmarkDotNet/Configs/WakeLockType.cs
@@ -0,0 +1,20 @@
+namespace BenchmarkDotNet.Configs
+{
+ public enum WakeLockType
+ {
+ ///
+ /// Allows the system to enter sleep and/or turn off the display while benchmarks are running.
+ ///
+ None,
+
+ ///
+ /// Forces the system to be in the working state while benchmarks are running.
+ ///
+ System,
+
+ ///
+ /// Forces the display to be on while benchmarks are running.
+ ///
+ Display
+ }
+}
\ No newline at end of file
diff --git a/src/BenchmarkDotNet/ConsoleArguments/CommandLineOptions.cs b/src/BenchmarkDotNet/ConsoleArguments/CommandLineOptions.cs
index e3aed1fedd..6c7b48b0e2 100644
--- a/src/BenchmarkDotNet/ConsoleArguments/CommandLineOptions.cs
+++ b/src/BenchmarkDotNet/ConsoleArguments/CommandLineOptions.cs
@@ -2,6 +2,7 @@
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
+using BenchmarkDotNet.Configs;
using BenchmarkDotNet.ConsoleArguments.ListBenchmarks;
using BenchmarkDotNet.Diagnosers;
using BenchmarkDotNet.Engines;
@@ -179,6 +180,9 @@ public bool UseDisassemblyDiagnoser
[Option("buildTimeout", Required = false, HelpText = "Build timeout in seconds.")]
public int? TimeOutInSeconds { get; set; }
+ [Option("wakeLock", Required = false, HelpText = "Prevents the system from entering sleep or turning off the display. None/System/Display.")]
+ public WakeLockType? WakeLock { get; set; }
+
[Option("stopOnFirstError", Required = false, Default = false, HelpText = "Stop on first error.")]
public bool StopOnFirstError { get; set; }
@@ -237,8 +241,8 @@ public static IEnumerable Examples
yield return new Example("Use Job.ShortRun for running the benchmarks", shortName, new CommandLineOptions { BaseJob = "short" });
yield return new Example("Run benchmarks in process", shortName, new CommandLineOptions { RunInProcess = true });
- yield return new Example("Run benchmarks for .NET 4.7.2, .NET Core 2.1 and Mono. .NET 4.7.2 will be baseline because it was first.", longName, new CommandLineOptions { Runtimes = new[] { "net472", "netcoreapp2.1", "Mono" } });
- yield return new Example("Run benchmarks for .NET Core 2.0, .NET Core 2.1 and .NET Core 2.2. .NET Core 2.0 will be baseline because it was first.", longName, new CommandLineOptions { Runtimes = new[] { "netcoreapp2.0", "netcoreapp2.1", "netcoreapp2.2" } });
+ yield return new Example("Run benchmarks for .NET 4.7.2, .NET 8.0 and Mono. .NET 4.7.2 will be baseline because it was first.", longName, new CommandLineOptions { Runtimes = new[] { "net472", "net8.0", "Mono" } });
+ yield return new Example("Run benchmarks for .NET Core 3.1, .NET 6.0 and .NET 8.0. .NET Core 3.1 will be baseline because it was first.", longName, new CommandLineOptions { Runtimes = new[] { "netcoreapp3.1", "net6.0", "net8.0" } });
yield return new Example("Use MemoryDiagnoser to get GC stats", shortName, new CommandLineOptions { UseMemoryDiagnoser = true });
yield return new Example("Use DisassemblyDiagnoser to get disassembly", shortName, new CommandLineOptions { UseDisassemblyDiagnoser = true });
yield return new Example("Use HardwareCountersDiagnoser to get hardware counter info", longName, new CommandLineOptions { HardwareCounters = new[] { nameof(HardwareCounter.CacheMisses), nameof(HardwareCounter.InstructionRetired) } });
@@ -250,8 +254,8 @@ public static IEnumerable Examples
yield return new Example("Run selected benchmarks once per iteration", longName, new CommandLineOptions { RunOncePerIteration = true });
yield return new Example("Run selected benchmarks 100 times per iteration. Perform single warmup iteration and 5 actual workload iterations", longName, new CommandLineOptions { InvocationCount = 100, WarmupIterationCount = 1, IterationCount = 5});
yield return new Example("Run selected benchmarks 250ms per iteration. Perform from 9 to 15 iterations", longName, new CommandLineOptions { IterationTimeInMilliseconds = 250, MinIterationCount = 9, MaxIterationCount = 15});
- yield return new Example("Run MannWhitney test with relative ratio of 5% for all benchmarks for .NET Core 2.0 (base) vs .NET Core 2.1 (diff). .NET Core 2.0 will be baseline because it was provided as first.", longName,
- new CommandLineOptions { Filters = new[] { "*"}, Runtimes = new[] { "netcoreapp2.0", "netcoreapp2.1" }, StatisticalTestThreshold = "5%" });
+ yield return new Example("Run MannWhitney test with relative ratio of 5% for all benchmarks for .NET 6.0 (base) vs .NET 8.0 (diff). .NET Core 6.0 will be baseline because it was provided as first.", longName,
+ new CommandLineOptions { Filters = new[] { "*"}, Runtimes = new[] { "net6.0", "net8.0" }, StatisticalTestThreshold = "5%" });
yield return new Example("Run benchmarks using environment variables 'ENV_VAR_KEY_1' with value 'value_1' and 'ENV_VAR_KEY_2' with value 'value_2'", longName,
new CommandLineOptions { EnvironmentVariables = new[] { "ENV_VAR_KEY_1:value_1", "ENV_VAR_KEY_2:value_2" } });
yield return new Example("Hide Mean and Ratio columns (use double quotes for multi-word columns: \"Alloc Ratio\")", shortName, new CommandLineOptions { HiddenColumns = new[] { "Mean", "Ratio" }, });
diff --git a/src/BenchmarkDotNet/ConsoleArguments/ConfigParser.cs b/src/BenchmarkDotNet/ConsoleArguments/ConfigParser.cs
index 76500bed44..a77a4796f8 100644
--- a/src/BenchmarkDotNet/ConsoleArguments/ConfigParser.cs
+++ b/src/BenchmarkDotNet/ConsoleArguments/ConfigParser.cs
@@ -28,9 +28,8 @@
using CommandLine;
using Perfolizer.Horology;
using Perfolizer.Mathematics.OutlierDetection;
-using Perfolizer.Mathematics.SignificanceTesting;
-using Perfolizer.Mathematics.Thresholds;
using BenchmarkDotNet.Toolchains.Mono;
+using Perfolizer.Metrology;
namespace BenchmarkDotNet.ConsoleArguments
{
@@ -72,7 +71,7 @@ public static class ConfigParser
{ "fullxml", new[] { XmlExporter.Full } }
};
- public static (bool isSuccess, IConfig config, CommandLineOptions options) Parse(string[] args, ILogger logger, IConfig globalConfig = null)
+ public static (bool isSuccess, IConfig config, CommandLineOptions options) Parse(string[] args, ILogger logger, IConfig? globalConfig = null)
{
(bool isSuccess, IConfig config, CommandLineOptions options) result = default;
@@ -361,7 +360,7 @@ private static IConfig CreateConfig(CommandLineOptions options, IConfig globalCo
if (options.DisplayAllStatistics)
config.AddColumn(StatisticColumn.AllStatistics);
if (!string.IsNullOrEmpty(options.StatisticalTestThreshold) && Threshold.TryParse(options.StatisticalTestThreshold, out var threshold))
- config.AddColumn(new StatisticalTestColumn(StatisticalTestKind.MannWhitney, threshold));
+ config.AddColumn(new StatisticalTestColumn(threshold));
if (options.ArtifactsDirectory != null)
config.ArtifactsPath = options.ArtifactsDirectory.FullName;
@@ -393,6 +392,9 @@ private static IConfig CreateConfig(CommandLineOptions options, IConfig globalCo
if (options.TimeOutInSeconds.HasValue)
config.WithBuildTimeout(TimeSpan.FromSeconds(options.TimeOutInSeconds.Value));
+ if (options.WakeLock.HasValue)
+ config.WithWakeLock(options.WakeLock.Value);
+
return config;
}
@@ -532,6 +534,8 @@ private static Job CreateJobForGivenRuntime(Job baseJob, string runtimeId, Comma
case RuntimeMoniker.Net60:
case RuntimeMoniker.Net70:
case RuntimeMoniker.Net80:
+ case RuntimeMoniker.Net90:
+ case RuntimeMoniker.Net10_0:
return baseJob
.WithRuntime(runtimeMoniker.GetRuntime())
.WithToolchain(CsProjCoreToolchain.From(new NetCoreAppSettings(runtimeId, null, runtimeId, options.CliPath?.FullName, options.RestorePath?.FullName)));
@@ -543,10 +547,16 @@ private static Job CreateJobForGivenRuntime(Job baseJob, string runtimeId, Comma
return CreateAotJob(baseJob, options, runtimeMoniker, "6.0.0-*", "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-experimental/nuget/v3/index.json");
case RuntimeMoniker.NativeAot70:
- return CreateAotJob(baseJob, options, runtimeMoniker, "", "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet7/nuget/v3/index.json");
+ return CreateAotJob(baseJob, options, runtimeMoniker, "", "https://api.nuget.org/v3/index.json");
case RuntimeMoniker.NativeAot80:
- return CreateAotJob(baseJob, options, runtimeMoniker, "", "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet8/nuget/v3/index.json");
+ return CreateAotJob(baseJob, options, runtimeMoniker, "", "https://api.nuget.org/v3/index.json");
+
+ case RuntimeMoniker.NativeAot90:
+ return CreateAotJob(baseJob, options, runtimeMoniker, "", "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet9/nuget/v3/index.json");
+
+ case RuntimeMoniker.NativeAot10_0:
+ return CreateAotJob(baseJob, options, runtimeMoniker, "", "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet10/nuget/v3/index.json");
case RuntimeMoniker.Wasm:
return MakeWasmJob(baseJob, options, RuntimeInformation.IsNetCore ? CoreRuntime.GetCurrentVersion().MsBuildMoniker : "net5.0", runtimeMoniker);
@@ -563,17 +573,29 @@ private static Job CreateJobForGivenRuntime(Job baseJob, string runtimeId, Comma
case RuntimeMoniker.WasmNet80:
return MakeWasmJob(baseJob, options, "net8.0", runtimeMoniker);
+ case RuntimeMoniker.WasmNet90:
+ return MakeWasmJob(baseJob, options, "net9.0", runtimeMoniker);
+
+ case RuntimeMoniker.WasmNet10_0:
+ return MakeWasmJob(baseJob, options, "net10.0", runtimeMoniker);
+
case RuntimeMoniker.MonoAOTLLVM:
- return MakeMonoAOTLLVMJob(baseJob, options, RuntimeInformation.IsNetCore ? CoreRuntime.GetCurrentVersion().MsBuildMoniker : "net6.0");
+ return MakeMonoAOTLLVMJob(baseJob, options, RuntimeInformation.IsNetCore ? CoreRuntime.GetCurrentVersion().MsBuildMoniker : "net6.0", runtimeMoniker);
case RuntimeMoniker.MonoAOTLLVMNet60:
- return MakeMonoAOTLLVMJob(baseJob, options, "net6.0");
+ return MakeMonoAOTLLVMJob(baseJob, options, "net6.0", runtimeMoniker);
case RuntimeMoniker.MonoAOTLLVMNet70:
- return MakeMonoAOTLLVMJob(baseJob, options, "net7.0");
+ return MakeMonoAOTLLVMJob(baseJob, options, "net7.0", runtimeMoniker);
case RuntimeMoniker.MonoAOTLLVMNet80:
- return MakeMonoAOTLLVMJob(baseJob, options, "net8.0");
+ return MakeMonoAOTLLVMJob(baseJob, options, "net8.0", runtimeMoniker);
+
+ case RuntimeMoniker.MonoAOTLLVMNet90:
+ return MakeMonoAOTLLVMJob(baseJob, options, "net9.0", runtimeMoniker);
+
+ case RuntimeMoniker.MonoAOTLLVMNet10_0:
+ return MakeMonoAOTLLVMJob(baseJob, options, "net10.0", runtimeMoniker);
case RuntimeMoniker.Mono60:
return MakeMonoJob(baseJob, options, MonoRuntime.Mono60);
@@ -584,6 +606,12 @@ private static Job CreateJobForGivenRuntime(Job baseJob, string runtimeId, Comma
case RuntimeMoniker.Mono80:
return MakeMonoJob(baseJob, options, MonoRuntime.Mono80);
+ case RuntimeMoniker.Mono90:
+ return MakeMonoJob(baseJob, options, MonoRuntime.Mono90);
+
+ case RuntimeMoniker.Mono10_0:
+ return MakeMonoJob(baseJob, options, MonoRuntime.Mono10_0);
+
default:
throw new NotSupportedException($"Runtime {runtimeId} is not supported");
}
@@ -624,9 +652,9 @@ private static Job MakeMonoJob(Job baseJob, CommandLineOptions options, MonoRunt
packagesPath: options.RestorePath?.FullName)));
}
- private static Job MakeMonoAOTLLVMJob(Job baseJob, CommandLineOptions options, string msBuildMoniker)
+ private static Job MakeMonoAOTLLVMJob(Job baseJob, CommandLineOptions options, string msBuildMoniker, RuntimeMoniker moniker)
{
- var monoAotLLVMRuntime = new MonoAotLLVMRuntime(aotCompilerPath: options.AOTCompilerPath, aotCompilerMode: options.AOTCompilerMode, msBuildMoniker: msBuildMoniker);
+ var monoAotLLVMRuntime = new MonoAotLLVMRuntime(aotCompilerPath: options.AOTCompilerPath, aotCompilerMode: options.AOTCompilerMode, msBuildMoniker: msBuildMoniker, moniker: moniker);
var toolChain = MonoAotLLVMToolChain.From(
new NetCoreAppSettings(
@@ -750,13 +778,20 @@ private static string GetCoreRunToolchainDisplayName(IReadOnlyList pat
return coreRunPath.FullName.Substring(lastCommonDirectorySeparatorIndex);
}
- private static bool TryParse(string runtime, out RuntimeMoniker runtimeMoniker)
+ internal static bool TryParse(string runtime, out RuntimeMoniker runtimeMoniker)
{
int index = runtime.IndexOf('-');
+ if (index >= 0)
+ {
+ runtime = runtime.Substring(0, index);
+ }
- return index < 0
- ? Enum.TryParse(runtime.Replace(".", string.Empty), ignoreCase: true, out runtimeMoniker)
- : Enum.TryParse(runtime.Substring(0, index).Replace(".", string.Empty), ignoreCase: true, out runtimeMoniker);
+ // Monikers older than Net 10 don't use any version delimiter, newer monikers use _ delimiter.
+ if (Enum.TryParse(runtime.Replace(".", string.Empty), ignoreCase: true, out runtimeMoniker))
+ {
+ return true;
+ }
+ return Enum.TryParse(runtime.Replace('.', '_'), ignoreCase: true, out runtimeMoniker);
}
}
}
\ No newline at end of file
diff --git a/src/BenchmarkDotNet/Portability/Cpu/HardwareIntrinsics.cs b/src/BenchmarkDotNet/Detectors/Cpu/HardwareIntrinsics.cs
similarity index 81%
rename from src/BenchmarkDotNet/Portability/Cpu/HardwareIntrinsics.cs
rename to src/BenchmarkDotNet/Detectors/Cpu/HardwareIntrinsics.cs
index 68c48809b7..a8c7655272 100644
--- a/src/BenchmarkDotNet/Portability/Cpu/HardwareIntrinsics.cs
+++ b/src/BenchmarkDotNet/Detectors/Cpu/HardwareIntrinsics.cs
@@ -1,14 +1,15 @@
using System;
using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
using System.Numerics;
+using System.Text;
using BenchmarkDotNet.Environments;
-using System.Diagnostics.CodeAnalysis;
#if NET6_0_OR_GREATER
using System.Runtime.Intrinsics.X86;
using System.Runtime.Intrinsics.Arm;
#endif
-namespace BenchmarkDotNet.Portability.Cpu
+namespace BenchmarkDotNet.Detectors.Cpu
{
internal static class HardwareIntrinsics
{
@@ -16,6 +17,8 @@ internal static class HardwareIntrinsics
internal static string GetShortInfo()
{
+ if (IsX86Avx512FSupported)
+ return GetShortAvx512Representation();
if (IsX86Avx2Supported)
return "AVX2";
else if (IsX86AvxSupported)
@@ -52,7 +55,9 @@ static IEnumerable GetCurrentProcessInstructionSets(Platform platform)
{
case Platform.X86:
case Platform.X64:
- if (IsX86Avx2Supported) yield return "AVX2";
+
+ if (IsX86Avx512FSupported) yield return GetShortAvx512Representation();
+ else if (IsX86Avx2Supported) yield return "AVX2";
else if (IsX86AvxSupported) yield return "AVX";
else if (IsX86Sse42Supported) yield return "SSE4.2";
else if (IsX86Sse41Supported) yield return "SSE4.1";
@@ -90,6 +95,18 @@ static IEnumerable GetCurrentProcessInstructionSets(Platform platform)
}
}
+ private static string GetShortAvx512Representation()
+ {
+ StringBuilder avx512 = new ("AVX-512F");
+ if (IsX86Avx512CDSupported) avx512.Append("+CD");
+ if (IsX86Avx512BWSupported) avx512.Append("+BW");
+ if (IsX86Avx512DQSupported) avx512.Append("+DQ");
+ if (IsX86Avx512FVLSupported) avx512.Append("+VL");
+ if (IsX86Avx512VbmiSupported) avx512.Append("+VBMI");
+
+ return avx512.ToString();
+ }
+
internal static bool IsX86BaseSupported =>
#if NET6_0_OR_GREATER
X86Base.IsSupported;
@@ -153,6 +170,48 @@ static IEnumerable GetCurrentProcessInstructionSets(Platform platform)
GetIsSupported("System.Runtime.Intrinsics.X86.Avx2");
#endif
+ internal static bool IsX86Avx512FSupported =>
+#if NET8_0_OR_GREATER
+ Avx512F.IsSupported;
+#else
+ GetIsSupported("System.Runtime.Intrinsics.X86.Avx512F");
+#endif
+
+ internal static bool IsX86Avx512FVLSupported =>
+#if NET8_0_OR_GREATER
+ Avx512F.VL.IsSupported;
+#else
+ GetIsSupported("System.Runtime.Intrinsics.X86.Avx512F+VL");
+#endif
+
+ internal static bool IsX86Avx512BWSupported =>
+#if NET8_0_OR_GREATER
+ Avx512BW.IsSupported;
+#else
+ GetIsSupported("System.Runtime.Intrinsics.X86.Avx512BW");
+#endif
+
+ internal static bool IsX86Avx512CDSupported =>
+#if NET8_0_OR_GREATER
+ Avx512CD.IsSupported;
+#else
+ GetIsSupported("System.Runtime.Intrinsics.X86.Avx512CD");
+#endif
+
+ internal static bool IsX86Avx512DQSupported =>
+#if NET8_0_OR_GREATER
+ Avx512DQ.IsSupported;
+#else
+ GetIsSupported("System.Runtime.Intrinsics.X86.Avx512DQ");
+#endif
+
+ internal static bool IsX86Avx512VbmiSupported =>
+#if NET8_0_OR_GREATER
+ Avx512Vbmi.IsSupported;
+#else
+ GetIsSupported("System.Runtime.Intrinsics.X86.Avx512Vbmi");
+#endif
+
internal static bool IsX86AesSupported =>
#if NET6_0_OR_GREATER
System.Runtime.Intrinsics.X86.Aes.IsSupported;
@@ -211,8 +270,12 @@ static IEnumerable GetCurrentProcessInstructionSets(Platform platform)
GetIsSupported("System.Runtime.Intrinsics.X86.AvxVnni");
#endif
- // X86Serialize was introduced in .NET 7.0, BDN does not target it so we need to use reflection
- internal static bool IsX86SerializeSupported => GetIsSupported("System.Runtime.Intrinsics.X86.X86Serialize");
+ internal static bool IsX86SerializeSupported =>
+#if NET7_0_OR_GREATER
+ X86Serialize.IsSupported;
+#else
+ GetIsSupported("System.Runtime.Intrinsics.X86.X86Serialize");
+#endif
internal static bool IsArmBaseSupported =>
#if NET6_0_OR_GREATER
diff --git a/src/BenchmarkDotNet/Detectors/Cpu/ICpuDetector.cs b/src/BenchmarkDotNet/Detectors/Cpu/ICpuDetector.cs
new file mode 100644
index 0000000000..6c0fcf683c
--- /dev/null
+++ b/src/BenchmarkDotNet/Detectors/Cpu/ICpuDetector.cs
@@ -0,0 +1,12 @@
+using Perfolizer.Phd.Dto;
+
+namespace BenchmarkDotNet.Detectors.Cpu;
+
+///
+/// Loads the for the current hardware
+///
+public interface ICpuDetector
+{
+ bool IsApplicable();
+ PhdCpu? Detect();
+}
\ No newline at end of file
diff --git a/src/BenchmarkDotNet/Detectors/Cpu/Linux/LinuxCpuDetector.cs b/src/BenchmarkDotNet/Detectors/Cpu/Linux/LinuxCpuDetector.cs
new file mode 100644
index 0000000000..fd3c98d049
--- /dev/null
+++ b/src/BenchmarkDotNet/Detectors/Cpu/Linux/LinuxCpuDetector.cs
@@ -0,0 +1,32 @@
+using System.Collections.Generic;
+using BenchmarkDotNet.Helpers;
+using BenchmarkDotNet.Portability;
+using Perfolizer.Phd.Dto;
+
+namespace BenchmarkDotNet.Detectors.Cpu.Linux;
+
+///
+/// CPU information from output of the `cat /proc/cpuinfo` and `lscpu` command.
+/// Linux only.
+///
+internal class LinuxCpuDetector : ICpuDetector
+{
+ public bool IsApplicable() => OsDetector.IsLinux();
+
+ public PhdCpu? Detect()
+ {
+ if (!IsApplicable()) return null;
+
+ // lscpu output respects the system locale, so we should force language invariant environment for correct parsing
+ var languageInvariantEnvironment = new Dictionary
+ {
+ ["LC_ALL"] = "C",
+ ["LANG"] = "C",
+ ["LANGUAGE"] = "C"
+ };
+
+ string cpuInfo = ProcessHelper.RunAndReadOutput("cat", "/proc/cpuinfo") ?? "";
+ string lscpu = ProcessHelper.RunAndReadOutput("/bin/bash", "-c \"lscpu\"", environmentVariables: languageInvariantEnvironment);
+ return LinuxCpuInfoParser.Parse(cpuInfo, lscpu);
+ }
+}
\ No newline at end of file
diff --git a/src/BenchmarkDotNet/Detectors/Cpu/Linux/LinuxCpuInfoParser.cs b/src/BenchmarkDotNet/Detectors/Cpu/Linux/LinuxCpuInfoParser.cs
new file mode 100644
index 0000000000..b1e554a6fd
--- /dev/null
+++ b/src/BenchmarkDotNet/Detectors/Cpu/Linux/LinuxCpuInfoParser.cs
@@ -0,0 +1,115 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Text.RegularExpressions;
+using BenchmarkDotNet.Extensions;
+using BenchmarkDotNet.Helpers;
+using BenchmarkDotNet.Portability;
+using Perfolizer.Horology;
+using Perfolizer.Phd.Dto;
+
+namespace BenchmarkDotNet.Detectors.Cpu.Linux;
+
+internal static class LinuxCpuInfoParser
+{
+ private static class ProcCpu
+ {
+ internal const string PhysicalId = "physical id";
+ internal const string CpuCores = "cpu cores";
+ internal const string ModelName = "model name";
+ internal const string MaxFrequency = "max freq";
+ }
+
+ private static class Lscpu
+ {
+ internal const string MaxFrequency = "CPU max MHz";
+ internal const string ModelName = "Model name";
+ internal const string CoresPerSocket = "Core(s) per socket";
+ }
+
+ /// Output of `cat /proc/cpuinfo`
+ /// Output of `lscpu`
+ internal static PhdCpu Parse(string? cpuInfo, string? lscpu)
+ {
+ var processorModelNames = new HashSet();
+ var processorsToPhysicalCoreCount = new Dictionary();
+ int logicalCoreCount = 0;
+ Frequency? maxFrequency = null;
+
+ var logicalCores = SectionsHelper.ParseSections(cpuInfo, ':');
+ foreach (var logicalCore in logicalCores)
+ {
+ if (logicalCore.TryGetValue(ProcCpu.PhysicalId, out string physicalId) &&
+ logicalCore.TryGetValue(ProcCpu.CpuCores, out string cpuCoresValue) &&
+ int.TryParse(cpuCoresValue, out int cpuCoreCount) &&
+ cpuCoreCount > 0)
+ processorsToPhysicalCoreCount[physicalId] = cpuCoreCount;
+
+ if (logicalCore.TryGetValue(ProcCpu.ModelName, out string modelName))
+ {
+ processorModelNames.Add(modelName);
+ logicalCoreCount++;
+ }
+
+ if (logicalCore.TryGetValue(ProcCpu.MaxFrequency, out string maxCpuFreqValue) &&
+ Frequency.TryParseMHz(maxCpuFreqValue, out var maxCpuFreq))
+ {
+ maxFrequency = maxCpuFreq;
+ }
+ }
+
+ int? coresPerSocket = null;
+ if (lscpu != null)
+ {
+ var lscpuParts = lscpu.Split('\n')
+ .Where(line => line.Contains(':'))
+ .SelectMany(line => line.Split([':'], 2))
+ .ToList();
+ for (int i = 0; i + 1 < lscpuParts.Count; i += 2)
+ {
+ string name = lscpuParts[i].Trim();
+ string value = lscpuParts[i + 1].Trim();
+
+ if (name.EqualsWithIgnoreCase(Lscpu.MaxFrequency) &&
+ Frequency.TryParseMHz(value.Replace(',', '.'), out var maxFrequencyParsed)) // Example: `CPU max MHz: 3200,0000`
+ maxFrequency = maxFrequencyParsed;
+
+ if (name.EqualsWithIgnoreCase(Lscpu.ModelName))
+ processorModelNames.Add(value);
+
+ if (name.EqualsWithIgnoreCase(Lscpu.CoresPerSocket) &&
+ int.TryParse(value, out int coreCount))
+ coresPerSocket = coreCount;
+ }
+ }
+
+ var nominalFrequency = processorModelNames
+ .Select(ParseFrequencyFromBrandString)
+ .WhereNotNull()
+ .FirstOrDefault() ?? maxFrequency;
+ string processorName = processorModelNames.Count > 0 ? string.Join(", ", processorModelNames) : null;
+ int? physicalProcessorCount = processorsToPhysicalCoreCount.Count > 0 ? processorsToPhysicalCoreCount.Count : null;
+ int? physicalCoreCount = processorsToPhysicalCoreCount.Count > 0 ? processorsToPhysicalCoreCount.Values.Sum() : coresPerSocket;
+ return new PhdCpu
+ {
+ ProcessorName = processorName,
+ PhysicalProcessorCount = physicalProcessorCount,
+ PhysicalCoreCount = physicalCoreCount,
+ LogicalCoreCount = logicalCoreCount > 0 ? logicalCoreCount : null,
+ NominalFrequencyHz = nominalFrequency?.Hertz.RoundToLong(),
+ MaxFrequencyHz = maxFrequency?.Hertz.RoundToLong()
+ };
+ }
+
+ internal static Frequency? ParseFrequencyFromBrandString(string brandString)
+ {
+ const string pattern = "(\\d.\\d+)GHz";
+ var matches = Regex.Matches(brandString, pattern, RegexOptions.IgnoreCase);
+ if (matches.Count > 0 && matches[0].Groups.Count > 1)
+ {
+ string match = Regex.Matches(brandString, pattern, RegexOptions.IgnoreCase)[0].Groups[1].ToString();
+ return Frequency.TryParseGHz(match, out var result) ? result : null;
+ }
+
+ return null;
+ }
+}
\ No newline at end of file
diff --git a/src/BenchmarkDotNet/Detectors/Cpu/Windows/MosCpuDetector.cs b/src/BenchmarkDotNet/Detectors/Cpu/Windows/MosCpuDetector.cs
new file mode 100644
index 0000000000..8c0b2718eb
--- /dev/null
+++ b/src/BenchmarkDotNet/Detectors/Cpu/Windows/MosCpuDetector.cs
@@ -0,0 +1,64 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Management;
+using BenchmarkDotNet.Extensions;
+using BenchmarkDotNet.Portability;
+using Perfolizer.Horology;
+using Perfolizer.Phd.Dto;
+
+namespace BenchmarkDotNet.Detectors.Cpu.Windows;
+
+internal class MosCpuDetector : ICpuDetector
+{
+#if NET6_0_OR_GREATER
+ [System.Runtime.Versioning.SupportedOSPlatform("windows")]
+#endif
+ public bool IsApplicable() => OsDetector.IsWindows() &&
+ RuntimeInformation.IsFullFramework &&
+ !RuntimeInformation.IsMono;
+
+#if NET6_0_OR_GREATER
+ [System.Runtime.Versioning.SupportedOSPlatform("windows")]
+#endif
+ public PhdCpu? Detect()
+ {
+ if (!IsApplicable()) return null;
+
+ var processorModelNames = new HashSet();
+ int physicalCoreCount = 0;
+ int logicalCoreCount = 0;
+ int processorsCount = 0;
+ int sumMaxFrequency = 0;
+
+ using (var mosProcessor = new ManagementObjectSearcher("SELECT * FROM Win32_Processor"))
+ {
+ foreach (var moProcessor in mosProcessor.Get().Cast())
+ {
+ string name = moProcessor[WmicCpuInfoKeyNames.Name]?.ToString();
+ if (!string.IsNullOrEmpty(name))
+ {
+ processorModelNames.Add(name);
+ processorsCount++;
+ physicalCoreCount += (int)(uint)moProcessor[WmicCpuInfoKeyNames.NumberOfCores];
+ logicalCoreCount += (int)(uint)moProcessor[WmicCpuInfoKeyNames.NumberOfLogicalProcessors];
+ sumMaxFrequency = (int)(uint)moProcessor[WmicCpuInfoKeyNames.MaxClockSpeed];
+ }
+ }
+ }
+
+ string processorName = processorModelNames.Count > 0 ? string.Join(", ", processorModelNames) : null;
+ Frequency? maxFrequency = sumMaxFrequency > 0 && processorsCount > 0
+ ? Frequency.FromMHz(sumMaxFrequency * 1.0 / processorsCount)
+ : null;
+
+ return new PhdCpu
+ {
+ ProcessorName = processorName,
+ PhysicalProcessorCount = processorsCount > 0 ? processorsCount : null,
+ PhysicalCoreCount = physicalCoreCount > 0 ? physicalCoreCount : null,
+ LogicalCoreCount = logicalCoreCount > 0 ? logicalCoreCount : null,
+ NominalFrequencyHz = maxFrequency?.Hertz.RoundToLong(),
+ MaxFrequencyHz = maxFrequency?.Hertz.RoundToLong()
+ };
+ }
+}
\ No newline at end of file
diff --git a/src/BenchmarkDotNet/Detectors/Cpu/Windows/WindowsCpuDetector.cs b/src/BenchmarkDotNet/Detectors/Cpu/Windows/WindowsCpuDetector.cs
new file mode 100644
index 0000000000..9969a1bca0
--- /dev/null
+++ b/src/BenchmarkDotNet/Detectors/Cpu/Windows/WindowsCpuDetector.cs
@@ -0,0 +1,3 @@
+namespace BenchmarkDotNet.Detectors.Cpu.Windows;
+
+internal class WindowsCpuDetector() : CpuDetector(new MosCpuDetector(), new WmicCpuDetector());
\ No newline at end of file
diff --git a/src/BenchmarkDotNet/Detectors/Cpu/Windows/WmicCpuDetector.cs b/src/BenchmarkDotNet/Detectors/Cpu/Windows/WmicCpuDetector.cs
new file mode 100644
index 0000000000..e7ae5401bc
--- /dev/null
+++ b/src/BenchmarkDotNet/Detectors/Cpu/Windows/WmicCpuDetector.cs
@@ -0,0 +1,30 @@
+using System.IO;
+using BenchmarkDotNet.Helpers;
+using BenchmarkDotNet.Portability;
+using Perfolizer.Phd.Dto;
+
+namespace BenchmarkDotNet.Detectors.Cpu.Windows;
+
+///
+/// CPU information from output of the `wmic cpu get Name, NumberOfCores, NumberOfLogicalProcessors /Format:List` command.
+/// Windows only.
+///
+internal class WmicCpuDetector : ICpuDetector
+{
+ private const string DefaultWmicPath = @"C:\Windows\System32\wbem\WMIC.exe";
+
+ public bool IsApplicable() => OsDetector.IsWindows();
+
+ public PhdCpu? Detect()
+ {
+ if (!IsApplicable()) return null;
+
+ const string argList = $"{WmicCpuInfoKeyNames.Name}, " +
+ $"{WmicCpuInfoKeyNames.NumberOfCores}, " +
+ $"{WmicCpuInfoKeyNames.NumberOfLogicalProcessors}, " +
+ $"{WmicCpuInfoKeyNames.MaxClockSpeed}";
+ string wmicPath = File.Exists(DefaultWmicPath) ? DefaultWmicPath : "wmic";
+ string wmicOutput = ProcessHelper.RunAndReadOutput(wmicPath, $"cpu get {argList} /Format:List");
+ return WmicCpuInfoParser.Parse(wmicOutput);
+ }
+}
\ No newline at end of file
diff --git a/src/BenchmarkDotNet/Detectors/Cpu/Windows/WmicCpuInfoKeyNames.cs b/src/BenchmarkDotNet/Detectors/Cpu/Windows/WmicCpuInfoKeyNames.cs
new file mode 100644
index 0000000000..65d8ecf7d8
--- /dev/null
+++ b/src/BenchmarkDotNet/Detectors/Cpu/Windows/WmicCpuInfoKeyNames.cs
@@ -0,0 +1,9 @@
+namespace BenchmarkDotNet.Detectors.Cpu.Windows;
+
+internal static class WmicCpuInfoKeyNames
+{
+ internal const string NumberOfLogicalProcessors = "NumberOfLogicalProcessors";
+ internal const string NumberOfCores = "NumberOfCores";
+ internal const string Name = "Name";
+ internal const string MaxClockSpeed = "MaxClockSpeed";
+}
\ No newline at end of file
diff --git a/src/BenchmarkDotNet/Detectors/Cpu/Windows/WmicCpuInfoParser.cs b/src/BenchmarkDotNet/Detectors/Cpu/Windows/WmicCpuInfoParser.cs
new file mode 100644
index 0000000000..af6ed8c12e
--- /dev/null
+++ b/src/BenchmarkDotNet/Detectors/Cpu/Windows/WmicCpuInfoParser.cs
@@ -0,0 +1,65 @@
+using System.Collections.Generic;
+using BenchmarkDotNet.Extensions;
+using BenchmarkDotNet.Helpers;
+using Perfolizer.Horology;
+using Perfolizer.Phd.Dto;
+
+namespace BenchmarkDotNet.Detectors.Cpu.Windows;
+
+internal static class WmicCpuInfoParser
+{
+ ///
+ /// Parses wmic output and returns
+ ///
+ /// Output of `wmic cpu get Name, NumberOfCores, NumberOfLogicalProcessors /Format:List`
+ internal static PhdCpu Parse(string? wmicOutput)
+ {
+ var processorModelNames = new HashSet();
+ int physicalCoreCount = 0;
+ int logicalCoreCount = 0;
+ int processorsCount = 0;
+ var sumMaxFrequency = Frequency.Zero;
+
+ var processors = SectionsHelper.ParseSections(wmicOutput, '=');
+ foreach (var processor in processors)
+ {
+ if (processor.TryGetValue(WmicCpuInfoKeyNames.NumberOfCores, out string numberOfCoresValue) &&
+ int.TryParse(numberOfCoresValue, out int numberOfCores) &&
+ numberOfCores > 0)
+ physicalCoreCount += numberOfCores;
+
+ if (processor.TryGetValue(WmicCpuInfoKeyNames.NumberOfLogicalProcessors, out string numberOfLogicalValue) &&
+ int.TryParse(numberOfLogicalValue, out int numberOfLogical) &&
+ numberOfLogical > 0)
+ logicalCoreCount += numberOfLogical;
+
+ if (processor.TryGetValue(WmicCpuInfoKeyNames.Name, out string name))
+ {
+ processorModelNames.Add(name);
+ processorsCount++;
+ }
+
+ if (processor.TryGetValue(WmicCpuInfoKeyNames.MaxClockSpeed, out string frequencyValue)
+ && int.TryParse(frequencyValue, out int frequency)
+ && frequency > 0)
+ {
+ sumMaxFrequency += frequency;
+ }
+ }
+
+ string? processorName = processorModelNames.Count > 0 ? string.Join(", ", processorModelNames) : null;
+ Frequency? maxFrequency = sumMaxFrequency > 0 && processorsCount > 0
+ ? Frequency.FromMHz(sumMaxFrequency / processorsCount)
+ : null;
+
+ return new PhdCpu
+ {
+ ProcessorName = processorName,
+ PhysicalProcessorCount = processorsCount > 0 ? processorsCount : null,
+ PhysicalCoreCount = physicalCoreCount > 0 ? physicalCoreCount : null,
+ LogicalCoreCount = logicalCoreCount > 0 ? logicalCoreCount : null,
+ NominalFrequencyHz = maxFrequency?.Hertz.RoundToLong(),
+ MaxFrequencyHz = maxFrequency?.Hertz.RoundToLong()
+ };
+ }
+}
\ No newline at end of file
diff --git a/src/BenchmarkDotNet/Detectors/Cpu/macOS/MacOsCpuDetector.cs b/src/BenchmarkDotNet/Detectors/Cpu/macOS/MacOsCpuDetector.cs
new file mode 100644
index 0000000000..54bf602020
--- /dev/null
+++ b/src/BenchmarkDotNet/Detectors/Cpu/macOS/MacOsCpuDetector.cs
@@ -0,0 +1,22 @@
+using BenchmarkDotNet.Helpers;
+using BenchmarkDotNet.Portability;
+using Perfolizer.Phd.Dto;
+
+namespace BenchmarkDotNet.Detectors.Cpu.macOS;
+
+///
+/// CPU information from output of the `sysctl -a` command.
+/// MacOSX only.
+///
+internal class MacOsCpuDetector : ICpuDetector
+{
+ public bool IsApplicable() => OsDetector.IsMacOS();
+
+ public PhdCpu? Detect()
+ {
+ if (!IsApplicable()) return null;
+
+ string sysctlOutput = ProcessHelper.RunAndReadOutput("sysctl", "-a");
+ return SysctlCpuInfoParser.Parse(sysctlOutput);
+ }
+}
\ No newline at end of file
diff --git a/src/BenchmarkDotNet/Detectors/Cpu/macOS/SysctlCpuInfoParser.cs b/src/BenchmarkDotNet/Detectors/Cpu/macOS/SysctlCpuInfoParser.cs
new file mode 100644
index 0000000000..9a96a5b677
--- /dev/null
+++ b/src/BenchmarkDotNet/Detectors/Cpu/macOS/SysctlCpuInfoParser.cs
@@ -0,0 +1,60 @@
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using BenchmarkDotNet.Extensions;
+using BenchmarkDotNet.Helpers;
+using Perfolizer.Phd.Dto;
+
+namespace BenchmarkDotNet.Detectors.Cpu.macOS;
+
+internal static class SysctlCpuInfoParser
+{
+ private static class Sysctl
+ {
+ internal const string ProcessorName = "machdep.cpu.brand_string";
+ internal const string PhysicalProcessorCount = "hw.packages";
+ internal const string PhysicalCoreCount = "hw.physicalcpu";
+ internal const string LogicalCoreCount = "hw.logicalcpu";
+ internal const string NominalFrequency = "hw.cpufrequency";
+ internal const string MaxFrequency = "hw.cpufrequency_max";
+ }
+
+ /// Output of `sysctl -a`
+ [SuppressMessage("ReSharper", "StringLiteralTypo")]
+ internal static PhdCpu Parse(string? sysctlOutput)
+ {
+ var sysctl = SectionsHelper.ParseSection(sysctlOutput, ':');
+ string processorName = sysctl.GetValueOrDefault(Sysctl.ProcessorName);
+ int? physicalProcessorCount = PositiveIntValue(sysctl, Sysctl.PhysicalProcessorCount);
+ int? physicalCoreCount = PositiveIntValue(sysctl, Sysctl.PhysicalCoreCount);
+ int? logicalCoreCount = PositiveIntValue(sysctl, Sysctl.LogicalCoreCount);
+ long? nominalFrequency = PositiveLongValue(sysctl, Sysctl.NominalFrequency);
+ long? maxFrequency = PositiveLongValue(sysctl, Sysctl.MaxFrequency);
+ return new PhdCpu
+ {
+ ProcessorName = processorName,
+ PhysicalProcessorCount = physicalProcessorCount,
+ PhysicalCoreCount = physicalCoreCount,
+ LogicalCoreCount = logicalCoreCount,
+ NominalFrequencyHz = nominalFrequency,
+ MaxFrequencyHz = maxFrequency
+ };
+ }
+
+ private static int? PositiveIntValue(Dictionary sysctl, string keyName)
+ {
+ if (sysctl.TryGetValue(keyName, out string value) &&
+ int.TryParse(value, out int result) &&
+ result > 0)
+ return result;
+ return null;
+ }
+
+ private static long? PositiveLongValue(Dictionary sysctl, string keyName)
+ {
+ if (sysctl.TryGetValue(keyName, out string value) &&
+ long.TryParse(value, out long result) &&
+ result > 0)
+ return result;
+ return null;
+ }
+}
\ No newline at end of file
diff --git a/src/BenchmarkDotNet/Detectors/CpuDetector.cs b/src/BenchmarkDotNet/Detectors/CpuDetector.cs
new file mode 100644
index 0000000000..4cf183430d
--- /dev/null
+++ b/src/BenchmarkDotNet/Detectors/CpuDetector.cs
@@ -0,0 +1,29 @@
+using System;
+using System.Linq;
+using BenchmarkDotNet.Detectors.Cpu;
+using BenchmarkDotNet.Detectors.Cpu.Linux;
+using BenchmarkDotNet.Detectors.Cpu.macOS;
+using BenchmarkDotNet.Detectors.Cpu.Windows;
+using BenchmarkDotNet.Extensions;
+using Perfolizer.Phd.Dto;
+
+namespace BenchmarkDotNet.Detectors;
+
+public class CpuDetector(params ICpuDetector[] detectors) : ICpuDetector
+{
+ public static CpuDetector CrossPlatform => new (
+ new WindowsCpuDetector(),
+ new LinuxCpuDetector(),
+ new MacOsCpuDetector());
+
+ private static readonly Lazy LazyCpu = new (() => CrossPlatform.Detect());
+ public static PhdCpu? Cpu => LazyCpu.Value;
+
+ public bool IsApplicable() => detectors.Any(loader => loader.IsApplicable());
+
+ public PhdCpu? Detect() => detectors
+ .Where(loader => loader.IsApplicable())
+ .Select(loader => loader.Detect())
+ .WhereNotNull()
+ .FirstOrDefault();
+}
\ No newline at end of file
diff --git a/src/BenchmarkDotNet/Detectors/OsDetector.cs b/src/BenchmarkDotNet/Detectors/OsDetector.cs
new file mode 100644
index 0000000000..2afd862f2f
--- /dev/null
+++ b/src/BenchmarkDotNet/Detectors/OsDetector.cs
@@ -0,0 +1,178 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using BenchmarkDotNet.Helpers;
+using Microsoft.Win32;
+using Perfolizer.Phd.Dto;
+using System.Runtime.InteropServices;
+using BenchmarkDotNet.Extensions;
+using static System.Runtime.InteropServices.RuntimeInformation;
+using RuntimeEnvironment = Microsoft.DotNet.PlatformAbstractions.RuntimeEnvironment;
+
+namespace BenchmarkDotNet.Detectors;
+
+public class OsDetector
+{
+ public static readonly OsDetector Instance = new ();
+ private OsDetector() { }
+
+ internal static string ExecutableExtension => IsWindows() ? ".exe" : string.Empty;
+ internal static string ScriptFileExtension => IsWindows() ? ".bat" : ".sh";
+
+ private readonly Lazy os = new (ResolveOs);
+ public static PhdOs GetOs() => Instance.os.Value;
+
+ private static PhdOs ResolveOs()
+ {
+ if (IsMacOS())
+ {
+ string systemVersion = ExternalToolsHelper.MacSystemProfilerData.Value.GetValueOrDefault("System Version") ?? "";
+ string kernelVersion = ExternalToolsHelper.MacSystemProfilerData.Value.GetValueOrDefault("Kernel Version") ?? "";
+ return new PhdOs
+ {
+ Name = "macOS",
+ Version = systemVersion,
+ KernelVersion = kernelVersion
+ };
+ }
+
+ if (IsLinux())
+ {
+ try
+ {
+ string version = LinuxOsReleaseHelper.GetNameByOsRelease(File.ReadAllLines("/etc/os-release"));
+ bool wsl = IsUnderWsl();
+ return new PhdOs
+ {
+ Name = "Linux",
+ Version = version,
+ Container = wsl ? "WSL" : null
+ };
+ }
+ catch (Exception)
+ {
+ // Ignore
+ }
+ }
+
+ string operatingSystem = RuntimeEnvironment.OperatingSystem;
+ string operatingSystemVersion = RuntimeEnvironment.OperatingSystemVersion;
+ if (IsWindows())
+ {
+ int? ubr = GetWindowsUbr();
+ if (ubr != null)
+ operatingSystemVersion += $".{ubr}";
+ }
+ return new PhdOs
+ {
+ Name = operatingSystem,
+ Version = operatingSystemVersion
+ };
+ }
+
+ private static bool IsUnderWsl()
+ {
+ if (!IsLinux())
+ return false;
+ try
+ {
+ return File.Exists("/proc/sys/fs/binfmt_misc/WSLInterop"); // https://superuser.com/a/1749811
+ }
+ catch (Exception)
+ {
+ return false;
+ }
+ }
+
+ // TODO: Introduce a common util API for registry calls, use it also in BenchmarkDotNet.Toolchains.CsProj.GetCurrentVersionBasedOnWindowsRegistry
+ ///
+ /// On Windows, this method returns UBR (Update Build Revision) based on Registry.
+ /// Returns null if the value is not available
+ ///
+ ///
+ private static int? GetWindowsUbr()
+ {
+ if (IsWindows())
+ {
+ try
+ {
+ using (var baseKey = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry32))
+ using (var ndpKey = baseKey.OpenSubKey(@"SOFTWARE\Microsoft\Windows NT\CurrentVersion"))
+ {
+ if (ndpKey == null)
+ return null;
+
+ return Convert.ToInt32(ndpKey.GetValue("UBR"));
+ }
+ }
+ catch (Exception)
+ {
+ return null;
+ }
+ }
+ return null;
+ }
+
+#if NET6_0_OR_GREATER
+ [System.Runtime.Versioning.SupportedOSPlatformGuard("windows")]
+#endif
+ internal static bool IsWindows() =>
+#if NET6_0_OR_GREATER
+ OperatingSystem.IsWindows(); // prefer linker-friendly OperatingSystem APIs
+#else
+ IsOSPlatform(OSPlatform.Windows);
+#endif
+
+#if NET6_0_OR_GREATER
+ [System.Runtime.Versioning.SupportedOSPlatformGuard("linux")]
+#endif
+ internal static bool IsLinux() =>
+#if NET6_0_OR_GREATER
+ OperatingSystem.IsLinux();
+#else
+ IsOSPlatform(OSPlatform.Linux);
+#endif
+
+#if NET6_0_OR_GREATER
+ [System.Runtime.Versioning.SupportedOSPlatformGuard("macos")]
+#endif
+ // ReSharper disable once InconsistentNaming
+ internal static bool IsMacOS() =>
+#if NET6_0_OR_GREATER
+ OperatingSystem.IsMacOS();
+#else
+ IsOSPlatform(OSPlatform.OSX);
+#endif
+
+#if NET6_0_OR_GREATER
+ [System.Runtime.Versioning.SupportedOSPlatformGuard("android")]
+#endif
+ internal static bool IsAndroid() =>
+#if NET6_0_OR_GREATER
+ OperatingSystem.IsAndroid();
+#else
+ Type.GetType("Java.Lang.Object, Mono.Android") != null;
+#endif
+
+#if NET6_0_OR_GREATER
+ [System.Runtime.Versioning.SupportedOSPlatformGuard("ios")]
+#endif
+ // ReSharper disable once InconsistentNaming
+ internal static bool IsIOS() =>
+#if NET6_0_OR_GREATER
+ OperatingSystem.IsIOS();
+#else
+ Type.GetType("Foundation.NSObject, Xamarin.iOS") != null;
+#endif
+
+#if NET6_0_OR_GREATER
+ [System.Runtime.Versioning.SupportedOSPlatformGuard("tvos")]
+#endif
+ // ReSharper disable once InconsistentNaming
+ internal static bool IsTvOS() =>
+#if NET6_0_OR_GREATER
+ OperatingSystem.IsTvOS();
+#else
+ IsOSPlatform(OSPlatform.Create("TVOS"));
+#endif
+}
\ No newline at end of file
diff --git a/src/BenchmarkDotNet/Diagnosers/AllocatedMemoryMetricDescriptor.cs b/src/BenchmarkDotNet/Diagnosers/AllocatedMemoryMetricDescriptor.cs
index dc6cac925d..634e795680 100644
--- a/src/BenchmarkDotNet/Diagnosers/AllocatedMemoryMetricDescriptor.cs
+++ b/src/BenchmarkDotNet/Diagnosers/AllocatedMemoryMetricDescriptor.cs
@@ -1,6 +1,7 @@
using System;
using BenchmarkDotNet.Columns;
using BenchmarkDotNet.Reports;
+using Perfolizer.Metrology;
namespace BenchmarkDotNet.Diagnosers
{
@@ -13,7 +14,7 @@ internal class AllocatedMemoryMetricDescriptor : IMetricDescriptor
public string Legend => "Allocated memory per single operation (managed only, inclusive, 1KB = 1024B)";
public string NumberFormat => "0.##";
public UnitType UnitType => UnitType.Size;
- public string Unit => SizeUnit.B.Name;
+ public string Unit => SizeUnit.B.Abbreviation;
public bool TheGreaterTheBetter => false;
public int PriorityInCategory => GC.MaxGeneration + 1;
public bool GetIsAvailable(Metric metric) => true;
diff --git a/src/BenchmarkDotNet/Diagnosers/AllocatedNativeMemoryDescriptor.cs b/src/BenchmarkDotNet/Diagnosers/AllocatedNativeMemoryDescriptor.cs
index 8781333f80..c4cb964355 100644
--- a/src/BenchmarkDotNet/Diagnosers/AllocatedNativeMemoryDescriptor.cs
+++ b/src/BenchmarkDotNet/Diagnosers/AllocatedNativeMemoryDescriptor.cs
@@ -1,5 +1,6 @@
using BenchmarkDotNet.Columns;
using BenchmarkDotNet.Reports;
+using Perfolizer.Metrology;
namespace BenchmarkDotNet.Diagnosers
{
@@ -12,7 +13,7 @@ internal class AllocatedNativeMemoryDescriptor : IMetricDescriptor
public string Legend => $"Allocated native memory per single operation";
public string NumberFormat => "N0";
public UnitType UnitType => UnitType.Size;
- public string Unit => SizeUnit.B.Name;
+ public string Unit => SizeUnit.B.Abbreviation;
public bool TheGreaterTheBetter => false;
public int PriorityInCategory => 0;
public bool GetIsAvailable(Metric metric) => true;
@@ -27,7 +28,7 @@ internal class NativeMemoryLeakDescriptor : IMetricDescriptor
public string Legend => $"Native memory leak size in byte.";
public string NumberFormat => "N0";
public UnitType UnitType => UnitType.Size;
- public string Unit => SizeUnit.B.Name;
+ public string Unit => SizeUnit.B.Abbreviation;
public bool TheGreaterTheBetter => false;
public int PriorityInCategory => 0;
public bool GetIsAvailable(Metric metric) => true;
diff --git a/src/BenchmarkDotNet/Diagnosers/DiagnoserActionParameters.cs b/src/BenchmarkDotNet/Diagnosers/DiagnoserActionParameters.cs
index a403e170f5..c7d957b385 100644
--- a/src/BenchmarkDotNet/Diagnosers/DiagnoserActionParameters.cs
+++ b/src/BenchmarkDotNet/Diagnosers/DiagnoserActionParameters.cs
@@ -6,7 +6,7 @@ namespace BenchmarkDotNet.Diagnosers
{
public class DiagnoserActionParameters
{
- public DiagnoserActionParameters(Process process, BenchmarkCase benchmarkCase, BenchmarkId benchmarkId)
+ public DiagnoserActionParameters(Process? process, BenchmarkCase benchmarkCase, BenchmarkId benchmarkId)
{
Process = process;
BenchmarkCase = benchmarkCase;
diff --git a/src/BenchmarkDotNet/Diagnosers/DiagnosersLoader.cs b/src/BenchmarkDotNet/Diagnosers/DiagnosersLoader.cs
index 1410aad8bc..40e8f231e7 100644
--- a/src/BenchmarkDotNet/Diagnosers/DiagnosersLoader.cs
+++ b/src/BenchmarkDotNet/Diagnosers/DiagnosersLoader.cs
@@ -5,6 +5,7 @@
using System.Reflection;
using System.Threading;
using BenchmarkDotNet.Configs;
+using BenchmarkDotNet.Detectors;
using BenchmarkDotNet.Loggers;
using BenchmarkDotNet.Portability;
@@ -40,11 +41,11 @@ private static IEnumerable LoadDiagnosers()
{
yield return EventPipeProfiler.Default;
- if (RuntimeInformation.IsLinux())
+ if (OsDetector.IsLinux())
yield return PerfCollectProfiler.Default;
}
- if (!RuntimeInformation.IsWindows())
+ if (!OsDetector.IsWindows())
yield break;
foreach (var windowsDiagnoser in LoadWindowsDiagnosers())
@@ -80,6 +81,11 @@ private static IDiagnoser[] LoadWindowsDiagnosers()
CreateDiagnoser(diagnosticsAssembly, "BenchmarkDotNet.Diagnostics.Windows.NativeMemoryProfiler")
};
}
+ catch (Exception ex) when (ex is FileNotFoundException || ex is BadImageFormatException)
+ {
+ // Return an array of UnresolvedDiagnoser objects when the assembly does not contain the requested diagnoser
+ return new[] { GetUnresolvedDiagnoser() };
+ }
catch (Exception ex) // we're loading a plug-in, better to be safe rather than sorry
{
ConsoleLogger.Default.WriteLineError($"Error loading {WindowsDiagnosticAssemblyFileName}: {ex.GetType().Name} - {ex.Message}");
diff --git a/src/BenchmarkDotNet/Diagnosers/EventPipeProfiler.cs b/src/BenchmarkDotNet/Diagnosers/EventPipeProfiler.cs
index 211d4a06e2..c1c76e8c9e 100644
--- a/src/BenchmarkDotNet/Diagnosers/EventPipeProfiler.cs
+++ b/src/BenchmarkDotNet/Diagnosers/EventPipeProfiler.cs
@@ -40,7 +40,7 @@ public EventPipeProfiler() :this(profile: EventPipeProfile.CpuSampling, performE
/// A named pre-defined set of provider configurations that allows common tracing scenarios to be specified succinctly.
/// A list of EventPipe providers to be enabled.
/// if set to true, benchmarks will be executed one more time with the profiler attached. If set to false, there will be no extra run but the results will contain overhead. True by default.
- public EventPipeProfiler(EventPipeProfile profile = EventPipeProfile.CpuSampling, IReadOnlyCollection providers = null, bool performExtraBenchmarksRun = true)
+ public EventPipeProfiler(EventPipeProfile profile = EventPipeProfile.CpuSampling, IReadOnlyCollection? providers = null, bool performExtraBenchmarksRun = true)
{
this.performExtraBenchmarksRun = performExtraBenchmarksRun;
eventPipeProviders = MapToProviders(profile, providers);
diff --git a/src/BenchmarkDotNet/Diagnosers/ExceptionDiagnoser.cs b/src/BenchmarkDotNet/Diagnosers/ExceptionDiagnoser.cs
index 782e895d3e..574d1e54f1 100644
--- a/src/BenchmarkDotNet/Diagnosers/ExceptionDiagnoser.cs
+++ b/src/BenchmarkDotNet/Diagnosers/ExceptionDiagnoser.cs
@@ -1,4 +1,5 @@
using BenchmarkDotNet.Analysers;
+using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Columns;
using BenchmarkDotNet.Engines;
using BenchmarkDotNet.Exporters;
@@ -14,9 +15,11 @@ namespace BenchmarkDotNet.Diagnosers
{
public class ExceptionDiagnoser : IDiagnoser
{
- public static readonly ExceptionDiagnoser Default = new ExceptionDiagnoser();
+ public static readonly ExceptionDiagnoser Default = new ExceptionDiagnoser(new ExceptionDiagnoserConfig(displayExceptionsIfZeroValue: true));
- private ExceptionDiagnoser() { }
+ public ExceptionDiagnoser(ExceptionDiagnoserConfig config) => Config = config;
+
+ public ExceptionDiagnoserConfig Config { get; }
public IEnumerable Ids => new[] { nameof(ExceptionDiagnoser) };
@@ -32,14 +35,18 @@ public void Handle(HostSignal signal, DiagnoserActionParameters parameters) { }
public IEnumerable ProcessResults(DiagnoserResults results)
{
- yield return new Metric(ExceptionsFrequencyMetricDescriptor.Instance, results.ExceptionFrequency);
+ yield return new Metric(new ExceptionsFrequencyMetricDescriptor(Config), results.ExceptionFrequency);
}
public IEnumerable Validate(ValidationParameters validationParameters) => Enumerable.Empty();
- private class ExceptionsFrequencyMetricDescriptor : IMetricDescriptor
+ internal class ExceptionsFrequencyMetricDescriptor : IMetricDescriptor
{
- internal static readonly IMetricDescriptor Instance = new ExceptionsFrequencyMetricDescriptor();
+ public ExceptionDiagnoserConfig Config { get; }
+ public ExceptionsFrequencyMetricDescriptor(ExceptionDiagnoserConfig config = null)
+ {
+ Config = config;
+ }
public string Id => "ExceptionFrequency";
public string DisplayName => Column.Exceptions;
@@ -49,7 +56,13 @@ private class ExceptionsFrequencyMetricDescriptor : IMetricDescriptor
public string Unit => "Count";
public bool TheGreaterTheBetter => false;
public int PriorityInCategory => 0;
- public bool GetIsAvailable(Metric metric) => true;
+ public bool GetIsAvailable(Metric metric)
+ {
+ if (Config == null)
+ return metric.Value > 0;
+ else
+ return Config.DisplayExceptionsIfZeroValue || metric.Value > 0;
+ }
}
}
-}
+}
\ No newline at end of file
diff --git a/src/BenchmarkDotNet/Diagnosers/PerfCollectProfiler.cs b/src/BenchmarkDotNet/Diagnosers/PerfCollectProfiler.cs
index 6dd5bdd2e9..f1b584ce09 100644
--- a/src/BenchmarkDotNet/Diagnosers/PerfCollectProfiler.cs
+++ b/src/BenchmarkDotNet/Diagnosers/PerfCollectProfiler.cs
@@ -5,6 +5,7 @@
using System.Linq;
using System.Runtime.InteropServices;
using BenchmarkDotNet.Analysers;
+using BenchmarkDotNet.Detectors;
using BenchmarkDotNet.Engines;
using BenchmarkDotNet.Exporters;
using BenchmarkDotNet.Extensions;
@@ -53,7 +54,7 @@ public class PerfCollectProfiler : IProfiler
public IEnumerable