diff --git a/.github/workflows/generate-changelog.yaml b/.github/workflows/generate-changelog.yaml
index 71592e4d46..a0538e4eac 100644
--- a/.github/workflows/generate-changelog.yaml
+++ b/.github/workflows/generate-changelog.yaml
@@ -19,8 +19,8 @@ jobs:
with:
ref: master
- - name: Download changelog
- run: ./build.cmd docs-update --depth 1 --preview
+ - name: Fetch changelog
+ run: ./build.cmd docs-fetch --depth 1 --preview --force-clone
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -28,8 +28,8 @@ jobs:
uses: JamesIves/github-pages-deploy-action@3.7.1
with:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- BRANCH: docs-changelog-details
- FOLDER: docs/_changelog/details
+ BRANCH: docs-changelog
+ FOLDER: docs/_changelog
GIT_CONFIG_NAME: Andrey Akinshin
GIT_CONFIG_EMAIL: andrey.akinshin@gmail.com
- CLEAN: true
\ No newline at end of file
+ CLEAN: true
diff --git a/.github/workflows/generate-gh-pages.yaml b/.github/workflows/generate-gh-pages.yaml
index cb01860995..8983ed711f 100644
--- a/.github/workflows/generate-gh-pages.yaml
+++ b/.github/workflows/generate-gh-pages.yaml
@@ -22,8 +22,8 @@ jobs:
- name: Build BenchmarkDotNet
run: ./build.cmd build
- - name: Download changelog
- run: ./build.cmd docs-update --depth 1 --preview
+ - name: Fetch changelog
+ run: ./build.cmd docs-fetch --depth 1 --preview --force-clone
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -31,7 +31,7 @@ jobs:
run: ./build.cmd docs-build
- name: Upload Artifacts
- uses: actions/upload-artifact@v1
+ uses: actions/upload-artifact@v4
with:
name: site
path: docs/_site
@@ -48,9 +48,13 @@ jobs:
ref: docs-stable
- name: Download Artifacts
- uses: actions/download-artifact@v1
+ uses: actions/download-artifact@v4
with:
name: site
+ path: site
+
+ - name: Print file tree
+ run: tree $GITHUB_WORKSPACE
- name: Deploy documentation
uses: JamesIves/github-pages-deploy-action@3.7.1
diff --git a/.github/workflows/publish-nightly.yaml b/.github/workflows/publish-nightly.yaml
index 3a3dc0bc9b..01f439f41e 100644
--- a/.github/workflows/publish-nightly.yaml
+++ b/.github/workflows/publish-nightly.yaml
@@ -5,19 +5,20 @@ on:
push:
branches:
- master
+ workflow_dispatch:
jobs:
publish:
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..f1c627295e 100644
--- a/.github/workflows/report-test-results.yaml
+++ b/.github/workflows/report-test-results.yaml
@@ -11,13 +11,26 @@ jobs:
report:
runs-on: ubuntu-latest
permissions: write-all
+ if: ${{ github.event.workflow_run.conclusion != 'cancelled' }}
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-selected.yaml b/.github/workflows/run-tests-selected.yaml
new file mode 100644
index 0000000000..ddbcc74e09
--- /dev/null
+++ b/.github/workflows/run-tests-selected.yaml
@@ -0,0 +1,89 @@
+name: run-tests-selected
+run-name: Run selected tests (${{ inputs.runs_on }} --framework ${{ inputs.framework}} --filter ${{ inputs.filter }})
+
+on:
+ workflow_dispatch:
+ inputs:
+ runs_on:
+ type: choice
+ description: GitHub Actions runner image name
+ required: true
+ default: ubuntu-latest
+ options:
+ - windows-latest
+ - ubuntu-latest
+ - macos-latest
+ - windows-11-arm
+ - ubuntu-24.04-arm
+ - macos-13
+ project:
+ type: string
+ description: Specify test project path
+ required: true
+ default: tests/BenchmarkDotNet.IntegrationTests
+ options:
+ - tests/BenchmarkDotNet.Tests
+ - tests/BenchmarkDotNet.IntegrationTests
+ - tests/BenchmarkDotNet.IntegrationTests.ManualRunning
+ framework:
+ type: choice
+ description: Specify target framework
+ required: true
+ options:
+ - net8.0
+ - net462
+ filter:
+ type: string
+ description: Test filter text (It's used for `dotnet test --filter`) Use default value when running all tests
+ required: true
+ default: "BenchmarkDotNet"
+ iteration_count:
+ type: number
+ description: Count of test loop (It's expected to be used for flaky tests)
+ required: true
+ default: 1
+
+jobs:
+ test:
+ name: test (${{ inputs.runs_on }} --framework ${{ inputs.framework}} --filter ${{ inputs.filter }})
+ runs-on: ${{ inputs.runs_on }}
+ timeout-minutes: 60 # Explicitly set timeout. When wrong input parameter is passed. It may continue to run until it times out (Default:360 minutes))
+ steps:
+ - uses: actions/checkout@v4
+
+ # Setup
+ - name: Setup
+ run: |
+ mkdir artifacts
+
+ # Build
+ - name: Run build
+ working-directory: ${{ github.event.inputs.project }}
+ run: |
+ dotnet build -c Release --framework ${{ inputs.framework }} -tl:off
+
+ # Test
+ - name: Run tests
+ shell: pwsh
+ working-directory: ${{ github.event.inputs.project }}
+ run: |
+ $PSNativeCommandUseErrorActionPreference = $true
+ $iterationCount = ${{ inputs.iteration_count }}
+
+ foreach($i in 1..$iterationCount) {
+ Write-Output ('##[group]Executing Iteration: {0}/${{ inputs.iteration_count }}' -f $i)
+
+ dotnet test -c Release --framework ${{ inputs.framework }} --filter ${{ inputs.filter }} -tl:off --no-build --logger "console;verbosity=normal"
+
+ Write-Output '##[endgroup]'
+ }
+
+ # Upload artifact files that are located at `$(GITHUB_WORKSPACE)/artifacts` directory
+ - name: Upload test results
+ uses: actions/upload-artifact@v4
+ if: always()
+ with:
+ name: results
+ if-no-files-found: ignore
+ path: |
+ artifacts/**/*
diff --git a/.github/workflows/run-tests.yaml b/.github/workflows/run-tests.yaml
index 90887cbecd..9d3f25a9e8 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,75 +39,118 @@ 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
+ uses: egor-tensin/setup-clang@ef434b41eb33a70396fb336b1bae39c76d740c3d # v1.4
with:
version: latest
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: "22"
+ - 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
+ name: test-macos (${{ matrix.os.arch }})
+ runs-on: ${{ matrix.os.runs-on }}
+ strategy:
+ matrix:
+ os:
+ - runs-on: 'macos-latest'
+ jsvu-os: 'mac64arm'
+ arch: 'arm64'
+ - runs-on: 'macos-13'
+ jsvu-os: 'mac64'
+ arch: 'x64'
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v4
+ - name: Set up node
+ uses: actions/setup-node@v4
+ with:
+ node-version: "22"
+ - name: Set up v8
+ run: npm install jsvu -g && jsvu --os=${{ matrix.os.jsvu-os }} --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(${{ matrix.os.arch }})-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: "22"
- name: Install cSpell
- run: npm install -g cspell
+ run: npm install -g cspell@9.0.2
- 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..53f5cf0a60 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,8 @@
True
True
True
+ True
+ True
True
True
True
@@ -173,6 +178,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 855812b5a5..7507704b8b 100644
--- a/NuGet.Config
+++ b/NuGet.Config
@@ -13,5 +13,6 @@
+
diff --git a/README.md b/README.md
index 1d183aeb6d..a9b8a76e9b 100644
--- a/README.md
+++ b/README.md
@@ -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 [18100+ GitHub projects](https://github.com/dotnet/BenchmarkDotNet/network/dependents) including
+BenchmarkDotNet is already adopted by [27000+ 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 6b8ecc7ffc..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..3e3c42c27d 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");
@@ -154,6 +157,7 @@ public void GenerateFile(FilePath filePath, StringBuilder content)
public void GenerateFile(FilePath filePath, string content, bool reportNoChanges = false)
{
+ this.EnsureDirectoryExists(filePath.GetDirectory());
var relativePath = RootDirectory.GetRelativePath(filePath);
if (this.FileExists(filePath))
{
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/Meta/Repo.cs b/build/BenchmarkDotNet.Build/Meta/Repo.cs
index 38215fcce8..22a02d51bb 100644
--- a/build/BenchmarkDotNet.Build/Meta/Repo.cs
+++ b/build/BenchmarkDotNet.Build/Meta/Repo.cs
@@ -12,7 +12,7 @@ public static class Repo
public const string HttpsUrlBase = $"https://github.com/{Owner}/{Name}";
public const string HttpsGitUrl = $"{HttpsUrlBase}.git";
- public const string ChangelogDetailsBranch = "docs-changelog-details";
+ public const string ChangelogBranch = "docs-changelog";
public const string DocsStableBranch = "docs-stable";
public const string MasterBranch = "master";
diff --git a/build/BenchmarkDotNet.Build/Options/KnownOptions.cs b/build/BenchmarkDotNet.Build/Options/KnownOptions.cs
index c5101e1d52..6da4e390ed 100644
--- a/build/BenchmarkDotNet.Build/Options/KnownOptions.cs
+++ b/build/BenchmarkDotNet.Build/Options/KnownOptions.cs
@@ -29,6 +29,12 @@ public static class KnownOptions
Aliases = new[] { "-d" }
};
+ public static readonly BoolOption ForceClone = new("--force-clone")
+ {
+ Description = "Forces re-cloning of the changelog repository, deleting any existing directory.",
+ Aliases = new[] { "-f" }
+ };
+
public static readonly BoolOption Help = new("--help")
{
Description = "Prints help information",
diff --git a/build/BenchmarkDotNet.Build/Program.cs b/build/BenchmarkDotNet.Build/Program.cs
index f099c38094..5fd25cc298 100644
--- a/build/BenchmarkDotNet.Build/Program.cs
+++ b/build/BenchmarkDotNet.Build/Program.cs
@@ -28,10 +28,10 @@ public HelpInfo GetHelp()
{
return new HelpInfo
{
- Examples = new[]
- {
+ Examples =
+ [
new Example(Name)
- }
+ ]
};
}
}
@@ -48,10 +48,30 @@ public HelpInfo GetHelp()
{
return new HelpInfo
{
- Examples = new[]
- {
+ Examples =
+ [
new Example(Name).WithMsBuildArgument("Configuration", "Debug")
- }
+ ]
+ };
+ }
+}
+
+[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 Example(Name)
+ ]
};
}
}
@@ -68,12 +88,12 @@ public HelpInfo GetHelp()
{
return new HelpInfo
{
- Examples = new[]
- {
+ Examples =
+ [
new Example(Name)
.WithArgument(KnownOptions.Exclusive)
.WithArgument(KnownOptions.Verbosity, "Diagnostic")
- }
+ ]
};
}
}
@@ -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();
}
@@ -125,68 +145,69 @@ public HelpInfo GetHelp()
{
return new HelpInfo
{
- Examples = new[]
- {
+ Examples =
+ [
new Example(Name)
.WithMsBuildArgument("VersionPrefix", "0.1.1729")
.WithMsBuildArgument("VersionSuffix", "preview"),
new Example(Name).WithArgument(KnownOptions.Stable)
- }
+ ]
};
}
}
[TaskName(Name)]
-[TaskDescription("Update generated documentation files")]
-public class DocsUpdateTask : FrostingTask, IHelpProvider
+[TaskDescription("Fetch changelog files")]
+public class DocsFetchTask : FrostingTask, IHelpProvider
{
- private const string Name = "docs-update";
- public override void Run(BuildContext context) => context.DocumentationRunner.Update();
+ private const string Name = "docs-fetch";
+ public override void Run(BuildContext context) => context.DocumentationRunner.Fetch();
public HelpInfo GetHelp()
{
return new HelpInfo
{
Description = $"This task updates the following files:\n" +
- $"* README.md (the number of dependent projects number)\n" +
+ $"* Clones branch 'docs-changelog' to docs/_changelog\n" +
$"* Last changelog footer (if {KnownOptions.Stable.CommandLineName} is specified)\n" +
$"* All changelog details in docs/_changelog\n" +
- $" (This dir is a cloned version of this repo from branch {Repo.ChangelogDetailsBranch})",
- Options = new IOption[] { KnownOptions.DocsPreview, KnownOptions.DocsDepth },
- EnvironmentVariables = new[] { EnvVar.GitHubToken },
- Examples = new[]
- {
+ $" (This dir is a cloned version of this repo from branch {Repo.ChangelogBranch})",
+ Options = [KnownOptions.DocsPreview, KnownOptions.DocsDepth, KnownOptions.ForceClone],
+ EnvironmentVariables = [EnvVar.GitHubToken],
+ Examples =
+ [
new Example(Name)
.WithArgument(KnownOptions.DocsDepth, "3")
.WithArgument(KnownOptions.DocsPreview)
- }
+ .WithArgument(KnownOptions.ForceClone)
+ ]
};
}
}
[TaskName(Name)]
-[TaskDescription("Prepare auxiliary documentation files")]
-public class DocsPrepareTask : FrostingTask, IHelpProvider
+[TaskDescription("Generate auxiliary documentation files")]
+public class DocsGenerateTask : FrostingTask, IHelpProvider
{
- private const string Name = "docs-prepare";
- public override void Run(BuildContext context) => context.DocumentationRunner.Prepare();
+ private const string Name = "docs-generate";
+ public override void Run(BuildContext context) => context.DocumentationRunner.Generate();
public HelpInfo GetHelp()
{
return new HelpInfo
{
- Options = new IOption[] { KnownOptions.DocsPreview },
- Examples = new[]
- {
+ Options = [KnownOptions.DocsPreview],
+ Examples =
+ [
new Example(Name).WithArgument(KnownOptions.DocsPreview)
- }
+ ]
};
}
}
[TaskName(Name)]
[TaskDescription("Build final documentation")]
-[IsDependentOn(typeof(DocsPrepareTask))]
+[IsDependentOn(typeof(DocsGenerateTask))]
public class DocsBuildTask : FrostingTask, IHelpProvider
{
private const string Name = "docs-build";
@@ -195,11 +216,11 @@ public class DocsBuildTask : FrostingTask, IHelpProvider
public HelpInfo GetHelp() => new()
{
Description = "The 'build' task should be run manually to build api docs",
- Options = new IOption[] { KnownOptions.DocsPreview },
- Examples = new[]
- {
+ Options = [KnownOptions.DocsPreview],
+ Examples =
+ [
new Example(Name).WithArgument(KnownOptions.DocsPreview)
- }
+ ]
};
}
@@ -207,7 +228,8 @@ public class DocsBuildTask : FrostingTask, IHelpProvider
[TaskDescription("Release new version")]
[IsDependentOn(typeof(BuildTask))]
[IsDependentOn(typeof(PackTask))]
-[IsDependentOn(typeof(DocsUpdateTask))]
+[IsDependentOn(typeof(DocsFetchTask))]
+[IsDependentOn(typeof(DocsGenerateTask))]
[IsDependentOn(typeof(DocsBuildTask))]
public class ReleaseTask : FrostingTask, IHelpProvider
{
@@ -216,10 +238,10 @@ public class ReleaseTask : FrostingTask, IHelpProvider
public HelpInfo GetHelp() => new()
{
- Options = new IOption[] { KnownOptions.NextVersion, KnownOptions.Push },
- EnvironmentVariables = new[] { EnvVar.GitHubToken, EnvVar.NuGetToken },
- Examples = new[]
- {
+ Options = [KnownOptions.NextVersion, KnownOptions.Push],
+ EnvironmentVariables = [EnvVar.GitHubToken, EnvVar.NuGetToken],
+ Examples =
+ [
new Example(Name)
.WithArgument(KnownOptions.Stable)
.WithArgument(KnownOptions.NextVersion, "0.1.1729")
@@ -227,6 +249,6 @@ public class ReleaseTask : FrostingTask, IHelpProvider
new Example(Name)
.WithArgument(KnownOptions.Stable)
.WithArgument(KnownOptions.Push)
- }
+ ]
};
}
\ No newline at end of file
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/Changelog/ChangelogBuilder.cs b/build/BenchmarkDotNet.Build/Runners/Changelog/ChangelogBuilder.cs
new file mode 100644
index 0000000000..5bfc8c91b2
--- /dev/null
+++ b/build/BenchmarkDotNet.Build/Runners/Changelog/ChangelogBuilder.cs
@@ -0,0 +1,256 @@
+using System;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Text;
+using BenchmarkDotNet.Build.Meta;
+using BenchmarkDotNet.Build.Options;
+using Cake.Common.Diagnostics;
+using Cake.Common.IO;
+using Cake.Core.IO;
+using Cake.FileHelpers;
+
+namespace BenchmarkDotNet.Build.Runners.Changelog;
+
+public class ChangelogBuilder
+{
+ private readonly BuildContext context;
+ private readonly bool preview;
+ private readonly string depth;
+ private readonly bool forceClone;
+
+ ///
+ /// Directory with original changelog part files from branch 'docs-changelog'
+ ///
+ public DirectoryPath SrcDirectory { get; }
+
+ ///
+ /// Final changelog files to be used by docfx
+ ///
+ public DirectoryPath DocfxDirectory { get; }
+
+ public ChangelogBuilder(BuildContext context)
+ {
+ this.context = context;
+ preview = KnownOptions.DocsPreview.Resolve(context);
+ depth = KnownOptions.DocsDepth.Resolve(context);
+ forceClone = KnownOptions.ForceClone.Resolve(context);
+
+ var docsDirectory = context.RootDirectory.Combine("docs");
+ SrcDirectory = docsDirectory.Combine("_changelog");
+ DocfxDirectory = docsDirectory.Combine("changelog");
+ }
+
+ public void Fetch()
+ {
+ EnvVar.GitHubToken.AssertHasValue();
+
+ EnsureSrcDirectoryExist(forceClone);
+
+ var history = context.VersionHistory;
+ var stableVersionCount = history.StableVersions.Length;
+
+ if (depth.Equals("all", StringComparison.OrdinalIgnoreCase))
+ {
+ FetchDetails(
+ history.StableVersions.First(),
+ history.FirstCommit);
+
+ for (var i = 1; i < stableVersionCount; i++)
+ FetchDetails(
+ history.StableVersions[i],
+ history.StableVersions[i - 1]);
+ }
+ else if (depth != "")
+ {
+ if (!int.TryParse(depth, CultureInfo.InvariantCulture, out var depthValue))
+ throw new InvalidDataException($"Failed to parse the depth value: '{depth}'");
+
+ for (var i = Math.Max(stableVersionCount - depthValue, 1); i < stableVersionCount; i++)
+ FetchDetails(
+ history.StableVersions[i],
+ history.StableVersions[i - 1]);
+ }
+
+ if (preview)
+ FetchDetails(
+ history.CurrentVersion,
+ history.StableVersions.Last(),
+ "HEAD");
+ }
+
+ private void FetchDetails(string version, string versionPrevious, string lastCommit = "")
+ {
+ EnsureSrcDirectoryExist();
+ context.Information($"Downloading changelog details for v{version}");
+ var detailsDirectory = SrcDirectory.Combine("details");
+ ChangelogDetailsBuilder.Run(context, detailsDirectory, version, versionPrevious, lastCommit);
+ }
+
+ public void Generate()
+ {
+ GenerateLastFooter();
+
+ foreach (var version in context.VersionHistory.StableVersions)
+ GenerateVersion(version);
+ if (preview)
+ GenerateVersion(context.VersionHistory.CurrentVersion);
+
+ GenerateIndex();
+ GenerateFull();
+ GenerateToc();
+ }
+
+ public void GenerateLastFooter()
+ {
+ var version = context.VersionHistory.CurrentVersion;
+ var previousVersion = context.VersionHistory.StableVersions.Last();
+ var date = KnownOptions.Stable.Resolve(context)
+ ? DateTime.Now.ToString("MMMM dd, yyyy", CultureInfo.InvariantCulture)
+ : "TBA";
+
+ var content = new StringBuilder();
+ content.AppendLine($"_Date: {date}_");
+ content.AppendLine("");
+ content.AppendLine(
+ $"_Milestone: [v{version}](https://github.com/dotnet/BenchmarkDotNet/issues?q=milestone%3Av{version})_");
+ content.AppendLine(
+ $"([List of commits](https://github.com/dotnet/BenchmarkDotNet/compare/v{previousVersion}...v{version}))");
+ content.AppendLine("");
+ content.AppendLine("_NuGet Packages:_");
+ foreach (var packageName in context.NuGetPackageNames)
+ content.AppendLine($"* https://www.nuget.org/packages/{packageName}/{version}");
+
+ var fileName = "v" + context.VersionHistory.CurrentVersion + ".md";
+ var filePath = SrcDirectory.Combine("footer").CombineWithFilePath(fileName);
+ context.GenerateFile(filePath, content);
+ }
+
+ private void GenerateVersion(string version)
+ {
+ EnsureSrcDirectoryExist();
+ var md = $"v{version}.md";
+ var header = SrcDirectory.Combine("header").CombineWithFilePath(md);
+ var footer = SrcDirectory.Combine("footer").CombineWithFilePath(md);
+ var details = SrcDirectory.Combine("details").CombineWithFilePath(md);
+ var release = DocfxDirectory.CombineWithFilePath(md);
+
+ var content = new StringBuilder();
+ content.AppendLine("---");
+ content.AppendLine("uid: changelog.v" + version);
+ content.AppendLine("---");
+ content.AppendLine("");
+ content.AppendLine("# BenchmarkDotNet v" + version);
+ content.AppendLine("");
+ content.AppendLine("");
+
+ if (context.FileExists(header))
+ {
+ content.AppendLine(context.FileReadText(header));
+ content.AppendLine("");
+ content.AppendLine("");
+ }
+
+ if (context.FileExists(details))
+ {
+ content.AppendLine(context.FileReadText(details));
+ content.AppendLine("");
+ content.AppendLine("");
+ }
+
+ if (context.FileExists(footer))
+ {
+ content.AppendLine("## Additional details");
+ content.AppendLine("");
+ content.AppendLine(context.FileReadText(footer));
+ }
+
+ context.GenerateFile(release, content.ToString());
+ }
+
+ private void GenerateIndex()
+ {
+ var content = new StringBuilder();
+ content.AppendLine("---");
+ content.AppendLine("uid: changelog");
+ content.AppendLine("---");
+ content.AppendLine("");
+ content.AppendLine("# ChangeLog");
+ content.AppendLine("");
+ if (preview)
+ content.AppendLine($"* @changelog.v{context.VersionHistory.CurrentVersion}");
+ foreach (var version in context.VersionHistory.StableVersions.Reverse())
+ content.AppendLine($"* @changelog.v{version}");
+ content.AppendLine("* @changelog.full");
+
+ context.GenerateFile(DocfxDirectory.CombineWithFilePath("index.md"), content);
+ }
+
+ private void GenerateFull()
+ {
+ var content = new StringBuilder();
+ content.AppendLine("---");
+ content.AppendLine("uid: changelog.full");
+ content.AppendLine("---");
+ content.AppendLine("");
+ content.AppendLine("# Full ChangeLog");
+ content.AppendLine("");
+ if (preview)
+ content.AppendLine(
+ $"[!include[v{context.VersionHistory.CurrentVersion}](v{context.VersionHistory.CurrentVersion}.md)]");
+ foreach (var version in context.VersionHistory.StableVersions.Reverse())
+ content.AppendLine($"[!include[v{version}](v{version}.md)]");
+
+ context.GenerateFile(DocfxDirectory.CombineWithFilePath("full.md"), content);
+ }
+
+ private void GenerateToc()
+ {
+ var content = new StringBuilder();
+
+ if (preview)
+ {
+ content.AppendLine($"- name: v{context.VersionHistory.CurrentVersion}");
+ content.AppendLine($" href: v{context.VersionHistory.CurrentVersion}.md");
+ }
+
+ foreach (var version in context.VersionHistory.StableVersions.Reverse())
+ {
+ content.AppendLine($"- name: v{version}");
+ content.AppendLine($" href: v{version}.md");
+ }
+
+ content.AppendLine("- name: Full ChangeLog");
+ content.AppendLine(" href: full.md");
+
+ context.GenerateFile(DocfxDirectory.CombineWithFilePath("toc.yml"), content);
+ }
+
+ private void EnsureSrcDirectoryExist(bool forceClone = false)
+ {
+ void Log(string message) => context.Information($"[Changelog] {message}");
+
+ Log($"Preparing git sub-repository for changelog branch '{Repo.ChangelogBranch}'. " +
+ $"Target directory: '{SrcDirectory}'.");
+ if (context.DirectoryExists(SrcDirectory) && forceClone)
+ {
+ Log($"Directory '{SrcDirectory}' already exists and forceClean is specified. " +
+ $"Deleting the current directory...");
+ context.DeleteDirectory(
+ SrcDirectory,
+ new DeleteDirectorySettings { Force = true, Recursive = true });
+ Log($"Directory '{SrcDirectory}' deleted successfully.");
+ }
+
+ if (!context.DirectoryExists(SrcDirectory))
+ {
+ Log($"Cloning branch '{Repo.ChangelogBranch}' from '{Repo.HttpsGitUrl}' to '{SrcDirectory}'.");
+ context.GitRunner.Clone(SrcDirectory, Repo.HttpsGitUrl, Repo.ChangelogBranch);
+ Log($"Clone completed: '{Repo.ChangelogBranch}' -> '{SrcDirectory}'.");
+ }
+ else
+ {
+ Log($"Directory '{SrcDirectory}' already exists. Skipping clone.");
+ }
+ }
+}
\ No newline at end of file
diff --git a/build/BenchmarkDotNet.Build/ChangeLogBuilder.cs b/build/BenchmarkDotNet.Build/Runners/Changelog/ChangelogDetailsBuilder.cs
similarity index 93%
rename from build/BenchmarkDotNet.Build/ChangeLogBuilder.cs
rename to build/BenchmarkDotNet.Build/Runners/Changelog/ChangelogDetailsBuilder.cs
index e7769e1fbd..27540402e3 100644
--- a/build/BenchmarkDotNet.Build/ChangeLogBuilder.cs
+++ b/build/BenchmarkDotNet.Build/Runners/Changelog/ChangelogDetailsBuilder.cs
@@ -10,15 +10,15 @@
using Cake.Core.IO;
using Octokit;
-namespace BenchmarkDotNet.Build;
+namespace BenchmarkDotNet.Build.Runners.Changelog;
-public static class ChangeLogBuilder
+public static class ChangelogDetailsBuilder
{
- private class Config
+ private class Config(string currentVersion, string previousVersion, string lastCommit)
{
- public string CurrentVersion { get; }
- public string PreviousVersion { get; }
- public string LastCommit { get; }
+ public string CurrentVersion { get; } = currentVersion;
+ public string PreviousVersion { get; } = previousVersion;
+ public string LastCommit { get; } = lastCommit;
public void Deconstruct(out string currentMilestone, out string previousMilestone, out string lastCommit)
{
@@ -26,13 +26,6 @@ public void Deconstruct(out string currentMilestone, out string previousMileston
previousMilestone = PreviousVersion;
lastCommit = LastCommit;
}
-
- public Config(string currentVersion, string previousVersion, string lastCommit)
- {
- CurrentVersion = currentVersion;
- PreviousVersion = previousVersion;
- LastCommit = lastCommit;
- }
}
private class MarkdownBuilder
diff --git a/build/BenchmarkDotNet.Build/Runners/DocumentationRunner.cs b/build/BenchmarkDotNet.Build/Runners/DocumentationRunner.cs
index 2d2128a86f..e1d1af7469 100644
--- a/build/BenchmarkDotNet.Build/Runners/DocumentationRunner.cs
+++ b/build/BenchmarkDotNet.Build/Runners/DocumentationRunner.cs
@@ -1,11 +1,9 @@
-using System;
-using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using BenchmarkDotNet.Build.Helpers;
using BenchmarkDotNet.Build.Meta;
-using BenchmarkDotNet.Build.Options;
+using BenchmarkDotNet.Build.Runners.Changelog;
using Cake.Common.Diagnostics;
using Cake.Common.IO;
using Cake.Core.IO;
@@ -16,85 +14,47 @@ namespace BenchmarkDotNet.Build.Runners;
public class DocumentationRunner
{
private readonly BuildContext context;
- private readonly bool preview;
- private readonly string depth;
-
- public DirectoryPath ChangelogDirectory { get; }
- public DirectoryPath ChangelogSrcDirectory { get; }
- private readonly DirectoryPath changelogDetailsDirectory;
+ private readonly ChangelogBuilder changelogBuilder;
private readonly DirectoryPath docsGeneratedDirectory;
private readonly FilePath docfxJsonFile;
private readonly FilePath redirectFile;
private readonly FilePath readmeFile;
private readonly FilePath rootIndexFile;
- private readonly FilePath changelogIndexFile;
- private readonly FilePath changelogFullFile;
- private readonly FilePath changelogTocFile;
- private readonly FilePath lastFooterFile;
+
+ public DirectoryPath ChangelogSrcDirectory => changelogBuilder.SrcDirectory;
public DocumentationRunner(BuildContext context)
{
this.context = context;
- preview = KnownOptions.DocsPreview.Resolve(context);
- depth = KnownOptions.DocsDepth.Resolve(context);
+ changelogBuilder = new ChangelogBuilder(context);
var docsDirectory = context.RootDirectory.Combine("docs");
- ChangelogDirectory = docsDirectory.Combine("changelog");
- ChangelogSrcDirectory = docsDirectory.Combine("_changelog");
- changelogDetailsDirectory = ChangelogSrcDirectory.Combine("details");
docsGeneratedDirectory = docsDirectory.Combine("_site");
-
redirectFile = docsDirectory.Combine("_redirects").CombineWithFilePath("_redirects");
docfxJsonFile = docsDirectory.CombineWithFilePath("docfx.json");
readmeFile = context.RootDirectory.CombineWithFilePath("README.md");
rootIndexFile = docsDirectory.CombineWithFilePath("index.md");
- changelogIndexFile = ChangelogDirectory.CombineWithFilePath("index.md");
- changelogFullFile = ChangelogDirectory.CombineWithFilePath("full.md");
- changelogTocFile = ChangelogDirectory.CombineWithFilePath("toc.yml");
- lastFooterFile = ChangelogSrcDirectory.Combine("footer")
- .CombineWithFilePath("v" + context.VersionHistory.CurrentVersion + ".md");
}
- public void Update()
+ public void Fetch()
{
EnvVar.GitHubToken.AssertHasValue();
+ changelogBuilder.Fetch();
+ }
- UpdateReadme();
- UpdateLastFooter();
-
- EnsureChangelogDetailsExist();
-
- var history = context.VersionHistory;
- var stableVersionCount = history.StableVersions.Length;
-
- if (depth.Equals("all", StringComparison.OrdinalIgnoreCase))
- {
- DocfxChangelogDownload(
- history.StableVersions.First(),
- history.FirstCommit);
-
- for (var i = 1; i < stableVersionCount; i++)
- DocfxChangelogDownload(
- history.StableVersions[i],
- history.StableVersions[i - 1]);
- }
- else if (depth != "")
- {
- if (!int.TryParse(depth, CultureInfo.InvariantCulture, out var depthValue))
- throw new InvalidDataException($"Failed to parse the depth value: '{depth}'");
+ public void Generate()
+ {
+ changelogBuilder.Generate();
- for (var i = Math.Max(stableVersionCount - depthValue, 1); i < stableVersionCount; i++)
- DocfxChangelogDownload(
- history.StableVersions[i],
- history.StableVersions[i - 1]);
- }
+ UpdateReadme();
+ GenerateIndexMd();
+ }
- if (preview)
- DocfxChangelogDownload(
- history.CurrentVersion,
- history.StableVersions.Last(),
- "HEAD");
+ public void Build()
+ {
+ RunDocfx();
+ GenerateRedirects();
}
private void UpdateReadme()
@@ -108,24 +68,6 @@ private void UpdateReadme()
context.GenerateFile(context.ReadmeFile, content, true);
}
- public void Prepare()
- {
- foreach (var version in context.VersionHistory.StableVersions)
- DocfxChangelogGenerate(version);
- if (preview)
- DocfxChangelogGenerate(context.VersionHistory.CurrentVersion);
-
- GenerateIndexMd();
- GenerateChangelogIndex();
- GenerateChangelogFull();
- GenerateChangelogToc();
- }
-
- public void Build()
- {
- RunDocfx();
- GenerateRedirects();
- }
private void RunDocfx()
{
@@ -149,124 +91,6 @@ private void GenerateIndexMd()
context.GenerateFile(rootIndexFile, content);
}
- private void GenerateChangelogToc()
- {
- var content = new StringBuilder();
-
- if (preview)
- {
- content.AppendLine($"- name: v{context.VersionHistory.CurrentVersion}");
- content.AppendLine($" href: v{context.VersionHistory.CurrentVersion}.md");
- }
-
- foreach (var version in context.VersionHistory.StableVersions.Reverse())
- {
- content.AppendLine($"- name: v{version}");
- content.AppendLine($" href: v{version}.md");
- }
-
- content.AppendLine("- name: Full ChangeLog");
- content.AppendLine(" href: full.md");
-
- context.GenerateFile(changelogTocFile, content);
- }
-
- private void GenerateChangelogFull()
- {
- var content = new StringBuilder();
- content.AppendLine("---");
- content.AppendLine("uid: changelog.full");
- content.AppendLine("---");
- content.AppendLine("");
- content.AppendLine("# Full ChangeLog");
- content.AppendLine("");
- if (preview)
- content.AppendLine(
- $"[!include[v{context.VersionHistory.CurrentVersion}](v{context.VersionHistory.CurrentVersion}.md)]");
- foreach (var version in context.VersionHistory.StableVersions.Reverse())
- content.AppendLine($"[!include[v{version}](v{version}.md)]");
-
- context.GenerateFile(changelogFullFile, content);
- }
-
- private void GenerateChangelogIndex()
- {
- var content = new StringBuilder();
- content.AppendLine("---");
- content.AppendLine("uid: changelog");
- content.AppendLine("---");
- content.AppendLine("");
- content.AppendLine("# ChangeLog");
- content.AppendLine("");
- if (preview)
- content.AppendLine($"* @changelog.v{context.VersionHistory.CurrentVersion}");
- foreach (var version in context.VersionHistory.StableVersions.Reverse())
- content.AppendLine($"* @changelog.v{version}");
- content.AppendLine("* @changelog.full");
-
- context.GenerateFile(changelogIndexFile, content);
- }
-
- private void DocfxChangelogGenerate(string version)
- {
- EnsureChangelogDetailsExist();
- var md = $"v{version}.md";
- var header = ChangelogSrcDirectory.Combine("header").CombineWithFilePath(md);
- var footer = ChangelogSrcDirectory.Combine("footer").CombineWithFilePath(md);
- var details = ChangelogSrcDirectory.Combine("details").CombineWithFilePath(md);
- var release = ChangelogDirectory.CombineWithFilePath(md);
-
- var content = new StringBuilder();
- content.AppendLine("---");
- content.AppendLine("uid: changelog.v" + version);
- content.AppendLine("---");
- content.AppendLine("");
- content.AppendLine("# BenchmarkDotNet v" + version);
- content.AppendLine("");
- content.AppendLine("");
-
- if (context.FileExists(header))
- {
- content.AppendLine(context.FileReadText(header));
- content.AppendLine("");
- content.AppendLine("");
- }
-
- if (context.FileExists(details))
- {
- content.AppendLine(context.FileReadText(details));
- content.AppendLine("");
- content.AppendLine("");
- }
-
- if (context.FileExists(footer))
- {
- content.AppendLine("## Additional details");
- content.AppendLine("");
- content.AppendLine(context.FileReadText(footer));
- }
-
- context.GenerateFile(release, content.ToString());
- }
-
- private void EnsureChangelogDetailsExist(bool forceClean = false)
- {
- if (context.DirectoryExists(changelogDetailsDirectory) && forceClean)
- context.DeleteDirectory(
- changelogDetailsDirectory,
- new DeleteDirectorySettings { Force = true, Recursive = true });
-
- if (!context.DirectoryExists(changelogDetailsDirectory))
- context.GitRunner.Clone(changelogDetailsDirectory, Repo.HttpsGitUrl, Repo.ChangelogDetailsBranch);
- }
-
- private void DocfxChangelogDownload(string version, string versionPrevious, string lastCommit = "")
- {
- EnsureChangelogDetailsExist();
- context.Information($"Downloading changelog details for v{version}");
- ChangeLogBuilder.Run(context, changelogDetailsDirectory, version, versionPrevious, lastCommit);
- }
-
private void GenerateRedirects()
{
if (!context.FileExists(redirectFile))
@@ -300,27 +124,4 @@ private void GenerateRedirects()
context.GenerateFile(fullFilePath, content);
}
}
-
- private void UpdateLastFooter()
- {
- var version = context.VersionHistory.CurrentVersion;
- var previousVersion = context.VersionHistory.StableVersions.Last();
- var date = KnownOptions.Stable.Resolve(context)
- ? DateTime.Now.ToString("MMMM dd, yyyy", CultureInfo.InvariantCulture)
- : "TBA";
-
- var content = new StringBuilder();
- content.AppendLine($"_Date: {date}_");
- content.AppendLine("");
- content.AppendLine(
- $"_Milestone: [v{version}](https://github.com/dotnet/BenchmarkDotNet/issues?q=milestone%3Av{version})_");
- content.AppendLine(
- $"([List of commits](https://github.com/dotnet/BenchmarkDotNet/compare/v{previousVersion}...v{version}))");
- content.AppendLine("");
- content.AppendLine("_NuGet Packages:_");
- foreach (var packageName in context.NuGetPackageNames)
- content.AppendLine($"* https://www.nuget.org/packages/{packageName}/{version}");
-
- context.GenerateFile(lastFooterFile, content);
- }
}
\ No newline at end of file
diff --git a/build/BenchmarkDotNet.Build/Runners/ReleaseRunner.cs b/build/BenchmarkDotNet.Build/Runners/ReleaseRunner.cs
index fb9aa895ec..e9d1fcb9de 100644
--- a/build/BenchmarkDotNet.Build/Runners/ReleaseRunner.cs
+++ b/build/BenchmarkDotNet.Build/Runners/ReleaseRunner.cs
@@ -143,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..89b777d09f 100644
--- a/build/BenchmarkDotNet.Build/Runners/UnitTestRunner.cs
+++ b/build/BenchmarkDotNet.Build/Runners/UnitTestRunner.cs
@@ -4,31 +4,29 @@
using Cake.Common.Tools.DotNet;
using Cake.Common.Tools.DotNet.Test;
using Cake.Core.IO;
+using System.Runtime.InteropServices;
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)
{
@@ -50,7 +48,8 @@ private DotNetTestSettings GetTestSettingsParameters(FilePath logFile, string tf
private void RunTests(FilePath projectFile, string alias, string tfm)
{
var os = Utils.GetOs();
- var trxFileName = $"{os}-{alias}-{tfm}.trx";
+ var arch = RuntimeInformation.OSArchitecture.ToString().ToLower();
+ var trxFileName = $"{os}({arch})-{alias}-{tfm}.trx";
var trxFile = TestOutputDirectory.CombineWithFilePath(trxFileName);
var settings = GetTestSettingsParameters(trxFile, tfm);
@@ -58,14 +57,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..a34d415fe3 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"
@@ -36,8 +39,10 @@
"ignoreWords": [
"Akinshin",
"Andrey",
+ "Cassell",
"Expecto",
"Jint",
+ "JITted",
"LoongArch64",
"macrobenchmark",
"MediatR",
@@ -46,6 +51,9 @@
"NodaTime",
"Npgsql",
"Sitnik's",
+ "Sitnik",
+ "Stepanov",
+ "Yegor",
"Wojciech",
"Avalonia",
"Gitter"
diff --git a/build/common.props b/build/common.props
index a9bd13f812..6fc04d2938 100644
--- a/build/common.props
+++ b/build/common.props
@@ -23,24 +23,29 @@
true
annotations
+
+ true
-
+
$(MSBuildThisFileDirectory)strongNameKey.snk
true
+
+
+
true
- 11.0
+ 12.0
- 0.13.10
+ 0.15.4
@@ -56,12 +61,13 @@
-
+
all
runtime; build; native; contentfiles; analyzers
-
+
all
+ runtime; build; native; contentfiles; analyzers
diff --git a/build/sdk/global.json b/build/sdk/global.json
index c1e852daeb..be5c357f30 100644
--- a/build/sdk/global.json
+++ b/build/sdk/global.json
@@ -1,6 +1,6 @@
{
"sdk": {
- "version": "7.0.401",
+ "version": "8.0.410",
"rollForward": "disable"
}
}
diff --git a/build/versions.txt b/build/versions.txt
index e202cbc55a..4b22ce0ad0 100644
--- a/build/versions.txt
+++ b/build/versions.txt
@@ -54,4 +54,12 @@
0.13.7
0.13.8
0.13.9
-0.13.10
\ No newline at end of file
+0.13.10
+0.13.11
+0.13.12
+0.14.0
+0.15.0
+0.15.1
+0.15.2
+0.15.3
+0.15.4
\ No newline at end of file
diff --git a/docs/.gitignore b/docs/.gitignore
index aad3551319..ebe91edcfe 100644
--- a/docs/.gitignore
+++ b/docs/.gitignore
@@ -11,4 +11,6 @@ _exported_templates
docfx-bin
api/*
!api/index.md
-/index.md
\ No newline at end of file
+/index.md
+_changelog
+changelog
diff --git a/docs/_changelog/.gitignore b/docs/_changelog/.gitignore
deleted file mode 100644
index 3bc49a8a60..0000000000
--- a/docs/_changelog/.gitignore
+++ /dev/null
@@ -1 +0,0 @@
-details
diff --git a/docs/_changelog/footer/v0.10.0.md b/docs/_changelog/footer/v0.10.0.md
deleted file mode 100644
index 823ad6954c..0000000000
--- a/docs/_changelog/footer/v0.10.0.md
+++ /dev/null
@@ -1,11 +0,0 @@
-_Date: November 10, 2016_
-
-_Milestone: [v0.10.0](https://github.com/PerfDotNet/BenchmarkDotNet/issues?q=milestone%3Av0.10.0)_
-
-_NuGet Packages:_
-* https://www.nuget.org/packages/BenchmarkDotNet/0.10.0
-* https://www.nuget.org/packages/BenchmarkDotNet.Core/0.10.0
-* https://www.nuget.org/packages/BenchmarkDotNet.Toolchains.Roslyn/0.10.0
-* https://www.nuget.org/packages/BenchmarkDotNet.Diagnostics.Windows/0.10.0
-
-_Online Documentation:_ https://dotnet.github.io/BenchmarkDotNet/
\ No newline at end of file
diff --git a/docs/_changelog/footer/v0.10.1.md b/docs/_changelog/footer/v0.10.1.md
deleted file mode 100644
index 6a2f16d19d..0000000000
--- a/docs/_changelog/footer/v0.10.1.md
+++ /dev/null
@@ -1,9 +0,0 @@
-_Date: December 04, 2016_
-
-_Milestone: [v0.10.1](https://github.com/PerfDotNet/BenchmarkDotNet/issues?q=milestone%3Av0.10.1)_
-
-_NuGet Packages:_
-* https://www.nuget.org/packages/BenchmarkDotNet/0.10.1
-* https://www.nuget.org/packages/BenchmarkDotNet.Core/0.10.1
-* https://www.nuget.org/packages/BenchmarkDotNet.Toolchains.Roslyn/0.10.1
-* https://www.nuget.org/packages/BenchmarkDotNet.Diagnostics.Windows/0.10.1
\ No newline at end of file
diff --git a/docs/_changelog/footer/v0.10.10.md b/docs/_changelog/footer/v0.10.10.md
deleted file mode 100644
index 9dde0a1cc3..0000000000
--- a/docs/_changelog/footer/v0.10.10.md
+++ /dev/null
@@ -1,10 +0,0 @@
-_Date: November 03, 2017_
-
-_Milestone: [v0.10.10](https://github.com/PerfDotNet/BenchmarkDotNet/issues?q=milestone%3Av0.10.10)_
-([List of commits](https://github.com/dotnet/BenchmarkDotNet/compare/v0.10.9...v0.10.10))
-
-_NuGet Packages:_
-* https://www.nuget.org/packages/BenchmarkDotNet/0.10.10
-* https://www.nuget.org/packages/BenchmarkDotNet.Core/0.10.10
-* https://www.nuget.org/packages/BenchmarkDotNet.Toolchains.Roslyn/0.10.10
-* https://www.nuget.org/packages/BenchmarkDotNet.Diagnostics.Windows/0.10.10
\ No newline at end of file
diff --git a/docs/_changelog/footer/v0.10.11.md b/docs/_changelog/footer/v0.10.11.md
deleted file mode 100644
index 0de3715f93..0000000000
--- a/docs/_changelog/footer/v0.10.11.md
+++ /dev/null
@@ -1,10 +0,0 @@
-_Date: December 01, 2017_
-
-_Milestone: [v0.10.11](https://github.com/PerfDotNet/BenchmarkDotNet/issues?q=milestone%3Av0.10.11)_
-([List of commits](https://github.com/dotnet/BenchmarkDotNet/compare/v0.10.10...v0.10.11))
-
-_NuGet Packages:_
-* https://www.nuget.org/packages/BenchmarkDotNet/0.10.11
-* https://www.nuget.org/packages/BenchmarkDotNet.Core/0.10.11
-* https://www.nuget.org/packages/BenchmarkDotNet.Toolchains.Roslyn/0.10.11
-* https://www.nuget.org/packages/BenchmarkDotNet.Diagnostics.Windows/0.10.11
\ No newline at end of file
diff --git a/docs/_changelog/footer/v0.10.12.md b/docs/_changelog/footer/v0.10.12.md
deleted file mode 100644
index 3106385452..0000000000
--- a/docs/_changelog/footer/v0.10.12.md
+++ /dev/null
@@ -1,10 +0,0 @@
-_Date: January 15, 2018_
-
-_Milestone: [v0.10.12](https://github.com/PerfDotNet/BenchmarkDotNet/issues?q=milestone%3Av0.10.12)_
-([List of commits](https://github.com/dotnet/BenchmarkDotNet/compare/v0.10.11...v0.10.12))
-
-_NuGet Packages:_
-* https://www.nuget.org/packages/BenchmarkDotNet/0.10.12
-* https://www.nuget.org/packages/BenchmarkDotNet.Core/0.10.12
-* https://www.nuget.org/packages/BenchmarkDotNet.Toolchains.Roslyn/0.10.12
-* https://www.nuget.org/packages/BenchmarkDotNet.Diagnostics.Windows/0.10.12
\ No newline at end of file
diff --git a/docs/_changelog/footer/v0.10.13.md b/docs/_changelog/footer/v0.10.13.md
deleted file mode 100644
index 6eb70e1fc2..0000000000
--- a/docs/_changelog/footer/v0.10.13.md
+++ /dev/null
@@ -1,10 +0,0 @@
-_Date: March 02, 2018_
-
-_Milestone: [v0.10.13](https://github.com/PerfDotNet/BenchmarkDotNet/issues?q=milestone%3Av0.10.13)_
-([List of commits](https://github.com/dotnet/BenchmarkDotNet/compare/v0.10.12...v0.10.13))
-
-_NuGet Packages:_
-* https://www.nuget.org/packages/BenchmarkDotNet/0.10.13
-* https://www.nuget.org/packages/BenchmarkDotNet.Core/0.10.13
-* https://www.nuget.org/packages/BenchmarkDotNet.Toolchains.Roslyn/0.10.13
-* https://www.nuget.org/packages/BenchmarkDotNet.Diagnostics.Windows/0.10.13
\ No newline at end of file
diff --git a/docs/_changelog/footer/v0.10.14.md b/docs/_changelog/footer/v0.10.14.md
deleted file mode 100644
index 2fd3ea5542..0000000000
--- a/docs/_changelog/footer/v0.10.14.md
+++ /dev/null
@@ -1,10 +0,0 @@
-_Date: April 09, 2018_
-
-_Milestone: [v0.10.14](https://github.com/PerfDotNet/BenchmarkDotNet/issues?q=milestone%3Av0.10.14)_
-([List of commits](https://github.com/dotnet/BenchmarkDotNet/compare/v0.10.13...v0.10.14))
-
-_NuGet Packages:_
-* https://www.nuget.org/packages/BenchmarkDotNet/0.10.14
-* https://www.nuget.org/packages/BenchmarkDotNet.Core/0.10.14
-* https://www.nuget.org/packages/BenchmarkDotNet.Toolchains.Roslyn/0.10.14
-* https://www.nuget.org/packages/BenchmarkDotNet.Diagnostics.Windows/0.10.14
\ No newline at end of file
diff --git a/docs/_changelog/footer/v0.10.2.md b/docs/_changelog/footer/v0.10.2.md
deleted file mode 100644
index 46e50a038f..0000000000
--- a/docs/_changelog/footer/v0.10.2.md
+++ /dev/null
@@ -1,9 +0,0 @@
-_Date: January 21, 2017_
-
-_Milestone: [v0.10.2](https://github.com/PerfDotNet/BenchmarkDotNet/issues?q=milestone%3Av0.10.2)_
-
-_NuGet Packages:_
-* https://www.nuget.org/packages/BenchmarkDotNet/0.10.2
-* https://www.nuget.org/packages/BenchmarkDotNet.Core/0.10.2
-* https://www.nuget.org/packages/BenchmarkDotNet.Toolchains.Roslyn/0.10.2
-* https://www.nuget.org/packages/BenchmarkDotNet.Diagnostics.Windows/0.10.2
\ No newline at end of file
diff --git a/docs/_changelog/footer/v0.10.3.md b/docs/_changelog/footer/v0.10.3.md
deleted file mode 100644
index 623439e439..0000000000
--- a/docs/_changelog/footer/v0.10.3.md
+++ /dev/null
@@ -1,9 +0,0 @@
-_Date: March 01, 2017_
-
-_Milestone: [v0.10.3](https://github.com/PerfDotNet/BenchmarkDotNet/issues?q=milestone%3Av0.10.3)_
-
-_NuGet Packages:_
-* https://www.nuget.org/packages/BenchmarkDotNet/0.10.3
-* https://www.nuget.org/packages/BenchmarkDotNet.Core/0.10.3
-* https://www.nuget.org/packages/BenchmarkDotNet.Toolchains.Roslyn/0.10.3
-* https://www.nuget.org/packages/BenchmarkDotNet.Diagnostics.Windows/0.10.3
\ No newline at end of file
diff --git a/docs/_changelog/footer/v0.10.4.md b/docs/_changelog/footer/v0.10.4.md
deleted file mode 100644
index 9f497b878b..0000000000
--- a/docs/_changelog/footer/v0.10.4.md
+++ /dev/null
@@ -1,9 +0,0 @@
-_Date: April 21, 2017_
-
-_Milestone: [v0.10.4](https://github.com/PerfDotNet/BenchmarkDotNet/issues?q=milestone%3Av0.10.4)_
-
-_NuGet Packages:_
-* https://www.nuget.org/packages/BenchmarkDotNet/0.10.4
-* https://www.nuget.org/packages/BenchmarkDotNet.Core/0.10.4
-* https://www.nuget.org/packages/BenchmarkDotNet.Toolchains.Roslyn/0.10.4
-* https://www.nuget.org/packages/BenchmarkDotNet.Diagnostics.Windows/0.10.4
\ No newline at end of file
diff --git a/docs/_changelog/footer/v0.10.5.md b/docs/_changelog/footer/v0.10.5.md
deleted file mode 100644
index b4d8d5b914..0000000000
--- a/docs/_changelog/footer/v0.10.5.md
+++ /dev/null
@@ -1,9 +0,0 @@
-_Date: April 26, 2017_
-
-_Milestone: [v0.10.5](https://github.com/PerfDotNet/BenchmarkDotNet/issues?q=milestone%3Av0.10.5)_
-
-_NuGet Packages:_
-* https://www.nuget.org/packages/BenchmarkDotNet/0.10.5
-* https://www.nuget.org/packages/BenchmarkDotNet.Core/0.10.5
-* https://www.nuget.org/packages/BenchmarkDotNet.Toolchains.Roslyn/0.10.5
-* https://www.nuget.org/packages/BenchmarkDotNet.Diagnostics.Windows/0.10.5
\ No newline at end of file
diff --git a/docs/_changelog/footer/v0.10.6.md b/docs/_changelog/footer/v0.10.6.md
deleted file mode 100644
index b29dfc3939..0000000000
--- a/docs/_changelog/footer/v0.10.6.md
+++ /dev/null
@@ -1,9 +0,0 @@
-_Date: May 12, 2017_
-
-_Milestone: [v0.10.6](https://github.com/PerfDotNet/BenchmarkDotNet/issues?q=milestone%3Av0.10.6)_
-
-_NuGet Packages:_
-* https://www.nuget.org/packages/BenchmarkDotNet/0.10.6
-* https://www.nuget.org/packages/BenchmarkDotNet.Core/0.10.6
-* https://www.nuget.org/packages/BenchmarkDotNet.Toolchains.Roslyn/0.10.6
-* https://www.nuget.org/packages/BenchmarkDotNet.Diagnostics.Windows/0.10.6
\ No newline at end of file
diff --git a/docs/_changelog/footer/v0.10.7.md b/docs/_changelog/footer/v0.10.7.md
deleted file mode 100644
index 5ee21fee21..0000000000
--- a/docs/_changelog/footer/v0.10.7.md
+++ /dev/null
@@ -1,11 +0,0 @@
-_Date: June 05, 2017_
-
-_Milestone: [v0.10.7](https://github.com/PerfDotNet/BenchmarkDotNet/issues?q=milestone%3Av0.10.7)_
-
-_Overview post: https://aakinshin.net/posts/bdn-v0_10_7/_
-
-_NuGet Packages:_
-* https://www.nuget.org/packages/BenchmarkDotNet/0.10.7
-* https://www.nuget.org/packages/BenchmarkDotNet.Core/0.10.7
-* https://www.nuget.org/packages/BenchmarkDotNet.Toolchains.Roslyn/0.10.7
-* https://www.nuget.org/packages/BenchmarkDotNet.Diagnostics.Windows/0.10.7
\ No newline at end of file
diff --git a/docs/_changelog/footer/v0.10.8.md b/docs/_changelog/footer/v0.10.8.md
deleted file mode 100644
index 791022d425..0000000000
--- a/docs/_changelog/footer/v0.10.8.md
+++ /dev/null
@@ -1,9 +0,0 @@
-_Date: June 09, 2017_
-
-_Milestone: [v0.10.8](https://github.com/PerfDotNet/BenchmarkDotNet/issues?q=milestone%3Av0.10.8)_
-
-_NuGet Packages:_
-* https://www.nuget.org/packages/BenchmarkDotNet/0.10.8
-* https://www.nuget.org/packages/BenchmarkDotNet.Core/0.10.8
-* https://www.nuget.org/packages/BenchmarkDotNet.Toolchains.Roslyn/0.10.8
-* https://www.nuget.org/packages/BenchmarkDotNet.Diagnostics.Windows/0.10.8
\ No newline at end of file
diff --git a/docs/_changelog/footer/v0.10.9.md b/docs/_changelog/footer/v0.10.9.md
deleted file mode 100644
index 8fc3fef03e..0000000000
--- a/docs/_changelog/footer/v0.10.9.md
+++ /dev/null
@@ -1,9 +0,0 @@
-_Date: July 28, 2017_
-
-_Milestone: [v0.10.9](https://github.com/PerfDotNet/BenchmarkDotNet/issues?q=milestone%3Av0.10.9)_
-
-_NuGet Packages:_
-* https://www.nuget.org/packages/BenchmarkDotNet/0.10.9
-* https://www.nuget.org/packages/BenchmarkDotNet.Core/0.10.9
-* https://www.nuget.org/packages/BenchmarkDotNet.Toolchains.Roslyn/0.10.9
-* https://www.nuget.org/packages/BenchmarkDotNet.Diagnostics.Windows/0.10.9
\ No newline at end of file
diff --git a/docs/_changelog/footer/v0.11.0.md b/docs/_changelog/footer/v0.11.0.md
deleted file mode 100644
index f9637a79e6..0000000000
--- a/docs/_changelog/footer/v0.11.0.md
+++ /dev/null
@@ -1,8 +0,0 @@
-_Date: July 23, 2018_
-
-_Milestone: [v0.11.0](https://github.com/PerfDotNet/BenchmarkDotNet/issues?q=milestone%3Av0.11.0)_
-([List of commits](https://github.com/dotnet/BenchmarkDotNet/compare/v0.10.14...v0.11.0))
-
-_NuGet Packages:_
-* https://www.nuget.org/packages/BenchmarkDotNet/0.11.0
-* https://www.nuget.org/packages/BenchmarkDotNet.Diagnostics.Windows/0.11.0
\ No newline at end of file
diff --git a/docs/_changelog/footer/v0.11.1.md b/docs/_changelog/footer/v0.11.1.md
deleted file mode 100644
index f6c73f9510..0000000000
--- a/docs/_changelog/footer/v0.11.1.md
+++ /dev/null
@@ -1,8 +0,0 @@
-_Date: August 22, 2018_
-
-_Milestone: [v0.11.1](https://github.com/PerfDotNet/BenchmarkDotNet/issues?q=milestone%3Av0.11.1)_
-([List of commits](https://github.com/dotnet/BenchmarkDotNet/compare/v0.11.0...v0.11.1))
-
-_NuGet Packages:_
-* https://www.nuget.org/packages/BenchmarkDotNet/0.11.1
-* https://www.nuget.org/packages/BenchmarkDotNet.Diagnostics.Windows/0.11.1
\ No newline at end of file
diff --git a/docs/_changelog/footer/v0.11.2.md b/docs/_changelog/footer/v0.11.2.md
deleted file mode 100644
index 338170b8ae..0000000000
--- a/docs/_changelog/footer/v0.11.2.md
+++ /dev/null
@@ -1,8 +0,0 @@
-_Date: November 1, 2018_
-
-_Milestone: [v0.11.2](https://github.com/PerfDotNet/BenchmarkDotNet/issues?q=milestone%3Av0.11.2)_
-([List of commits](https://github.com/dotnet/BenchmarkDotNet/compare/v0.11.1...v0.11.2))
-
-_NuGet Packages:_
-* https://www.nuget.org/packages/BenchmarkDotNet/0.11.2
-* https://www.nuget.org/packages/BenchmarkDotNet.Diagnostics.Windows/0.11.2
\ No newline at end of file
diff --git a/docs/_changelog/footer/v0.11.3.md b/docs/_changelog/footer/v0.11.3.md
deleted file mode 100644
index befc032994..0000000000
--- a/docs/_changelog/footer/v0.11.3.md
+++ /dev/null
@@ -1,8 +0,0 @@
-_Date: November 20, 2018_
-
-_Milestone: [v0.11.3](https://github.com/PerfDotNet/BenchmarkDotNet/issues?q=milestone%3Av0.11.3)_
-([List of commits](https://github.com/dotnet/BenchmarkDotNet/compare/v0.11.2...v0.11.3))
-
-_NuGet Packages:_
-* https://www.nuget.org/packages/BenchmarkDotNet/0.11.3
-* https://www.nuget.org/packages/BenchmarkDotNet.Diagnostics.Windows/0.11.3
\ No newline at end of file
diff --git a/docs/_changelog/footer/v0.11.4.md b/docs/_changelog/footer/v0.11.4.md
deleted file mode 100644
index 5cbac89988..0000000000
--- a/docs/_changelog/footer/v0.11.4.md
+++ /dev/null
@@ -1,9 +0,0 @@
-_Date: February 15, 2019_
-
-_Milestone: [v0.11.4](https://github.com/PerfDotNet/BenchmarkDotNet/issues?q=milestone%3Av0.11.4)_
-([List of commits](https://github.com/dotnet/BenchmarkDotNet/compare/v0.11.3...v0.11.4))
-
-_NuGet Packages:_
-* https://www.nuget.org/packages/BenchmarkDotNet/0.11.4
-* https://www.nuget.org/packages/BenchmarkDotNet.Diagnostics.Windows/0.11.4
-* https://www.nuget.org/packages/BenchmarkDotNet.Tool/0.11.4
\ No newline at end of file
diff --git a/docs/_changelog/footer/v0.11.5.md b/docs/_changelog/footer/v0.11.5.md
deleted file mode 100644
index be3282de78..0000000000
--- a/docs/_changelog/footer/v0.11.5.md
+++ /dev/null
@@ -1,10 +0,0 @@
-_Date: April 2, 2019_
-
-_Milestone: [v0.11.5](https://github.com/dotnet/BenchmarkDotNet/issues?q=milestone%3Av0.11.5)_
-([List of commits](https://github.com/dotnet/BenchmarkDotNet/compare/v0.11.4...v0.11.5))
-
-_NuGet Packages:_
-* https://www.nuget.org/packages/BenchmarkDotNet/0.11.5
-* https://www.nuget.org/packages/BenchmarkDotNet.Diagnostics.Windows/0.11.5
-* https://www.nuget.org/packages/BenchmarkDotNet.Tool/0.11.5
-* https://www.nuget.org/packages/BenchmarkDotNet.Annotations/0.11.5
\ No newline at end of file
diff --git a/docs/_changelog/footer/v0.12.0.md b/docs/_changelog/footer/v0.12.0.md
deleted file mode 100644
index 565429ca6a..0000000000
--- a/docs/_changelog/footer/v0.12.0.md
+++ /dev/null
@@ -1,11 +0,0 @@
-_Date: October 24, 2019_
-
-_Milestone: [v0.12.0](https://github.com/dotnet/BenchmarkDotNet/issues?q=milestone%3Av0.12.0)_
-([List of commits](https://github.com/dotnet/BenchmarkDotNet/compare/v0.11.5...v0.12.0))
-
-_NuGet Packages:_
-* https://www.nuget.org/packages/BenchmarkDotNet/0.12.0
-* https://www.nuget.org/packages/BenchmarkDotNet.Diagnostics.Windows/0.12.0
-* https://www.nuget.org/packages/BenchmarkDotNet.Tool/0.12.0
-* https://www.nuget.org/packages/BenchmarkDotNet.Annotations/0.12.0
-* https://www.nuget.org/packages/BenchmarkDotNet.Templates/0.12.0
\ No newline at end of file
diff --git a/docs/_changelog/footer/v0.12.1.md b/docs/_changelog/footer/v0.12.1.md
deleted file mode 100644
index af5fca9dc4..0000000000
--- a/docs/_changelog/footer/v0.12.1.md
+++ /dev/null
@@ -1,11 +0,0 @@
-_Date: April 6, 2020_
-
-_Milestone: [v0.12.1](https://github.com/dotnet/BenchmarkDotNet/issues?q=milestone%3Av0.12.1)_
-([List of commits](https://github.com/dotnet/BenchmarkDotNet/compare/v0.12.0...v0.12.1))
-
-_NuGet Packages:_
-* https://www.nuget.org/packages/BenchmarkDotNet/0.12.1
-* https://www.nuget.org/packages/BenchmarkDotNet.Diagnostics.Windows/0.12.1
-* https://www.nuget.org/packages/BenchmarkDotNet.Tool/0.12.1
-* https://www.nuget.org/packages/BenchmarkDotNet.Annotations/0.12.1
-* https://www.nuget.org/packages/BenchmarkDotNet.Templates/0.12.1
\ No newline at end of file
diff --git a/docs/_changelog/footer/v0.13.0.md b/docs/_changelog/footer/v0.13.0.md
deleted file mode 100644
index 7a8ed327bc..0000000000
--- a/docs/_changelog/footer/v0.13.0.md
+++ /dev/null
@@ -1,10 +0,0 @@
-_Date: May 19, 2021_
-
-_Milestone: [v0.13.0](https://github.com/dotnet/BenchmarkDotNet/issues?q=milestone%3Av0.13.0)_
-([List of commits](https://github.com/dotnet/BenchmarkDotNet/compare/v0.12.1...v0.13.0))
-
-_NuGet Packages:_
-* https://www.nuget.org/packages/BenchmarkDotNet/0.13.0
-* https://www.nuget.org/packages/BenchmarkDotNet.Diagnostics.Windows/0.13.0
-* https://www.nuget.org/packages/BenchmarkDotNet.Annotations/0.13.0
-* https://www.nuget.org/packages/BenchmarkDotNet.Templates/0.13.0
\ No newline at end of file
diff --git a/docs/_changelog/footer/v0.13.1.md b/docs/_changelog/footer/v0.13.1.md
deleted file mode 100644
index eb71d8729f..0000000000
--- a/docs/_changelog/footer/v0.13.1.md
+++ /dev/null
@@ -1,10 +0,0 @@
-_Date: August 11, 2021_
-
-_Milestone: [v0.13.1](https://github.com/dotnet/BenchmarkDotNet/issues?q=milestone%3Av0.13.1)_
-([List of commits](https://github.com/dotnet/BenchmarkDotNet/compare/v0.13.0...v0.13.1))
-
-_NuGet Packages:_
-* https://www.nuget.org/packages/BenchmarkDotNet/0.13.1
-* https://www.nuget.org/packages/BenchmarkDotNet.Diagnostics.Windows/0.13.1
-* https://www.nuget.org/packages/BenchmarkDotNet.Annotations/0.13.1
-* https://www.nuget.org/packages/BenchmarkDotNet.Templates/0.13.1
\ No newline at end of file
diff --git a/docs/_changelog/footer/v0.13.10.md b/docs/_changelog/footer/v0.13.10.md
deleted file mode 100644
index 68ef4fd0db..0000000000
--- a/docs/_changelog/footer/v0.13.10.md
+++ /dev/null
@@ -1,11 +0,0 @@
-_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.2.md b/docs/_changelog/footer/v0.13.2.md
deleted file mode 100644
index c9022e590e..0000000000
--- a/docs/_changelog/footer/v0.13.2.md
+++ /dev/null
@@ -1,10 +0,0 @@
-_Date: August 26, 2022_
-
-_Milestone: [v0.13.2](https://github.com/dotnet/BenchmarkDotNet/issues?q=milestone%3Av0.13.2)_
-([List of commits](https://github.com/dotnet/BenchmarkDotNet/compare/v0.13.1...v0.13.2))
-
-_NuGet Packages:_
-* https://www.nuget.org/packages/BenchmarkDotNet/0.13.2
-* https://www.nuget.org/packages/BenchmarkDotNet.Diagnostics.Windows/0.13.2
-* https://www.nuget.org/packages/BenchmarkDotNet.Annotations/0.13.2
-* https://www.nuget.org/packages/BenchmarkDotNet.Templates/0.13.2
\ No newline at end of file
diff --git a/docs/_changelog/footer/v0.13.3.md b/docs/_changelog/footer/v0.13.3.md
deleted file mode 100644
index f0fa6dff23..0000000000
--- a/docs/_changelog/footer/v0.13.3.md
+++ /dev/null
@@ -1,10 +0,0 @@
-_Date: December 26, 2022_
-
-_Milestone: [v0.13.3](https://github.com/dotnet/BenchmarkDotNet/issues?q=milestone%3Av0.13.3)_
-([List of commits](https://github.com/dotnet/BenchmarkDotNet/compare/v0.13.2...v0.13.3))
-
-_NuGet Packages:_
-* https://www.nuget.org/packages/BenchmarkDotNet/0.13.3
-* https://www.nuget.org/packages/BenchmarkDotNet.Diagnostics.Windows/0.13.3
-* https://www.nuget.org/packages/BenchmarkDotNet.Annotations/0.13.3
-* https://www.nuget.org/packages/BenchmarkDotNet.Templates/0.13.3
\ No newline at end of file
diff --git a/docs/_changelog/footer/v0.13.4.md b/docs/_changelog/footer/v0.13.4.md
deleted file mode 100644
index b6ac2ad6af..0000000000
--- a/docs/_changelog/footer/v0.13.4.md
+++ /dev/null
@@ -1,10 +0,0 @@
-_Date: January 13, 2023_
-
-_Milestone: [v0.13.4](https://github.com/dotnet/BenchmarkDotNet/issues?q=milestone%3Av0.13.4)_
-([List of commits](https://github.com/dotnet/BenchmarkDotNet/compare/v0.13.3...v0.13.4))
-
-_NuGet Packages:_
-* https://www.nuget.org/packages/BenchmarkDotNet/0.13.4
-* https://www.nuget.org/packages/BenchmarkDotNet.Diagnostics.Windows/0.13.4
-* https://www.nuget.org/packages/BenchmarkDotNet.Annotations/0.13.4
-* https://www.nuget.org/packages/BenchmarkDotNet.Templates/0.13.4
\ No newline at end of file
diff --git a/docs/_changelog/footer/v0.13.5.md b/docs/_changelog/footer/v0.13.5.md
deleted file mode 100644
index 577eeaea84..0000000000
--- a/docs/_changelog/footer/v0.13.5.md
+++ /dev/null
@@ -1,10 +0,0 @@
-_Date: February 17, 2023_
-
-_Milestone: [v0.13.5](https://github.com/dotnet/BenchmarkDotNet/issues?q=milestone%3Av0.13.5)_
-([List of commits](https://github.com/dotnet/BenchmarkDotNet/compare/v0.13.4...v0.13.5))
-
-_NuGet Packages:_
-* https://www.nuget.org/packages/BenchmarkDotNet/0.13.5
-* https://www.nuget.org/packages/BenchmarkDotNet.Diagnostics.Windows/0.13.5
-* https://www.nuget.org/packages/BenchmarkDotNet.Annotations/0.13.5
-* https://www.nuget.org/packages/BenchmarkDotNet.Templates/0.13.5
\ No newline at end of file
diff --git a/docs/_changelog/footer/v0.13.6.md b/docs/_changelog/footer/v0.13.6.md
deleted file mode 100644
index 7235cd2302..0000000000
--- a/docs/_changelog/footer/v0.13.6.md
+++ /dev/null
@@ -1,11 +0,0 @@
-_Date: July 11, 2023_
-
-_Milestone: [v0.13.6](https://github.com/dotnet/BenchmarkDotNet/issues?q=milestone%3Av0.13.6)_
-([List of commits](https://github.com/dotnet/BenchmarkDotNet/compare/v0.13.5...v0.13.6))
-
-_NuGet Packages:_
-* https://www.nuget.org/packages/BenchmarkDotNet/0.13.6
-* https://www.nuget.org/packages/BenchmarkDotNet.Annotations/0.13.6
-* https://www.nuget.org/packages/BenchmarkDotNet.Diagnostics.dotTrace/0.13.6
-* https://www.nuget.org/packages/BenchmarkDotNet.Diagnostics.Windows/0.13.6
-* https://www.nuget.org/packages/BenchmarkDotNet.Templates/0.13.6
diff --git a/docs/_changelog/footer/v0.13.7.md b/docs/_changelog/footer/v0.13.7.md
deleted file mode 100644
index 1b256837d4..0000000000
--- a/docs/_changelog/footer/v0.13.7.md
+++ /dev/null
@@ -1,11 +0,0 @@
-_Date: August 04, 2023_
-
-_Milestone: [v0.13.7](https://github.com/dotnet/BenchmarkDotNet/issues?q=milestone%3Av0.13.7)_
-([List of commits](https://github.com/dotnet/BenchmarkDotNet/compare/v0.13.6...v0.13.7))
-
-_NuGet Packages:_
-* https://www.nuget.org/packages/BenchmarkDotNet/0.13.7
-* https://www.nuget.org/packages/BenchmarkDotNet.Annotations/0.13.7
-* https://www.nuget.org/packages/BenchmarkDotNet.Diagnostics.dotTrace/0.13.7
-* https://www.nuget.org/packages/BenchmarkDotNet.Diagnostics.Windows/0.13.7
-* https://www.nuget.org/packages/BenchmarkDotNet.Templates/0.13.7
diff --git a/docs/_changelog/footer/v0.13.8.md b/docs/_changelog/footer/v0.13.8.md
deleted file mode 100644
index a8551cc47f..0000000000
--- a/docs/_changelog/footer/v0.13.8.md
+++ /dev/null
@@ -1,11 +0,0 @@
-_Date: September 08, 2023_
-
-_Milestone: [v0.13.8](https://github.com/dotnet/BenchmarkDotNet/issues?q=milestone%3Av0.13.8)_
-([List of commits](https://github.com/dotnet/BenchmarkDotNet/compare/v0.13.7...v0.13.8))
-
-_NuGet Packages:_
-* https://www.nuget.org/packages/BenchmarkDotNet/0.13.8
-* https://www.nuget.org/packages/BenchmarkDotNet.Annotations/0.13.8
-* https://www.nuget.org/packages/BenchmarkDotNet.Diagnostics.dotTrace/0.13.8
-* https://www.nuget.org/packages/BenchmarkDotNet.Diagnostics.Windows/0.13.8
-* https://www.nuget.org/packages/BenchmarkDotNet.Templates/0.13.8
diff --git a/docs/_changelog/footer/v0.13.9.md b/docs/_changelog/footer/v0.13.9.md
deleted file mode 100644
index 962bc252cc..0000000000
--- a/docs/_changelog/footer/v0.13.9.md
+++ /dev/null
@@ -1,11 +0,0 @@
-_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.8.2.md b/docs/_changelog/footer/v0.8.2.md
deleted file mode 100644
index cbd405d3ae..0000000000
--- a/docs/_changelog/footer/v0.8.2.md
+++ /dev/null
@@ -1,4 +0,0 @@
-_Date: January 19, 2016_
-
-_NuGet Packages:_
-* https://www.nuget.org/packages/BenchmarkDotNet/0.8.2
\ No newline at end of file
diff --git a/docs/_changelog/footer/v0.9.0.md b/docs/_changelog/footer/v0.9.0.md
deleted file mode 100644
index 55346f6951..0000000000
--- a/docs/_changelog/footer/v0.9.0.md
+++ /dev/null
@@ -1,4 +0,0 @@
-_Date: February 9, 2016_
-
-_NuGet Packages:_
-* https://www.nuget.org/packages/BenchmarkDotNet/0.9.0
\ No newline at end of file
diff --git a/docs/_changelog/footer/v0.9.1.md b/docs/_changelog/footer/v0.9.1.md
deleted file mode 100644
index b38f8a6436..0000000000
--- a/docs/_changelog/footer/v0.9.1.md
+++ /dev/null
@@ -1,4 +0,0 @@
-_Date: February 10, 2016_
-
-_NuGet Packages:_
-* https://www.nuget.org/packages/BenchmarkDotNet/0.9.1
\ No newline at end of file
diff --git a/docs/_changelog/footer/v0.9.2.md b/docs/_changelog/footer/v0.9.2.md
deleted file mode 100644
index 73c081a4cd..0000000000
--- a/docs/_changelog/footer/v0.9.2.md
+++ /dev/null
@@ -1,6 +0,0 @@
-_Milestone: [v0.9.2](https://github.com/PerfDotNet/BenchmarkDotNet/issues?q=milestone%3Av0.9.2)_
-
-_Date: March 5, 2016_
-
-_NuGet Packages:_
-* https://www.nuget.org/packages/BenchmarkDotNet/0.9.2
\ No newline at end of file
diff --git a/docs/_changelog/footer/v0.9.3.md b/docs/_changelog/footer/v0.9.3.md
deleted file mode 100644
index da9b45b165..0000000000
--- a/docs/_changelog/footer/v0.9.3.md
+++ /dev/null
@@ -1,7 +0,0 @@
-_Milestone: [v0.9.3](https://github.com/PerfDotNet/BenchmarkDotNet/issues?q=milestone%3Av0.9.3)_
-
-_Date: March 13, 2016_
-
-_NuGet Packages:_
-* https://www.nuget.org/packages/BenchmarkDotNet/0.9.3
-* https://www.nuget.org/packages/BenchmarkDotNet/0.9.3-beta
\ No newline at end of file
diff --git a/docs/_changelog/footer/v0.9.4.md b/docs/_changelog/footer/v0.9.4.md
deleted file mode 100644
index 0927d0a62b..0000000000
--- a/docs/_changelog/footer/v0.9.4.md
+++ /dev/null
@@ -1,7 +0,0 @@
-_Milestone: [v0.9.4](https://github.com/PerfDotNet/BenchmarkDotNet/issues?q=milestone%3Av0.9.4)_
-
-_Date: March 24, 2016_
-
-_NuGet Packages:_
-* https://www.nuget.org/packages/BenchmarkDotNet/0.9.4
-* https://www.nuget.org/packages/BenchmarkDotNet/0.9.4-beta
\ No newline at end of file
diff --git a/docs/_changelog/footer/v0.9.5.md b/docs/_changelog/footer/v0.9.5.md
deleted file mode 100644
index ea7d920994..0000000000
--- a/docs/_changelog/footer/v0.9.5.md
+++ /dev/null
@@ -1,7 +0,0 @@
-_Milestone: [v0.9.5](https://github.com/PerfDotNet/BenchmarkDotNet/issues?q=milestone%3Av0.9.5)_
-
-_Date: May 02, 2016_
-
-_NuGet Packages:_
-* https://www.nuget.org/packages/BenchmarkDotNet/0.9.5
-* https://www.nuget.org/packages/BenchmarkDotNet/0.9.5-beta
\ No newline at end of file
diff --git a/docs/_changelog/footer/v0.9.6.md b/docs/_changelog/footer/v0.9.6.md
deleted file mode 100644
index cc71d2d927..0000000000
--- a/docs/_changelog/footer/v0.9.6.md
+++ /dev/null
@@ -1,8 +0,0 @@
-_Milestone: [v0.9.6](https://github.com/PerfDotNet/BenchmarkDotNet/issues?q=milestone%3Av0.9.6)_
-
-_Date: May 11, 2016_
-
-_NuGet Packages:_
-* https://www.nuget.org/packages/BenchmarkDotNet/0.9.6
-* https://www.nuget.org/packages/BenchmarkDotNet/0.9.6-beta
-* https://www.nuget.org/packages/BenchmarkDotNet.Diagnostics.Windows/0.9.6
\ No newline at end of file
diff --git a/docs/_changelog/footer/v0.9.7.md b/docs/_changelog/footer/v0.9.7.md
deleted file mode 100644
index 21f1d109e5..0000000000
--- a/docs/_changelog/footer/v0.9.7.md
+++ /dev/null
@@ -1,8 +0,0 @@
-_Milestone: [v0.9.7](https://github.com/PerfDotNet/BenchmarkDotNet/issues?q=milestone%3Av0.9.7)_
-
-_Date: May 29, 2016_
-
-_NuGet Packages:_
-* https://www.nuget.org/packages/BenchmarkDotNet/0.9.7
-* https://www.nuget.org/packages/BenchmarkDotNet/0.9.7-beta
-* https://www.nuget.org/packages/BenchmarkDotNet.Diagnostics.Windows/0.9.7
\ No newline at end of file
diff --git a/docs/_changelog/footer/v0.9.8.md b/docs/_changelog/footer/v0.9.8.md
deleted file mode 100644
index fa5bc33931..0000000000
--- a/docs/_changelog/footer/v0.9.8.md
+++ /dev/null
@@ -1,7 +0,0 @@
-_Milestone: [v0.9.8](https://github.com/PerfDotNet/BenchmarkDotNet/issues?q=milestone%3Av0.9.8)_
-
-_Date: July 07, 2016_
-
-_NuGet Packages:_
-* https://www.nuget.org/packages/BenchmarkDotNet/0.9.8
-* https://www.nuget.org/packages/BenchmarkDotNet.Diagnostics.Windows/0.9.8
\ No newline at end of file
diff --git a/docs/_changelog/footer/v0.9.9.md b/docs/_changelog/footer/v0.9.9.md
deleted file mode 100644
index 2e6b340c90..0000000000
--- a/docs/_changelog/footer/v0.9.9.md
+++ /dev/null
@@ -1,11 +0,0 @@
-_Date: August 18, 2016_
-
-_Milestone: [v0.9.9](https://github.com/PerfDotNet/BenchmarkDotNet/issues?q=milestone%3Av0.9.9)_
-
-_NuGet Packages:_
-* https://www.nuget.org/packages/BenchmarkDotNet/0.9.9
-* https://www.nuget.org/packages/BenchmarkDotNet.Core/0.9.9
-* https://www.nuget.org/packages/BenchmarkDotNet.Toolchains.Roslyn/0.9.9
-* https://www.nuget.org/packages/BenchmarkDotNet.Diagnostics.Windows/0.9.9
-
-_Online Documentation:_ https://perfdotnet.github.io/BenchmarkDotNet/
\ No newline at end of file
diff --git a/docs/_changelog/header/v0.10.0.md b/docs/_changelog/header/v0.10.0.md
deleted file mode 100644
index 48c4103b0a..0000000000
--- a/docs/_changelog/header/v0.10.0.md
+++ /dev/null
@@ -1,12 +0,0 @@
-* Now BenchmarkDotNet is a part of .NET Foundation
-* Job and Column API refactoring (see new documentation)
-* Measurement engine improvements
-* Horology enhancement (see `TimeInterval` and `Frequency`)
-* Introduced `RankColumn` which is based on `WelchTTest` (see [157aabc3](https://github.com/PerfDotNet/BenchmarkDotNet/commit/cf839a0d7ecfdf93da709b63fe324fd2157aabc3))
-* JsonExporters refactoring (see the Exporters/Json section in the documentation)
- * Renamed JsonExporters classed and attributes
- * JsonExporters with custom settings
- * JsonExporters now includes information about the target type namespace (see [#246](https://github.com/PerfDotNet/BenchmarkDotNet/issues/246)).
-* Add `JetBrains.Annotations` (see [#253](https://github.com/PerfDotNet/BenchmarkDotNet/pull/253))
-* RFC 4180 support in CSV exporters (see [#241](https://github.com/PerfDotNet/BenchmarkDotNet/issues/241))
-* Many bugfixes
\ No newline at end of file
diff --git a/docs/_changelog/header/v0.10.1.md b/docs/_changelog/header/v0.10.1.md
deleted file mode 100644
index 70e3be82c8..0000000000
--- a/docs/_changelog/header/v0.10.1.md
+++ /dev/null
@@ -1,8 +0,0 @@
-* MemoryDiagnoser got improved. The changes:
- * Memory Diagnoser is now part of BenchmarkDotNet.Core.dll, and it's **enabled by default**
- * MemoryDiagnoser is **100% accurate** about allocated memory when using default settings or Job.ShortRun or any longer job. (see [#284](https://github.com/dotnet/BenchmarkDotNet/pull/284))
- * Memory Diagnoser no longer includes allocations from Cleanup/Setup methods (see [#186](https://github.com/dotnet/BenchmarkDotNet/issues/186))
- * the results are now scaled so they are stable across the runs. (see [#133](https://github.com/dotnet/BenchmarkDotNet/issues/133))
-* .NET Core 1.1+ support, we no longer support 1.0, we target netcoreapp1.1 now. Reason: we wanted to use `GC.GetAllocatedBytesForCurrentThread` in MemoryDiagnoser which is available only in 1.1+
-* Improved information about environment in summary
-* Minor bugfixes
\ No newline at end of file
diff --git a/docs/_changelog/header/v0.10.10.md b/docs/_changelog/header/v0.10.10.md
deleted file mode 100644
index ef34841958..0000000000
--- a/docs/_changelog/header/v0.10.10.md
+++ /dev/null
@@ -1,10 +0,0 @@
-Highlights:
-
-* Disassembly Diagnoser (read more here: [Disassembling .NET Code with BenchmarkDotNet](https://adamsitnik.com/Disassembly-Diagnoser/))
-* ParamsSources
-* .NET Core x86 support
-* Environment variables and Mono args support
-* Better environment description
-* More: additional sections in the documentation, bug fixes, build script improvements, internal refactoring.
-
-Overview post: [BenchmarkDotNet v0.10.10](https://aakinshin.net/posts/bdn-v0_10_10/)
\ No newline at end of file
diff --git a/docs/_changelog/header/v0.10.11.md b/docs/_changelog/header/v0.10.11.md
deleted file mode 100644
index 3e53cf54cb..0000000000
--- a/docs/_changelog/header/v0.10.11.md
+++ /dev/null
@@ -1,8 +0,0 @@
-## Highlights
-
-* ByRef and Stack-only support ([#492](https://github.com/dotnet/BenchmarkDotNet/issues/492), [sample](https://github.com/dotnet/BenchmarkDotNet/blob/edf20758871ec621fdbfd93d862769da46c4bf15/samples/BenchmarkDotNet.Samples/IL/IL_RefReturns.cs))
-* .NET Core 2.1 support ([#587](https://github.com/dotnet/BenchmarkDotNet/issues/587))
-* Improved LINQPad support
-* Smart logic for precision in ScaledColumn ([#509](https://github.com/dotnet/BenchmarkDotNet/issues/509), [#590](https://github.com/dotnet/BenchmarkDotNet/issues/590))
-* Better macOS version detection ([15d72388](https://github.com/dotnet/BenchmarkDotNet/commit/15d72388436c1060e87662b5f4519b9e7e071627))
-* Minor fixes and improvements
\ No newline at end of file
diff --git a/docs/_changelog/header/v0.10.12.md b/docs/_changelog/header/v0.10.12.md
deleted file mode 100644
index 43cff33827..0000000000
--- a/docs/_changelog/header/v0.10.12.md
+++ /dev/null
@@ -1,22 +0,0 @@
-Overview post: [BenchmarkDotNet v0.10.12](https://aakinshin.net/posts/bdn-v0_10_12/)
-
-### Highlights
-
-* **Improved DisassemblyDiagnoser:**
- BenchmarkDotNet contains an embedded disassembler so that it can print assembly code for all benchmarks;
- it's not easy, but the disassembler evolves in every release.
-* **Improved MemoryDiagnoser:**
- it has a better precision level, and it takes less time to evaluate memory allocations in a benchmark.
-* **New TailCallDiagnoser:**
- now you get notifications when JIT applies the tail call optimizations to your methods.
-* **Better environment info:**
- when your share performance results, it's very important to share information about your environment.
- The library generates the environment summary for you by default.
- Now it contains information about the amount of physical CPU, physical cores, and logic cores.
- If you run a benchmark on a virtual machine, you will get the name of the hypervisor
- (e.g., Hyper-V, VMware, or VirtualBox).
-* **Better summary table:**
- one of the greatest features of BenchmarkDotNet is the summary table.
- It shows all important information about results in a compact and understandable form.
- Now it has better customization options: you can display relative performance of different environments
- (e.g., compare .NET Framework and .NET Core) and group benchmarks by categories.
\ No newline at end of file
diff --git a/docs/_changelog/header/v0.10.13.md b/docs/_changelog/header/v0.10.13.md
deleted file mode 100644
index 73f5aa1e53..0000000000
--- a/docs/_changelog/header/v0.10.13.md
+++ /dev/null
@@ -1 +0,0 @@
-Overview post: [BenchmarkDotNet v0.10.13](https://aakinshin.net/posts/bdn-v0_10_13/)
\ No newline at end of file
diff --git a/docs/_changelog/header/v0.10.14.md b/docs/_changelog/header/v0.10.14.md
deleted file mode 100644
index aefb183459..0000000000
--- a/docs/_changelog/header/v0.10.14.md
+++ /dev/null
@@ -1,4 +0,0 @@
-* Per-method parameterization ([Read more](https://benchmarkdotnet.org/articles/features/parameterization.html))
-* Console histograms and multimodal disribution detection
-* Many improvements for Mono disassembly support on Windows ([Read more](https://aakinshin.net/posts/dotnet-crossruntime-disasm/))
-* Many bugfixes
diff --git a/docs/_changelog/header/v0.10.2.md b/docs/_changelog/header/v0.10.2.md
deleted file mode 100644
index a93c8c36ae..0000000000
--- a/docs/_changelog/header/v0.10.2.md
+++ /dev/null
@@ -1,9 +0,0 @@
-* Closed [#307](https://github.com/dotnet/BenchmarkDotNet/issues/307): culture invariant statistics output
-* Closed [#321](https://github.com/dotnet/BenchmarkDotNet/issues/321): persist optimized, auto-generated dll compiled from url/plain code
-* Closed [#322](https://github.com/dotnet/BenchmarkDotNet/issues/332): always restore the console foreground color
-* Closed [#337](https://github.com/dotnet/BenchmarkDotNet/issues/337): Better detection of Rscript.exe in RPlotExporter
-* Closed [#345](https://github.com/dotnet/BenchmarkDotNet/issues/345): fix bug in WelchTTestPValueColumn for DryJob
-* VS 2017 [compatibility fix](https://github.com/dotnet/BenchmarkDotNet/commit/f4bdae5b7e203e0f0a7d283db5faa78107674f31)
-* [fix](https://github.com/dotnet/BenchmarkDotNet/commit/24dea483b8312efba669d82a6fac3603e60050f5) bold markup for Atlassian exporter
-* Improved precision of nanobenchmarks
-* Minor infrastructure changes and misc fixes
\ No newline at end of file
diff --git a/docs/_changelog/header/v0.10.3.md b/docs/_changelog/header/v0.10.3.md
deleted file mode 100644
index 976a78eeaf..0000000000
--- a/docs/_changelog/header/v0.10.3.md
+++ /dev/null
@@ -1,8 +0,0 @@
-* **New .csprojs** support for .NET Core. Also for F# ([#366](https://github.com/dotnet/BenchmarkDotNet/issues/366))!
-* New plots and RPlotExporter (density plots for each job; cumulative mean plots)
-* Fixed exporter order (now RPlotExporer uses the actual measurements instead of previous version)
-* Xplat improvments in RuntimeInformation
-* Introduced `RunStrategy.Monitoring`
-* Possibility to set custom path for Mono ([#306](https://github.com/dotnet/BenchmarkDotNet/issues/306))
-* Possibility to set any .NET Core version >= 1.1 ([#336](https://github.com/dotnet/BenchmarkDotNet/issues/336))
-* **MemoryDiagnoser is now disabled by default (Breaking changes!!)** ([#369](https://github.com/dotnet/BenchmarkDotNet/issues/369))
\ No newline at end of file
diff --git a/docs/_changelog/header/v0.10.4.md b/docs/_changelog/header/v0.10.4.md
deleted file mode 100644
index f423a6567a..0000000000
--- a/docs/_changelog/header/v0.10.4.md
+++ /dev/null
@@ -1,23 +0,0 @@
-* New logo
-* Update to Roslyn 2.0, drop .NET 4.5 support ([#303](https://github.com/dotnet/BenchmarkDotNet/issues/303))
-* Initial support of HardwareCounters (Windows only)
-* Initial experimental support of in-process benchmarks
-* Optional configs for `BenchmarkSwitcher` ([#391](https://github.com/dotnet/BenchmarkDotNet/issues/391), [#392](https://github.com/dotnet/BenchmarkDotNet/pull/392))
-* Host API interface ([#356](https://github.com/dotnet/BenchmarkDotNet/pull/356))
-* Improved measurements for async benchmarks ([#415](https://github.com/dotnet/BenchmarkDotNet/issues/415))
-* Improved precision level (MinIterationTimes is 500ms instead of 200ms; introduced `AccuracyMode.MaxAbsoluteError` and `AccuracyMode.MaxRelativeError` instead of `AccuracyMode.MaxStdErrRelative`; logic which select amount of iterations uses confidence intervals instead of standard errors; the Error column (half of CI99.9%) is shown by default instead of StdErr)
-* Introduced `ISummaryStyle`, raw data in CSV reports ([#118](https://github.com/dotnet/BenchmarkDotNet/issues/118), [#146](https://github.com/dotnet/BenchmarkDotNet/issues/146), [#396](https://github.com/dotnet/BenchmarkDotNet/pull/396))
-* Handle cases when report files are existed and locked ([#414](https://github.com/dotnet/BenchmarkDotNet/issues/414), [#416](https://github.com/dotnet/BenchmarkDotNet/pull/416))
-* MarkdownExporter right-justifies numeric columns ([#421](https://github.com/dotnet/BenchmarkDotNet/pull/421))
-* Better colors for console output ([#376](https://github.com/dotnet/BenchmarkDotNet/issues/376))
-* Column legends
-* Add information about CPU microarchitecture for well-known processors to summary
-* Fix AssemblyInformationalVersionAttribute ([#382](https://github.com/dotnet/BenchmarkDotNet/issues/382))
-* Fix incorrect method filtering in BenchmarkSwitcher ([#365](https://github.com/dotnet/BenchmarkDotNet/issues/365))
-* Fix OS Version in Summary for Windows 10 ([#351](https://github.com/dotnet/BenchmarkDotNet/issues/351))
-* Fix OS Version on Mono
-* Fix --class and --method filtering ([#249](https://github.com/dotnet/BenchmarkDotNet/issues/249))
-* Fix --exporters option ([#189](https://github.com/dotnet/BenchmarkDotNet/issues/189))
-* Fix escaping logic in CsvExporter ([#294](https://github.com/dotnet/BenchmarkDotNet/issues/294), [#409](https://github.com/dotnet/BenchmarkDotNet/pull/409))
-* [Fix](https://github.com/dotnet/BenchmarkDotNet/commit/0a251b81b826876179740cc8b79c994a73a5cd51) MacOS detection
-* Minor bugfixes and API improvements
\ No newline at end of file
diff --git a/docs/_changelog/header/v0.10.5.md b/docs/_changelog/header/v0.10.5.md
deleted file mode 100644
index 3ed3541c70..0000000000
--- a/docs/_changelog/header/v0.10.5.md
+++ /dev/null
@@ -1,5 +0,0 @@
-* Fixed SizeUnit presentation in the summary table ([#434](https://github.com/dotnet/BenchmarkDotNet/issues/434))
-* In MemoryDiagnoser, now 1kB = 1024B (instead of 1000 in v0.10.4) ([#434](https://github.com/dotnet/BenchmarkDotNet/issues/434))
-* Fix false allocations detection ([#436](https://github.com/dotnet/BenchmarkDotNet/pull/436) [9b44de70](https://github.com/dotnet/BenchmarkDotNet/commit/9b44de704b96e2333d762b14daa152d859b1917d))
-* Hide ScaledSD column for small values ([da857ad7](https://github.com/dotnet/BenchmarkDotNet/commit/da857ad7eda77db813692d3c3678f8ad04f5af78))
-* Autoselecting amount of digits after the decimal point ([#404](https://github.com/dotnet/BenchmarkDotNet/issues/404))
\ No newline at end of file
diff --git a/docs/_changelog/header/v0.10.6.md b/docs/_changelog/header/v0.10.6.md
deleted file mode 100644
index 33948b2f80..0000000000
--- a/docs/_changelog/header/v0.10.6.md
+++ /dev/null
@@ -1,6 +0,0 @@
-* Removed buggy allocation from Engine which was spoiling the results of MemoryDiagnoser for micro benchmarks. This part of the code is now guarded with very strict integration tests, it should never happen again. We now also exclude the side effects of the Allocation Quantum. **This bug was serious, you must update to `0.10.6`** ([#439](https://github.com/dotnet/BenchmarkDotNet/issues/439))
-* Support of the `PackageTargetFallback` setting which allows to reference components that target old framework monikers (like `dotnet5.4` or `portable-net45+win8`) ([#438](https://github.com/dotnet/BenchmarkDotNet/issues/438))
-* Added `InstructionRetiredPerCycleColumn` which shows up automatically when `HardwareCounter.InstructionRetired` and `HardwareCounter.TotalCycles` are used.
-* Support benchmark classes without namespace ([#446](https://github.com/dotnet/BenchmarkDotNet/issues/446))
-* Fix problem with RPlotExporter and quoted directories in %PATH% ([#446](https://github.com/dotnet/BenchmarkDotNet/issues/446))
-* Show Windows brand version in summary
\ No newline at end of file
diff --git a/docs/_changelog/header/v0.10.7.md b/docs/_changelog/header/v0.10.7.md
deleted file mode 100644
index e58ad7f60e..0000000000
--- a/docs/_changelog/header/v0.10.7.md
+++ /dev/null
@@ -1,6 +0,0 @@
-* LINQPad support (5.22.05+) ([#66](https://github.com/dotnet/BenchmarkDotNet/issues/66), [#445](https://github.com/dotnet/BenchmarkDotNet/issues/445))
-* Benchmark filters and categories ([#248](https://github.com/dotnet/BenchmarkDotNet/issues/248))
-* Updated setup/cleanup attributes: `[GlobalSetup]`, `[GlobalCleanup]`, `[IterationSetup]`, `[IterationCleanup]` ([#270](https://github.com/dotnet/BenchmarkDotNet/issues/270), [#274](https://github.com/dotnet/BenchmarkDotNet/issues/274), [#325](https://github.com/dotnet/BenchmarkDotNet/issues/325), [#456](https://github.com/dotnet/BenchmarkDotNet/issues/456))
-* Better Value Types support ([afa803d0](https://github.com/dotnet/BenchmarkDotNet/commit/afa803d0e38c0e11864b2e4394d4a85d3801d944))
-* Building Sources on Linux: it's possible to build the solution (with unloaded F#/VB projects), run samples (for both net46/netcoreapp1.1), run unit tests (for netcoreapp1.1 only)
-* Fix minor bugs in `JsonExporter` ([#451](https://github.com/dotnet/BenchmarkDotNet/pull/451))
\ No newline at end of file
diff --git a/docs/_changelog/header/v0.10.8.md b/docs/_changelog/header/v0.10.8.md
deleted file mode 100644
index c93b24ee6e..0000000000
--- a/docs/_changelog/header/v0.10.8.md
+++ /dev/null
@@ -1,4 +0,0 @@
-* Legend for time units ([#349](https://github.com/dotnet/BenchmarkDotNet/issues/349), [#459](https://github.com/dotnet/BenchmarkDotNet/issues/459), [f14e508e](https://github.com/dotnet/BenchmarkDotNet/commit/f14e508e44b510a26cc3ec5aed30ee7843a92baf))
-* XML exporter ([#157](https://github.com/dotnet/BenchmarkDotNet/issues/157), [#452](https://github.com/dotnet/BenchmarkDotNet/pull/452), [a0148db8](https://github.com/dotnet/BenchmarkDotNet/commit/a0148db80c518a9d255f496534a8d1666be52c69))
-* .NET Framework 4.7 support ([#461](https://github.com/dotnet/BenchmarkDotNet/issues/461), [3f2b5c3c](https://github.com/dotnet/BenchmarkDotNet/commit/3f2b5c3c134c62f34f0ecf1a9c90d91ad37f2c6a), [5513873a](https://github.com/dotnet/BenchmarkDotNet/commit/5513873ac40d07583d6136e431e3b7c8cdf6c851))
-* Public API for AllocationQuantum ([#450](https://github.com/dotnet/BenchmarkDotNet/issues/450), [#462](https://github.com/dotnet/BenchmarkDotNet/pull/462), [a0148db8](https://github.com/dotnet/BenchmarkDotNet/commit/a0148db80c518a9d255f496534a8d1666be52c69))
\ No newline at end of file
diff --git a/docs/_changelog/header/v0.10.9.md b/docs/_changelog/header/v0.10.9.md
deleted file mode 100644
index 248e6b007e..0000000000
--- a/docs/_changelog/header/v0.10.9.md
+++ /dev/null
@@ -1,14 +0,0 @@
-* Migrate from custom build scripts to Cake (C# Make) ([#426](https://github.com/dotnet/BenchmarkDotNet/issues/426), [#475](https://github.com/dotnet/BenchmarkDotNet/pull/475), thanks [@Ky7m](https://github.com/Ky7m))
-* Target Setup methods for specific Benchmarks ([#469](https://github.com/dotnet/BenchmarkDotNet/issues/469), [#501](https://github.com/dotnet/BenchmarkDotNet/pull/501), thanks [@ipjohnson](https://github.com/ipjohnson))
-* Many improvements in XmlExporter ([#476](https://github.com/dotnet/BenchmarkDotNet/pull/476), [#488](https://github.com/dotnet/BenchmarkDotNet/pull/488), thanks [@Teknikaali](https://github.com/Teknikaali))
-* Add MemoryDiagnoser results to JsonExporter output ([#453](https://github.com/dotnet/BenchmarkDotNet/issues/453), [#478](https://github.com/dotnet/BenchmarkDotNet/pull/478), thanks [@Teknikaali](https://github.com/Teknikaali))
-* Detect correct version of .NET Core (+ improved presentation for information about runtime) ([#448](https://github.com/dotnet/BenchmarkDotNet/issues/448), [ed586585...ed586585](https://github.com/dotnet/BenchmarkDotNet/compare/dc6dc411b4d8703d0a1abafe64fb1e0b0a83af1f...cea199f74923c99f88f4bb4d53e37f86b10269b7))
-* Fix UnauthorizedAccessException ([#380](https://github.com/dotnet/BenchmarkDotNet/issues/380), [#390](https://github.com/dotnet/BenchmarkDotNet/issues/390), [#490](https://github.com/dotnet/BenchmarkDotNet/issues/490), [#491](https://github.com/dotnet/BenchmarkDotNet/issues/491), [8505abb5](https://github.com/dotnet/BenchmarkDotNet/commit/8505abb5416bad90cda03f4972b067f9ac44b304))
-* Fix app.config generation ([#499](https://github.com/dotnet/BenchmarkDotNet/issues/499), [dc6dc411](https://github.com/dotnet/BenchmarkDotNet/commit/dc6dc411b4d8703d0a1abafe64fb1e0b0a83af1f))
-* Fix incorrect order of IterationCleanup and Benchmark jitting ([#481](https://github.com/dotnet/BenchmarkDotNet/issues/481), [#503](https://github.com/dotnet/BenchmarkDotNet/pull/503))
-* Fix test scripts for MacOS+zsh ([1177c8](https://github.com/dotnet/BenchmarkDotNet/commit/1177c80e2dbe931439e44bb0ce2ce25cad8b9ba2))
-* Unix-related ProcessorAffinity fixes ([#474](https://github.com/dotnet/BenchmarkDotNet/issues/474), [26d44411](https://github.com/dotnet/BenchmarkDotNet/commit/26d44411ea47f28a9cc7df84b2df0ef89b2bbcf7))
-* Minor fixes in docs ([#465](https://github.com/dotnet/BenchmarkDotNet/pull/465), [#467](https://github.com/dotnet/BenchmarkDotNet/pull/467), [#473](https://github.com/dotnet/BenchmarkDotNet/pull/473), [#480](https://github.com/dotnet/BenchmarkDotNet/pull/480), [#483](https://github.com/dotnet/BenchmarkDotNet/pull/483), thanks [@mtschneiders](https://github.com/mtschneiders), [@davkean](https://github.com/davkean), [@aarondandy](https://github.com/aarondandy), [@AmadeusW](https://github.com/AmadeusW))
-* Temporary hacks for [appveyor connectivity incident](https://appveyor.statuspage.io/incidents/m2vdvw39kdk8) ([#497](https://github.com/dotnet/BenchmarkDotNet/pull/497), [#506](https://github.com/dotnet/BenchmarkDotNet/pull/506))
-* Additional warnings for incorrect Configs ([#482](https://github.com/dotnet/BenchmarkDotNet/issues/482), [eb84825f](https://github.com/dotnet/BenchmarkDotNet/commit/eb84825ff08aa5d23d2d512d4d4bde3e95ca0815))
-* Additional warnings for F# methods with spaces ([#479](https://github.com/dotnet/BenchmarkDotNet/issues/479), [3c2c8dec](https://github.com/dotnet/BenchmarkDotNet/commit/3c2c8dec28d6c570f2901001058cd9c6000e6ca2), [7ba1c809](https://github.com/dotnet/BenchmarkDotNet/commit/7ba1c809004e0b75eaa87724155480eaf623f8a9), [3ca39afe](https://github.com/dotnet/BenchmarkDotNet/commit/3ca39afe9f0d25359f9b092181beb02d57c5ad32))
\ No newline at end of file
diff --git a/docs/_changelog/header/v0.11.0.md b/docs/_changelog/header/v0.11.0.md
deleted file mode 100644
index 9760c809ba..0000000000
--- a/docs/_changelog/header/v0.11.0.md
+++ /dev/null
@@ -1,350 +0,0 @@
-This is one of the biggest releases of BenchmarkDotNet ever.
-There are so many improvements.
-We have
- new documentation,
- many performance improvements,
- Job Mutators,
- better user experience,
- correct Ctrl+C handling,
- better generic benchmarks support,
- more scenarios for passing arguments to benchmarks,
- awesome support of console arguments,
- unicode support,
- LLVM support in MonoDisassembler,
- and many-many other improvements and bug fixes!
-
-A big part of the features and bug fixes were implemented to meet the enterprise requirements of Microsoft to make it possible to port CoreCLR, CoreFX, and CoreFXLab to BenchmarkDotNet.
-
-The release would not be possible without many contributions from amazing community members. This release is a combined effort. We build BenchmarkDotNet together to make benchmarking .NET code easy and available to everyone for free!
-
-# New documentation
-
-We have many improvements in our documentation!
-The new docs include:
-
-* [DocFX](https://dotnet.github.io/docfx/) under the hood
-* Detailed changelogs which includes all commits, merged pull requests and resolved issues
-* API references
-* Code samples for main features:
- we generate it automatically based on the `BenchmarkDotNet.Samples` project;
- it means that all samples can always be compiled
- (no more samples with outdated API)
-* Better UI
-* Documentation versioning: now it's possible to look at the documentation for recent BenchmarkDotNet versions
-
-# Performance improvements
-
-BenchmarkDotNet needs to be capable of running few thousands of CoreFX and CoreCLR benchmarks in an acceptable amount of time. The code itself was already optimized so we needed architectural and design changes to meet this requirement.
-
-## Generate one executable per runtime settings
-
-To ensure that the side effects of one benchmark run does not affect another benchmark run BenchmarkDotNet generates, builds and runs every benchmark in a dedicated process. So far we were generating and building one executable per benchmark, now we generate and build one executable per runtime settings. So if you want to run ten thousands of benchmarks for .NET Core 2.1 we are going to generate and build single executable, not ten thousand. If you target multiple runtimes the build is going to be executed in parallel. Moreover, if one of the parallel builds fail it's going to be repeated in a sequential way.
-
-Previously the time to generate and build 650 benchmarks from our Samples project was **one hour**. Now it's something around **13 seconds** which means **276 X** improvement for this particular scenario. You can see the changes [here](https://github.com/dotnet/BenchmarkDotNet/issues/699).
-
-## Don't execute long operations more than once per iteration
-
-BenchmarkDotNet was designed to allow for very accurate and stable micro-benchmarking. One of the techniques that we use is manual loop unrolling. In practice, it meant that for every iteration we were executing the benchmark at least 16 times (the default `UnrollFactor` value). It was of course not desired for the very time-consuming benchmarks.
-
-So far this feature was always enabled by default and users would need to configure `UnrollFactor=1` to disable it. Now BenchmarkDotNet is going to discover such scenario and don't perform manual loop unrolling for the very time-consuming benchmarks. BenchmarkDotNet uses `Job.IterationTime` setting (the default is 0.5s) in the Pilot Experiment stage to determine how many times given benchmark should be executed per iteration.
-
-Example:
-
-```cs
-public class Program
-{
- static void Main() => BenchmarkRunner.Run();
-
- [Benchmark]
- public void Sleep1s() => Thread.Sleep(TimeSpan.FromSeconds(1));
-}
-```
-
-Time to run with the previous version: **374 seconds**. With `0.11.0` it's **27 seconds** which gives us almost **14 X** improvement. A good example of benchmarks that are going to benefit from this change are computer game benchmarks and ML.NET benchmarks. You can see the changes [here](https://github.com/dotnet/BenchmarkDotNet/pull/760) and [here](https://github.com/dotnet/BenchmarkDotNet/pull/771).
-
-## Exposing more configuration settings
-
-The default settings were configured to work well with every scenario. Before running the benchmark, BenchmarkDotNet does not know anything about it. This is why it performs many warmup iterations before running the benchmarks.
-
-When you author benchmarks and run them many times you can come up with custom settings that produce similar results but in a shorter manner of time. To allow you to do that we have exposed:
-
-* `Job.MinIterationCount` (default value is 15)
-* `Job.MaxIterationCount` (default value is 100)
-* `Job.MinWarmupIterationCount` (default value is 6)
-* `Job.MaxWarmupIterationCount` (default value is 50)
-
-# User Experience
-
-One of the biggest success factors of BenchmarkDotNet is a great user experience. The tool just works as expected and makes your life easy. We want to make it even better!
-
-## .NET Standard 2.0
-
-We have ported BenchmarkDotNet to .NET Standard 2.0 and thanks to that we were able to not only simplify our code and build process but also merge `BenchmarkDotNet.Core.dll` and `BenchmarkDotNet.Toolchains.Roslyn.dll` into `BenchmarkDotNet.dll`. We still support .NET 4.6 but we have dropped .NET Core 1.1 support. More information and full discussion can be found [here](https://github.com/dotnet/BenchmarkDotNet/pull/688).
-
-**Note:** Our `BenchmarkDotNet.Diagnostics.Windows` package which uses `EventTrace` to implement ETW-based diagnosers was also ported to .NET Standard 2.0 and you can now use all the ETW diagnosers with .NET Core on Windows. We plan to add EventPipe support and make this page fully cross-platform and Unix compatible soon.
-
-## Using complex types as benchmark arguments
-
-So far we have required the users to implement `IParam` interface to make the custom complex types work as benchmark arguments/parameters. This has changed, now the users can use any complex types as arguments and it will just work ([more](https://github.com/dotnet/BenchmarkDotNet/pull/754)).
-
-```cs
-public class Program
-{
- static void Main(string[] args) => BenchmarkRunner.Run();
-
- public IEnumerable
Exe
- net462;net7.0
+ net462;net8.0
+ false
+
+ false
+
@@ -19,11 +23,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..948849aa05 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,18 +11,33 @@
AnyCPU
true
$(NoWarn);CA1018;CA5351;CA1825
+
+ false
+
+ false
-
+
+
+
+ 9.0.0
+
+
+
-
-
+
+
+
+
+
+
+
diff --git a/samples/BenchmarkDotNet.Samples/IntroArgumentsSource.cs b/samples/BenchmarkDotNet.Samples/IntroArgumentsSource.cs
index 33a8d90c0c..493422e51f 100644
--- a/samples/BenchmarkDotNet.Samples/IntroArgumentsSource.cs
+++ b/samples/BenchmarkDotNet.Samples/IntroArgumentsSource.cs
@@ -20,10 +20,13 @@ public class IntroArgumentsSource
}
[Benchmark]
- [ArgumentsSource(nameof(TimeSpans))]
+ [ArgumentsSource(typeof(BenchmarkArguments), nameof(BenchmarkArguments.TimeSpans))] // when the arguments come from a different type, specify that type here
public void SingleArgument(TimeSpan time) => Thread.Sleep(time);
+ }
- public IEnumerable TimeSpans() // for single argument it's an IEnumerable of objects (object)
+ public static class BenchmarkArguments
+ {
+ public static IEnumerable TimeSpans() // for single argument it's an IEnumerable of objects (object)
{
yield return TimeSpan.FromMilliseconds(10);
yield return TimeSpan.FromMilliseconds(100);
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/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/IntroNativeMemory.cs b/samples/BenchmarkDotNet.Samples/IntroNativeMemory.cs
index f968696b43..75f636e87e 100644
--- a/samples/BenchmarkDotNet.Samples/IntroNativeMemory.cs
+++ b/samples/BenchmarkDotNet.Samples/IntroNativeMemory.cs
@@ -3,6 +3,7 @@
using System.Runtime.InteropServices;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Diagnostics.Windows.Configs;
+using BenchmarkDotNet.Filters;
namespace BenchmarkDotNet.Samples
{
@@ -11,7 +12,8 @@ namespace BenchmarkDotNet.Samples
[MemoryDiagnoser]
public class IntroNativeMemory
{
- [Benchmark]
+#pragma warning disable CA1416
+ [Benchmark, WindowsOnly]
public void BitmapWithLeaks()
{
var flag = new Bitmap(200, 100);
@@ -20,7 +22,7 @@ public void BitmapWithLeaks()
graphics.DrawLine(blackPen, 100, 100, 500, 100);
}
- [Benchmark]
+ [Benchmark, WindowsOnly]
public void Bitmap()
{
using (var flag = new Bitmap(200, 100))
@@ -34,6 +36,7 @@ public void Bitmap()
}
}
}
+#pragma warning restore CA1416
private const int Size = 20; // Greater value could cause System.OutOfMemoryException for test with memory leaks.
private int ArraySize = Size * Marshal.SizeOf(typeof(int));
@@ -52,5 +55,13 @@ public unsafe void AllocHGlobalWithLeaks()
IntPtr unmanagedHandle = Marshal.AllocHGlobal(ArraySize);
Span unmanaged = new Span(unmanagedHandle.ToPointer(), ArraySize);
}
+
+ private class WindowsOnlyAttribute : FilterConfigBaseAttribute
+ {
+ public WindowsOnlyAttribute()
+ : base(new SimpleFilter(_ => RuntimeInformation.IsOSPlatform(OSPlatform.Windows)))
+ {
+ }
+ }
}
}
diff --git a/samples/BenchmarkDotNet.Samples/IntroNuGet.cs b/samples/BenchmarkDotNet.Samples/IntroNuGet.cs
index 2eb31cd89a..850a9ceebf 100644
--- a/samples/BenchmarkDotNet.Samples/IntroNuGet.cs
+++ b/samples/BenchmarkDotNet.Samples/IntroNuGet.cs
@@ -1,8 +1,9 @@
using System;
+using System.Collections.Immutable;
+using System.Linq;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Configs;
using BenchmarkDotNet.Jobs;
-using Newtonsoft.Json;
namespace BenchmarkDotNet.Samples
{
@@ -10,33 +11,49 @@ namespace BenchmarkDotNet.Samples
/// Benchmarks between various versions of a NuGet package
///
///
- /// Only supported with the CsProjCoreToolchain toolchain
+ /// Only supported with CsProj toolchains.
///
[Config(typeof(Config))]
public class IntroNuGet
{
- // Specify jobs with different versions of the same NuGet package to benchmark.
- // The NuGet versions referenced on these jobs must be greater or equal to the
- // same NuGet version referenced in this benchmark project.
- // Example: This benchmark project references Newtonsoft.Json 9.0.1
+ // Setup your csproj like this:
+ /*
+
+
+ 9.0.0
+
+
+
+
+ */
+ // All versions of the package must be source-compatible with your benchmark code.
private class Config : ManualConfig
{
public Config()
{
- var baseJob = Job.MediumRun;
+ string[] targetVersions = [
+ "9.0.0",
+ "9.0.3",
+ "9.0.5",
+ ];
- AddJob(baseJob.WithNuGet("Newtonsoft.Json", "11.0.2").WithId("11.0.2"));
- AddJob(baseJob.WithNuGet("Newtonsoft.Json", "11.0.1").WithId("11.0.1"));
- AddJob(baseJob.WithNuGet("Newtonsoft.Json", "10.0.3").WithId("10.0.3"));
- AddJob(baseJob.WithNuGet("Newtonsoft.Json", "10.0.2").WithId("10.0.2"));
- AddJob(baseJob.WithNuGet("Newtonsoft.Json", "10.0.1").WithId("10.0.1"));
- AddJob(baseJob.WithNuGet("Newtonsoft.Json", "9.0.1").WithId("9.0.1"));
+ foreach (var version in targetVersions)
+ {
+ AddJob(Job.MediumRun
+ .WithMsBuildArguments($"/p:SciVersion={version}")
+ .WithId($"v{version}")
+ );
+ }
}
}
+ private static readonly Random rand = new Random(Seed: 0);
+ private static readonly double[] values = Enumerable.Range(1, 10_000).Select(x => rand.NextDouble()).ToArray();
+
[Benchmark]
- public void SerializeAnonymousObject()
- => JsonConvert.SerializeObject(
- new { hello = "world", price = 1.99, now = DateTime.UtcNow });
+ public void ToImmutableArrayBenchmark()
+ {
+ var results = values.ToImmutableArray();
+ }
}
}
\ No newline at end of file
diff --git a/samples/BenchmarkDotNet.Samples/IntroParamsSource.cs b/samples/BenchmarkDotNet.Samples/IntroParamsSource.cs
index cfd00d50f4..8257439ddf 100644
--- a/samples/BenchmarkDotNet.Samples/IntroParamsSource.cs
+++ b/samples/BenchmarkDotNet.Samples/IntroParamsSource.cs
@@ -20,7 +20,16 @@ public class IntroParamsSource
// public static method
public static IEnumerable ValuesForB() => new[] { 10, 20 };
+ // public field getting its params from a method in another type
+ [ParamsSource(typeof(ParamsValues), nameof(ParamsValues.ValuesForC))]
+ public int C;
+
[Benchmark]
- public void Benchmark() => Thread.Sleep(A + B + 5);
+ public void Benchmark() => Thread.Sleep(A + B + C + 5);
+ }
+
+ public static class ParamsValues
+ {
+ public static IEnumerable ValuesForC() => new[] { 1000, 2000 };
}
}
\ No newline at end of file
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 ReturnGenericStructOfReferenceType() => new ValueTuple(null);
+
+ [Benchmark] public ValueTask ReturnValueTaskOfValueType() => new ValueTask(0);
+ [Benchmark] public ValueTask ReturnValueTaskOfReferenceType() => new ValueTask(result: null);
+
+ [Benchmark] public byte ReturnByte() => 0;
+ public struct Byte1 { public byte _1; }
+ [Benchmark] public Byte1 ReturnByte1() => new Byte1();
+ public struct Byte2 { public byte _1, _2; }
+ [Benchmark] public Byte2 ReturnByte2() => new Byte2();
+ public struct Byte3 { public byte _1, _2, _3; }
+ [Benchmark] public Byte3 ReturnByte3() => new Byte3();
+ public struct Byte4 { public byte _1, _2, _3, _4; }
+ [Benchmark] public Byte4 ReturnByte4() => new Byte4();
+
+ [Benchmark] public short ReturnShort() => 0;
+ public struct Short1 { public short _1; }
+ [Benchmark] public Short1 ReturnShort1() => new Short1();
+ public struct Short2 { public short _1, _2; }
+ [Benchmark] public Short2 ReturnShort2() => new Short2();
+ public struct Short3 { public short _1, _2, _3; }
+ [Benchmark] public Short3 ReturnShort3() => new Short3();
+ public struct Short4 { public short _1, _2, _3, _4; }
+ [Benchmark] public Short4 ReturnShort4() => new Short4();
+
+ [Benchmark] public int ReturnInt() => 0;
+ public struct Int1 { public int _1; }
+ [Benchmark] public Int1 ReturnInt1() => new Int1();
+ public struct Int2 { public int _1, _2; }
+ [Benchmark] public Int2 ReturnInt2() => new Int2();
+ public struct Int3 { public int _1, _2, _3; }
+ [Benchmark] public Int3 ReturnInt3() => new Int3();
+ public struct Int4 { public int _1, _2, _3, _4; }
+ [Benchmark] public Int4 ReturnInt4() => new Int4();
+
+ [Benchmark] public long ReturnLong() => 0;
+ public struct Long1 { public long _1; }
+ [Benchmark] public Long1 ReturnLong1() => new Long1();
+ public struct Long2 { public long _1, _2; }
+ [Benchmark] public Long2 ReturnLong2() => new Long2();
+ public struct Long3 { public long _1, _2, _3; }
+ [Benchmark] public Long3 ReturnLong3() => new Long3();
+ public struct Long4 { public long _1, _2, _3, _4; }
+ [Benchmark] public Long4 ReturnLong4() => new Long4();
+}
+// ReSharper restore InconsistentNaming
\ No newline at end of file
diff --git a/samples/BenchmarkDotNet.Samples/IntroStatisticalTesting.cs b/samples/BenchmarkDotNet.Samples/IntroStatisticalTesting.cs
index bbdf85aad5..6351281aa6 100644
--- a/samples/BenchmarkDotNet.Samples/IntroStatisticalTesting.cs
+++ b/samples/BenchmarkDotNet.Samples/IntroStatisticalTesting.cs
@@ -1,14 +1,10 @@
using System.Threading;
using BenchmarkDotNet.Attributes;
-using Perfolizer.Mathematics.SignificanceTesting;
-using Perfolizer.Mathematics.Thresholds;
namespace BenchmarkDotNet.Samples
{
- [StatisticalTestColumn(StatisticalTestKind.Welch, ThresholdUnit.Microseconds, 1, true)]
- [StatisticalTestColumn(StatisticalTestKind.MannWhitney, ThresholdUnit.Microseconds, 1, true)]
- [StatisticalTestColumn(StatisticalTestKind.Welch, ThresholdUnit.Ratio, 0.03, true)]
- [StatisticalTestColumn(StatisticalTestKind.MannWhitney, ThresholdUnit.Ratio, 0.03, true)]
+ [StatisticalTestColumn("500us")]
+ [StatisticalTestColumn("3%")]
[SimpleJob(warmupCount: 0, iterationCount: 5)]
public class IntroStatisticalTesting
{
diff --git a/samples/BenchmarkDotNet.Samples/IntroSummaryStyle.cs b/samples/BenchmarkDotNet.Samples/IntroSummaryStyle.cs
new file mode 100644
index 0000000000..6a9830be18
--- /dev/null
+++ b/samples/BenchmarkDotNet.Samples/IntroSummaryStyle.cs
@@ -0,0 +1,39 @@
+using System.Globalization;
+using BenchmarkDotNet.Attributes;
+using BenchmarkDotNet.Configs;
+using BenchmarkDotNet.Reports;
+using Perfolizer.Horology;
+using Perfolizer.Metrology;
+
+namespace BenchmarkDotNet.Samples
+{
+ [Config(typeof(Config))]
+ public class IntroSummaryStyle
+ {
+ private class Config : ManualConfig
+ {
+ public Config()
+ {
+ // Configure the summary style here
+ var summaryStyle = new SummaryStyle
+ (
+ cultureInfo: CultureInfo.InvariantCulture,
+ printUnitsInHeader: true,
+ printUnitsInContent: false,
+ sizeUnit: SizeUnit.KB,
+ timeUnit: TimeUnit.Nanosecond,
+ maxParameterColumnWidth: 20
+
+ );
+
+ WithSummaryStyle(summaryStyle);
+ }
+ }
+
+ [Params(10, 100)]
+ public int N;
+
+ [Benchmark]
+ public void Sleep() => System.Threading.Thread.Sleep(N);
+ }
+}
\ No newline at end of file
diff --git a/samples/BenchmarkDotNet.Samples/IntroVisualStudioDiagnoser.cs b/samples/BenchmarkDotNet.Samples/IntroVisualStudioDiagnoser.cs
new file mode 100644
index 0000000000..e513f39320
--- /dev/null
+++ b/samples/BenchmarkDotNet.Samples/IntroVisualStudioDiagnoser.cs
@@ -0,0 +1,23 @@
+using System;
+using BenchmarkDotNet.Attributes;
+using Microsoft.VSDiagnostics;
+
+namespace BenchmarkDotNet.Samples
+{
+ // Enables profiling with the CPU Usage tool
+ // See: https://learn.microsoft.com/visualstudio/profiling/profiling-with-benchmark-dotnet
+ [CPUUsageDiagnoser]
+ public class IntroVisualStudioProfiler
+ {
+ private readonly Random rand = new Random(42);
+
+ [Benchmark]
+ public void BurnCPU()
+ {
+ for (int i = 0; i < 100000; ++i)
+ {
+ rand.Next(1, 100);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/samples/BenchmarkDotNet.Samples/IntroWakeLock.cs b/samples/BenchmarkDotNet.Samples/IntroWakeLock.cs
new file mode 100644
index 0000000000..099b347c7b
--- /dev/null
+++ b/samples/BenchmarkDotNet.Samples/IntroWakeLock.cs
@@ -0,0 +1,30 @@
+using BenchmarkDotNet.Attributes;
+using BenchmarkDotNet.Configs;
+using System;
+using System.Threading;
+
+// *** Attribute Style applied to Assembly ***
+[assembly: WakeLock(WakeLockType.System)]
+
+namespace BenchmarkDotNet.Samples;
+
+// *** Attribute Style ***
+[WakeLock(WakeLockType.Display)]
+public class IntroWakeLock
+{
+ [Benchmark]
+ public void LongRunning() => Thread.Sleep(TimeSpan.FromSeconds(10));
+}
+
+// *** Object Style ***
+[Config(typeof(Config))]
+public class IntroWakeLockObjectStyle
+{
+ private class Config : ManualConfig
+ {
+ public Config() => WakeLock = WakeLockType.System;
+ }
+
+ [Benchmark]
+ public void LongRunning() => Thread.Sleep(TimeSpan.FromSeconds(10));
+}
diff --git a/src/BenchmarkDotNet.Annotations/Attributes/ArgumentsSourceAttribute.cs b/src/BenchmarkDotNet.Annotations/Attributes/ArgumentsSourceAttribute.cs
index a7d3fe38bb..f4836e329a 100644
--- a/src/BenchmarkDotNet.Annotations/Attributes/ArgumentsSourceAttribute.cs
+++ b/src/BenchmarkDotNet.Annotations/Attributes/ArgumentsSourceAttribute.cs
@@ -6,7 +6,18 @@ namespace BenchmarkDotNet.Attributes
public class ArgumentsSourceAttribute : PriorityAttribute
{
public string Name { get; }
+ public Type? Type { get; }
- public ArgumentsSourceAttribute(string name) => Name = name;
+ public ArgumentsSourceAttribute(string name)
+ {
+ Name = name;
+ Type = null;
+ }
+
+ public ArgumentsSourceAttribute(Type type, string name)
+ {
+ Name = name;
+ Type = type;
+ }
}
}
\ No newline at end of file
diff --git a/src/BenchmarkDotNet.Annotations/Attributes/ParamsAttribute.cs b/src/BenchmarkDotNet.Annotations/Attributes/ParamsAttribute.cs
index 22148ffa64..aa47ebdf96 100644
--- a/src/BenchmarkDotNet.Annotations/Attributes/ParamsAttribute.cs
+++ b/src/BenchmarkDotNet.Annotations/Attributes/ParamsAttribute.cs
@@ -5,7 +5,7 @@ namespace BenchmarkDotNet.Attributes
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)]
public class ParamsAttribute : PriorityAttribute
{
- public object?[] Values { get; }
+ public object?[] Values { get; protected set; }
// CLS-Compliant Code requires a constructor without an array in the argument list
public ParamsAttribute() => Values = new object[0];
diff --git a/src/BenchmarkDotNet.Annotations/Attributes/ParamsSourceAttribute.cs b/src/BenchmarkDotNet.Annotations/Attributes/ParamsSourceAttribute.cs
index 570be32179..3587907d64 100644
--- a/src/BenchmarkDotNet.Annotations/Attributes/ParamsSourceAttribute.cs
+++ b/src/BenchmarkDotNet.Annotations/Attributes/ParamsSourceAttribute.cs
@@ -6,7 +6,18 @@ namespace BenchmarkDotNet.Attributes
public class ParamsSourceAttribute : PriorityAttribute
{
public string Name { get; }
+ public Type? Type { get; }
- public ParamsSourceAttribute(string name) => Name = name;
+ public ParamsSourceAttribute(string name)
+ {
+ Name = name;
+ Type = null;
+ }
+
+ public ParamsSourceAttribute(Type type, string name)
+ {
+ Name = name;
+ Type = type;
+ }
}
}
\ No newline at end of file
diff --git a/src/BenchmarkDotNet.Annotations/BenchmarkDotNet.Annotations.csproj b/src/BenchmarkDotNet.Annotations/BenchmarkDotNet.Annotations.csproj
index 2e7361e163..ccc51bcd9a 100644
--- a/src/BenchmarkDotNet.Annotations/BenchmarkDotNet.Annotations.csproj
+++ b/src/BenchmarkDotNet.Annotations/BenchmarkDotNet.Annotations.csproj
@@ -2,7 +2,7 @@
Basic BenchmarkDotNet attributes that can be used to annotate your benchmarks
- netstandard1.0;netstandard2.0
+ netstandard2.0
$(NoWarn);1701;1702;1705;1591;3005;NU1702;CA1825
BenchmarkDotNet.Annotations
BenchmarkDotNet.Annotations
diff --git a/src/BenchmarkDotNet.Annotations/Jobs/RuntimeMoniker.cs b/src/BenchmarkDotNet.Annotations/Jobs/RuntimeMoniker.cs
index 4fc2db7388..16b242d2dc 100644
--- a/src/BenchmarkDotNet.Annotations/Jobs/RuntimeMoniker.cs
+++ b/src/BenchmarkDotNet.Annotations/Jobs/RuntimeMoniker.cs
@@ -110,6 +110,11 @@ public enum RuntimeMoniker
///
Net90,
+ ///
+ /// .NET 10.0
+ ///
+ Net10_0,
+
///
/// NativeAOT compiled as net6.0
///
@@ -130,6 +135,11 @@ public enum RuntimeMoniker
///
NativeAot90,
+ ///
+ /// NativeAOT compiled as net10.0
+ ///
+ NativeAot10_0,
+
///
/// WebAssembly with default .Net version
///
@@ -160,6 +170,11 @@ public enum RuntimeMoniker
///
WasmNet90,
+ ///
+ /// WebAssembly with net10.0
+ ///
+ WasmNet10_0,
+
///
/// Mono with the Ahead of Time LLVM Compiler backend
///
@@ -185,6 +200,11 @@ public enum RuntimeMoniker
///
MonoAOTLLVMNet90,
+ ///
+ /// Mono with the Ahead of Time LLVM Compiler backend and net10.0
+ ///
+ MonoAOTLLVMNet10_0,
+
///
/// .NET 6 using MonoVM (not CLR which is the default)
///
@@ -204,5 +224,10 @@ public enum RuntimeMoniker
/// .NET 9 using MonoVM (not CLR which is the default)
///
Mono90,
+
+ ///
+ /// .NET 10 using MonoVM (not CLR which is the default)
+ ///
+ Mono10_0,
}
}
diff --git a/src/BenchmarkDotNet.Diagnostics.Windows/BenchmarkDotNet.Diagnostics.Windows.csproj b/src/BenchmarkDotNet.Diagnostics.Windows/BenchmarkDotNet.Diagnostics.Windows.csproj
index 6503c7fa7b..e2eb3d2b68 100644
--- a/src/BenchmarkDotNet.Diagnostics.Windows/BenchmarkDotNet.Diagnostics.Windows.csproj
+++ b/src/BenchmarkDotNet.Diagnostics.Windows/BenchmarkDotNet.Diagnostics.Windows.csproj
@@ -12,6 +12,6 @@
-
+
diff --git a/src/BenchmarkDotNet.Diagnostics.Windows/EtwDiagnoser.cs b/src/BenchmarkDotNet.Diagnostics.Windows/EtwDiagnoser.cs
index 80423d72cf..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,10 +91,6 @@ 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)
{
if (parameters != null && parameters.Items.Count > 0)
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/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/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..b5d8a9c359 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
{
@@ -59,7 +60,7 @@ protected override void AttachToEvents(TraceEventSession session, BenchmarkCase
private sealed class MethodsJittedDescriptor : IMetricDescriptor
{
- internal static readonly MethodsJittedDescriptor Instance = new ();
+ internal static readonly MethodsJittedDescriptor Instance = new();
public string Id => nameof(MethodsJittedDescriptor);
public string DisplayName => "Methods JITted";
@@ -74,7 +75,7 @@ private sealed class MethodsJittedDescriptor : IMetricDescriptor
private sealed class MethodsTieredDescriptor : IMetricDescriptor
{
- internal static readonly MethodsTieredDescriptor Instance = new ();
+ internal static readonly MethodsTieredDescriptor Instance = new();
public string Id => nameof(MethodsTieredDescriptor);
public string DisplayName => "Methods Tiered";
@@ -89,7 +90,7 @@ private sealed class MethodsTieredDescriptor : IMetricDescriptor
private sealed class JitAllocatedMemoryDescriptor : IMetricDescriptor
{
- internal static readonly JitAllocatedMemoryDescriptor Instance = new ();
+ internal static readonly JitAllocatedMemoryDescriptor Instance = new();
public string Id => nameof(JitAllocatedMemoryDescriptor);
public string DisplayName => "JIT allocated memory";
@@ -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/Properties/AssemblyInfo.cs b/src/BenchmarkDotNet.Diagnostics.Windows/Properties/AssemblyInfo.cs
index a710492849..bf31db808e 100644
--- a/src/BenchmarkDotNet.Diagnostics.Windows/Properties/AssemblyInfo.cs
+++ b/src/BenchmarkDotNet.Diagnostics.Windows/Properties/AssemblyInfo.cs
@@ -7,8 +7,4 @@
[assembly: CLSCompliant(true)]
-#if RELEASE
[assembly: InternalsVisibleTo("BenchmarkDotNet.IntegrationTests,PublicKey=" + BenchmarkDotNetInfo.PublicKey)]
-#else
-[assembly: InternalsVisibleTo("BenchmarkDotNet.IntegrationTests")]
-#endif
\ No newline at end of file
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/NativeMemoryLogParser.cs b/src/BenchmarkDotNet.Diagnostics.Windows/Tracing/NativeMemoryLogParser.cs
index 76061eed08..295ea1d46b 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
@@ -38,7 +38,7 @@ public NativeMemoryLogParser(string etlFilePath, BenchmarkCase benchmarkCase, IL
this.benchmarkCase = benchmarkCase;
this.logger = logger;
- moduleName = programName;
+ moduleName = programName.ToLowerInvariant();
functionNames = new[]
{
nameof(EngineParameters.WorkloadActionUnroll),
@@ -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..cc209f4258
--- /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..4cc109a0a4
--- /dev/null
+++ b/src/BenchmarkDotNet.Diagnostics.dotMemory/Properties/AssemblyInfo.cs
@@ -0,0 +1,7 @@
+using System;
+using System.Runtime.CompilerServices;
+using BenchmarkDotNet.Properties;
+
+[assembly: CLSCompliant(true)]
+
+[assembly: InternalsVisibleTo("BenchmarkDotNet.Tests,PublicKey=" + BenchmarkDotNetInfo.PublicKey)]
diff --git a/src/BenchmarkDotNet.Diagnostics.dotTrace/BenchmarkDotNet.Diagnostics.dotTrace.csproj b/src/BenchmarkDotNet.Diagnostics.dotTrace/BenchmarkDotNet.Diagnostics.dotTrace.csproj
index e7852f1575..8206182d6a 100644
--- a/src/BenchmarkDotNet.Diagnostics.dotTrace/BenchmarkDotNet.Diagnostics.dotTrace.csproj
+++ b/src/BenchmarkDotNet.Diagnostics.dotTrace/BenchmarkDotNet.Diagnostics.dotTrace.csproj
@@ -14,7 +14,7 @@
-
+
diff --git a/src/BenchmarkDotNet.Diagnostics.dotTrace/DotTraceDiagnoser.cs b/src/BenchmarkDotNet.Diagnostics.dotTrace/DotTraceDiagnoser.cs
index fe9e77005a..8b5d3a858f 100644
--- a/src/BenchmarkDotNet.Diagnostics.dotTrace/DotTraceDiagnoser.cs
+++ b/src/BenchmarkDotNet.Diagnostics.dotTrace/DotTraceDiagnoser.cs
@@ -1,151 +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:
- case RuntimeMoniker.Net90:
- return true;
- case RuntimeMoniker.NotRecognized:
- case RuntimeMoniker.Mono:
- case RuntimeMoniker.NativeAot60:
- case RuntimeMoniker.NativeAot70:
- case RuntimeMoniker.NativeAot80:
- case RuntimeMoniker.NativeAot90:
- case RuntimeMoniker.Wasm:
- case RuntimeMoniker.WasmNet50:
- case RuntimeMoniker.WasmNet60:
- case RuntimeMoniker.WasmNet70:
- case RuntimeMoniker.WasmNet80:
- case RuntimeMoniker.WasmNet90:
- case RuntimeMoniker.MonoAOTLLVM:
- case RuntimeMoniker.MonoAOTLLVMNet60:
- case RuntimeMoniker.MonoAOTLLVMNet70:
- case RuntimeMoniker.MonoAOTLLVMNet80:
- case RuntimeMoniker.MonoAOTLLVMNet90:
- case RuntimeMoniker.Mono60:
- case RuntimeMoniker.Mono70:
- case RuntimeMoniker.Mono80:
- case RuntimeMoniker.Mono90:
+ 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 19e88a6de1..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 f2f07625fa..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) && snapshotDirectory != null)
- {
- 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 dfc9903b82..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 a02c9c1995..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 c353939f1f..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.Diagnostics.dotTrace/Properties/AssemblyInfo.cs b/src/BenchmarkDotNet.Diagnostics.dotTrace/Properties/AssemblyInfo.cs
index 270fdc2c9c..4cc109a0a4 100644
--- a/src/BenchmarkDotNet.Diagnostics.dotTrace/Properties/AssemblyInfo.cs
+++ b/src/BenchmarkDotNet.Diagnostics.dotTrace/Properties/AssemblyInfo.cs
@@ -4,8 +4,4 @@
[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.Disassembler.x64/BenchmarkDotNet.Disassembler.x64.csproj b/src/BenchmarkDotNet.Disassembler.x64/BenchmarkDotNet.Disassembler.x64.csproj
index 3162c74e5d..2f15efcc13 100644
--- a/src/BenchmarkDotNet.Disassembler.x64/BenchmarkDotNet.Disassembler.x64.csproj
+++ b/src/BenchmarkDotNet.Disassembler.x64/BenchmarkDotNet.Disassembler.x64.csproj
@@ -15,7 +15,7 @@
BenchmarkDotNet.Disassembler
-
-
+
+
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 c9ac897570..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)
@@ -178,7 +178,7 @@ 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;
@@ -186,7 +186,7 @@ private static PdbReader GetReaderForMethod(ClrMethod method)
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.Disassembler.x86/BenchmarkDotNet.Disassembler.x86.csproj b/src/BenchmarkDotNet.Disassembler.x86/BenchmarkDotNet.Disassembler.x86.csproj
index 21051d6104..5410f6d77b 100644
--- a/src/BenchmarkDotNet.Disassembler.x86/BenchmarkDotNet.Disassembler.x86.csproj
+++ b/src/BenchmarkDotNet.Disassembler.x86/BenchmarkDotNet.Disassembler.x86.csproj
@@ -21,7 +21,7 @@
-
-
+
+
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..cefa91a7ca
--- /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..df43599aec
--- /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..5def448d9c
--- /dev/null
+++ b/src/BenchmarkDotNet.TestAdapter/BenchmarkDotNet.TestAdapter.csproj
@@ -0,0 +1,30 @@
+
+
+
+ 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..d49c3d24e1
--- /dev/null
+++ b/src/BenchmarkDotNet.TestAdapter/BenchmarkEnumerator.cs
@@ -0,0 +1,70 @@
+using BenchmarkDotNet.Extensions;
+using BenchmarkDotNet.Helpers;
+using BenchmarkDotNet.Running;
+using BenchmarkDotNet.Toolchains;
+using System;
+using System.IO;
+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)
+ {
+#if NET462
+ // Temporary workaround for BenchmarkDotNet assembly loading issue that occurred under the following conditions:
+ // 1. Run BenchmarkDotNet.Samples project with following command.
+ // > dotnet test -c Release --list-tests --framework net462 -tl:off
+ // 2. When using `BenchmarkDotNet.TestAdapter` package and targeting .NET Framework.
+ AppDomain.CurrentDomain.AssemblyResolve += (sender, eventArgs) =>
+ {
+ if (eventArgs.Name.StartsWith("BenchmarkDotNet, Version="))
+ {
+ var baseDir = Path.GetDirectoryName(assemblyPath);
+ var path = Path.Combine(baseDir, "BenchmarkDotNet.dll");
+ if (File.Exists(path))
+ {
+ return Assembly.LoadFrom(path);
+ }
+ }
+
+ // Fallback to default assembly resolver
+ return null;
+ };
+#endif
+
+ 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..aad574ed76
--- /dev/null
+++ b/src/BenchmarkDotNet.TestAdapter/BenchmarkExecutor.cs
@@ -0,0 +1,90 @@
+using BenchmarkDotNet.Configs;
+using BenchmarkDotNet.Loggers;
+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)
+ .RemoveLoggersOfType() // Console logs are also outputted by VSTestLogger.
+ .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..28444ae8db
--- /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/Utility/LoggerHelper.cs b/src/BenchmarkDotNet.TestAdapter/Utility/LoggerHelper.cs
new file mode 100644
index 0000000000..08238c1927
--- /dev/null
+++ b/src/BenchmarkDotNet.TestAdapter/Utility/LoggerHelper.cs
@@ -0,0 +1,57 @@
+using Microsoft.VisualStudio.TestPlatform.ObjectModel.Logging;
+using System.Diagnostics;
+using System.IO;
+
+namespace BenchmarkDotNet.TestAdapter;
+
+internal class LoggerHelper
+{
+ public LoggerHelper(IMessageLogger logger, Stopwatch stopwatch)
+ {
+ InnerLogger = logger;
+ Stopwatch = stopwatch;
+ }
+
+ public IMessageLogger InnerLogger { get; private set; }
+
+ public Stopwatch Stopwatch { get; private set; }
+
+ public void Log(string format, params object[] args)
+ {
+ SendMessage(TestMessageLevel.Informational, null, string.Format(format, args));
+ }
+
+ public void LogWithSource(string source, string format, params object[] args)
+ {
+ SendMessage(TestMessageLevel.Informational, source, string.Format(format, args));
+ }
+
+ public void LogError(string format, params object[] args)
+ {
+ SendMessage(TestMessageLevel.Error, null, string.Format(format, args));
+ }
+
+ public void LogErrorWithSource(string source, string format, params object[] args)
+ {
+ SendMessage(TestMessageLevel.Error, source, string.Format(format, args));
+ }
+
+ public void LogWarning(string format, params object[] args)
+ {
+ SendMessage(TestMessageLevel.Warning, null, string.Format(format, args));
+ }
+
+ public void LogWarningWithSource(string source, string format, params object[] args)
+ {
+ SendMessage(TestMessageLevel.Warning, source, string.Format(format, args));
+ }
+
+ private void SendMessage(TestMessageLevel level, string? assemblyName, string message)
+ {
+ var assemblyText = assemblyName == null
+ ? "" :
+ $"{Path.GetFileNameWithoutExtension(assemblyName)}: ";
+
+ InnerLogger.SendMessage(level, $"[BenchmarkDotNet {Stopwatch.Elapsed:hh\\:mm\\:ss\\.ff}] {assemblyText}{message}");
+ }
+}
diff --git a/src/BenchmarkDotNet.TestAdapter/Utility/TestCaseFilter.cs b/src/BenchmarkDotNet.TestAdapter/Utility/TestCaseFilter.cs
new file mode 100644
index 0000000000..ffd891bf61
--- /dev/null
+++ b/src/BenchmarkDotNet.TestAdapter/Utility/TestCaseFilter.cs
@@ -0,0 +1,166 @@
+using Microsoft.VisualStudio.TestPlatform.ObjectModel;
+using Microsoft.VisualStudio.TestPlatform.ObjectModel.Adapter;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+
+namespace BenchmarkDotNet.TestAdapter;
+
+internal class TestCaseFilter
+{
+ private const string DisplayNameString = "DisplayName";
+ private const string FullyQualifiedNameString = "FullyQualifiedName";
+
+ private readonly HashSet knownTraits;
+ private List supportedPropertyNames;
+ private readonly ITestCaseFilterExpression? filterExpression;
+ private readonly bool successfullyGotFilter;
+ private readonly bool isDiscovery;
+
+ public TestCaseFilter(IDiscoveryContext discoveryContext, LoggerHelper logger)
+ {
+ // Traits are not known at discovery time because we load them from benchmarks
+ isDiscovery = true;
+ knownTraits = [];
+ supportedPropertyNames = GetSupportedPropertyNames();
+ successfullyGotFilter = GetTestCaseFilterExpressionFromDiscoveryContext(discoveryContext, logger, out filterExpression);
+ }
+
+ public TestCaseFilter(IRunContext runContext, LoggerHelper logger, string assemblyFileName, HashSet knownTraits)
+ {
+ this.knownTraits = knownTraits;
+ supportedPropertyNames = GetSupportedPropertyNames();
+ successfullyGotFilter = GetTestCaseFilterExpression(runContext, logger, assemblyFileName, out filterExpression);
+ }
+
+ public string GetTestCaseFilterValue()
+ {
+ return successfullyGotFilter
+ ? filterExpression?.TestCaseFilterValue ?? ""
+ : "";
+ }
+
+ public bool MatchTestCase(TestCase testCase)
+ {
+ if (!successfullyGotFilter)
+ {
+ // Had an error while getting filter, match no testcase to ensure discovered test list is empty
+ return false;
+ }
+ else if (filterExpression == null)
+ {
+ // No filter specified, keep every testcase
+ return true;
+ }
+
+ return filterExpression.MatchTestCase(testCase, p => PropertyProvider(testCase, p));
+ }
+
+ public object? PropertyProvider(TestCase testCase, string name)
+ {
+ // Traits filtering
+ if (isDiscovery || knownTraits.Contains(name))
+ {
+ var result = new List();
+
+ foreach (var trait in GetTraits(testCase))
+ if (string.Equals(trait.Key, name, StringComparison.OrdinalIgnoreCase))
+ result.Add(trait.Value);
+
+ if (result.Count > 0)
+ return result.ToArray();
+ }
+
+ // Property filtering
+ switch (name.ToLowerInvariant())
+ {
+ // FullyQualifiedName
+ case "fullyqualifiedname":
+ return testCase.FullyQualifiedName;
+ // DisplayName
+ case "displayname":
+ return testCase.DisplayName;
+ default:
+ return null;
+ }
+ }
+
+ private bool GetTestCaseFilterExpression(IRunContext runContext, LoggerHelper logger, string assemblyFileName, out ITestCaseFilterExpression? filter)
+ {
+ filter = null;
+
+ try
+ {
+ filter = runContext.GetTestCaseFilter(supportedPropertyNames, null!);
+ return true;
+ }
+ catch (TestPlatformFormatException e)
+ {
+ logger.LogWarning("{0}: Exception filtering tests: {1}", Path.GetFileNameWithoutExtension(assemblyFileName), e.Message);
+ return false;
+ }
+ }
+
+ private bool GetTestCaseFilterExpressionFromDiscoveryContext(IDiscoveryContext discoveryContext, LoggerHelper logger, out ITestCaseFilterExpression? filter)
+ {
+ filter = null;
+
+ if (discoveryContext is IRunContext runContext)
+ {
+ try
+ {
+ filter = runContext.GetTestCaseFilter(supportedPropertyNames, null!);
+ return true;
+ }
+ catch (TestPlatformException e)
+ {
+ logger.LogWarning("Exception filtering tests: {0}", e.Message);
+ return false;
+ }
+ }
+ else
+ {
+ try
+ {
+ // GetTestCaseFilter is present on DiscoveryContext but not in IDiscoveryContext interface
+ var method = discoveryContext.GetType().GetRuntimeMethod("GetTestCaseFilter", [typeof(IEnumerable), typeof(Func)]);
+ filter = (ITestCaseFilterExpression)method?.Invoke(discoveryContext, [supportedPropertyNames, null])!;
+
+ return true;
+ }
+ catch (TargetInvocationException e)
+ {
+ if (e?.InnerException is TestPlatformException ex)
+ {
+ logger.LogWarning("Exception filtering tests: {0}", ex.InnerException.Message ?? "");
+ return false;
+ }
+
+ throw e!.InnerException;
+ }
+ }
+ }
+
+ private List GetSupportedPropertyNames()
+ {
+ // Returns the set of well-known property names usually used with the Test Plugins (Used Test Traits + DisplayName + FullyQualifiedName)
+ if (supportedPropertyNames == null)
+ {
+ supportedPropertyNames = knownTraits.ToList();
+ supportedPropertyNames.Add(DisplayNameString);
+ supportedPropertyNames.Add(FullyQualifiedNameString);
+ }
+
+ return supportedPropertyNames;
+ }
+
+ private static IEnumerable> GetTraits(TestCase testCase)
+ {
+ var traitProperty = TestProperty.Find("TestObject.Traits");
+ return traitProperty != null
+ ? testCase.GetPropertyValue(traitProperty, Array.Empty>())
+ : [];
+ }
+}
diff --git a/src/BenchmarkDotNet.TestAdapter/VSTestAdapter.cs b/src/BenchmarkDotNet.TestAdapter/VSTestAdapter.cs
new file mode 100644
index 0000000000..3a3e155bf4
--- /dev/null
+++ b/src/BenchmarkDotNet.TestAdapter/VSTestAdapter.cs
@@ -0,0 +1,249 @@
+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.Diagnostics;
+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)
+ {
+ var stopwatch = Stopwatch.StartNew();
+ var loggerHelper = new LoggerHelper(logger, stopwatch);
+ var testCaseFilter = new TestCaseFilter(discoveryContext, loggerHelper);
+
+ foreach (var source in sources)
+ {
+ ValidateSourceIsAssemblyOrThrow(source);
+ foreach (var testCase in GetVsTestCasesFromAssembly(source, logger))
+ {
+ if (!testCaseFilter.MatchTestCase(testCase))
+ continue;
+
+ 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();
+
+ var stopwatch = Stopwatch.StartNew();
+ var logger = new LoggerHelper(frameworkHandle, stopwatch);
+
+ foreach (var testsPerAssembly in tests.GroupBy(t => t.Source))
+ {
+ RunBenchmarks(testsPerAssembly.Key, frameworkHandle, testsPerAssembly);
+ }
+
+ cts = null;
+ }
+
+ ///
+ /// Runs all/filtered 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();
+
+ var stopwatch = Stopwatch.StartNew();
+ var logger = new LoggerHelper(frameworkHandle, stopwatch);
+
+ foreach (var source in sources)
+ {
+ var filter = new TestCaseFilter(runContext!, logger, source, ["Category"]);
+ if (filter.GetTestCaseFilterValue() != "")
+ {
+ var discoveredBenchmarks = GetVsTestCasesFromAssembly(source, frameworkHandle);
+ var filteredTestCases = discoveredBenchmarks.Where(x => filter.MatchTestCase(x))
+ .ToArray();
+
+ if (filteredTestCases.Length == 0)
+ continue;
+
+ // Run filtered tests.
+ RunBenchmarks(source, frameworkHandle, filteredTestCases);
+ }
+ else
+ {
+ // Run all benchmarks
+ 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..f002e7eb29
--- /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..7184842aba
--- /dev/null
+++ b/src/BenchmarkDotNet.TestAdapter/build/BenchmarkDotNet.TestAdapter.props
@@ -0,0 +1,23 @@
+
+
+ $(MSBuildThisFileDirectory)..\entrypoints\
+
+ false
+
+
+
+
+
+
+
+
+
+
+ false
+
+
+
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 c5c54f8c91..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;
@@ -53,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) + ")"
@@ -66,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/PerfonarExporterAttribute.cs b/src/BenchmarkDotNet/Attributes/Exporters/PerfonarExporterAttribute.cs
new file mode 100644
index 0000000000..e7547f3e7a
--- /dev/null
+++ b/src/BenchmarkDotNet/Attributes/Exporters/PerfonarExporterAttribute.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)]
+internal class PerfonarExporterAttribute() : ExporterConfigBaseAttribute(new PerfonarJsonExporter(), new PerfonarMdExporter());
\ No newline at end of file
diff --git a/src/BenchmarkDotNet/Attributes/OrdererAttribute.cs b/src/BenchmarkDotNet/Attributes/OrdererAttribute.cs
index 83b5051895..b20a9108ca 100644
--- a/src/BenchmarkDotNet/Attributes/OrdererAttribute.cs
+++ b/src/BenchmarkDotNet/Attributes/OrdererAttribute.cs
@@ -9,9 +9,10 @@ public class OrdererAttribute : Attribute, IConfigSource
{
public OrdererAttribute(
SummaryOrderPolicy summaryOrderPolicy = SummaryOrderPolicy.Default,
- MethodOrderPolicy methodOrderPolicy = MethodOrderPolicy.Declared)
+ MethodOrderPolicy methodOrderPolicy = MethodOrderPolicy.Declared,
+ JobOrderPolicy jobOrderPolicy = JobOrderPolicy.Default)
{
- Config = ManualConfig.CreateEmpty().WithOrderer(new DefaultOrderer(summaryOrderPolicy, methodOrderPolicy));
+ Config = ManualConfig.CreateEmpty().WithOrderer(new DefaultOrderer(summaryOrderPolicy, methodOrderPolicy, jobOrderPolicy));
}
public IConfig Config { get; }
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..7953a036ae 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,26 +13,25 @@
-
-
-
-
-
+
+
+
+
-
-
-
+
+
+
-
+
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/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 f0b9655acc..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);
@@ -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..52a95bc04e 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/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 0fdda7b08d..551c38fa09 100644
--- a/src/BenchmarkDotNet/Configs/DebugConfig.cs
+++ b/src/BenchmarkDotNet/Configs/DebugConfig.cs
@@ -71,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 2323d4fdc4..d188b67415 100644
--- a/src/BenchmarkDotNet/Configs/DefaultConfig.cs
+++ b/src/BenchmarkDotNet/Configs/DefaultConfig.cs
@@ -4,6 +4,7 @@
using System.IO;
using BenchmarkDotNet.Analysers;
using BenchmarkDotNet.Columns;
+using BenchmarkDotNet.Detectors;
using BenchmarkDotNet.Diagnosers;
using BenchmarkDotNet.EventProcessors;
using BenchmarkDotNet.Exporters;
@@ -71,6 +72,7 @@ public IEnumerable GetValidators()
yield return DeferredExecutionValidator.FailOnError;
yield return ParamsAllValuesValidator.FailOnError;
yield return ParamsValidator.FailOnError;
+ yield return RuntimeValidator.DontFailOnError;
}
public IOrderer Orderer => null;
@@ -86,11 +88,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");
diff --git a/src/BenchmarkDotNet/Configs/IConfig.cs b/src/BenchmarkDotNet/Configs/IConfig.cs
index b311c235f5..9e3bf50ce6 100644
--- a/src/BenchmarkDotNet/Configs/IConfig.cs
+++ b/src/BenchmarkDotNet/Configs/IConfig.cs
@@ -55,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 1216ef3b16..0043ecbe4b 100644
--- a/src/BenchmarkDotNet/Configs/ImmutableConfig.cs
+++ b/src/BenchmarkDotNet/Configs/ImmutableConfig.cs
@@ -56,6 +56,7 @@ internal ImmutableConfig(
SummaryStyle summaryStyle,
ConfigOptions options,
TimeSpan buildTimeout,
+ WakeLockType wakeLock,
IReadOnlyList configAnalysisConclusion)
{
columnProviders = uniqueColumnProviders;
@@ -78,6 +79,7 @@ internal ImmutableConfig(
SummaryStyle = summaryStyle;
Options = options;
BuildTimeout = buildTimeout;
+ WakeLock = wakeLock;
ConfigAnalysisConclusion = configAnalysisConclusion;
}
@@ -89,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;
@@ -119,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 d7c0b5eb0f..f93e5590d0 100644
--- a/src/BenchmarkDotNet/Configs/ImmutableConfigBuilder.cs
+++ b/src/BenchmarkDotNet/Configs/ImmutableConfigBuilder.cs
@@ -76,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 5ea1be24e9..4ececd28f1 100644
--- a/src/BenchmarkDotNet/Configs/ManualConfig.cs
+++ b/src/BenchmarkDotNet/Configs/ManualConfig.cs
@@ -58,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;
@@ -109,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);
@@ -273,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);
}
///
@@ -319,6 +327,12 @@ public static ManualConfig Union(IConfig globalConfig, IConfig localConfig)
return manualConfig;
}
+ internal ManualConfig RemoveLoggersOfType()
+ {
+ loggers.RemoveAll(logger => logger is T);
+ return this;
+ }
+
internal void RemoveAllJobs() => jobs.Clear();
internal void RemoveAllDiagnosers() => diagnosers.Clear();
@@ -327,5 +341,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 0a125900f4..ac61ddb5f1 100644
--- a/src/BenchmarkDotNet/ConsoleArguments/ConfigParser.cs
+++ b/src/BenchmarkDotNet/ConsoleArguments/ConfigParser.cs
@@ -1,775 +1,797 @@
-using System;
-using System.Collections.Generic;
-using System.Diagnostics.CodeAnalysis;
-using System.IO;
-using System.Linq;
-using System.Text;
-using BenchmarkDotNet.Columns;
-using BenchmarkDotNet.Configs;
-using BenchmarkDotNet.Diagnosers;
-using BenchmarkDotNet.Environments;
-using BenchmarkDotNet.Exporters;
-using BenchmarkDotNet.Exporters.Csv;
-using BenchmarkDotNet.Exporters.Json;
-using BenchmarkDotNet.Exporters.Xml;
-using BenchmarkDotNet.Extensions;
-using BenchmarkDotNet.Filters;
-using BenchmarkDotNet.Jobs;
-using BenchmarkDotNet.Loggers;
-using BenchmarkDotNet.Portability;
-using BenchmarkDotNet.Reports;
-using BenchmarkDotNet.Toolchains.CoreRun;
-using BenchmarkDotNet.Toolchains.CsProj;
-using BenchmarkDotNet.Toolchains.DotNetCli;
-using BenchmarkDotNet.Toolchains.InProcess.Emit;
-using BenchmarkDotNet.Toolchains.MonoAotLLVM;
-using BenchmarkDotNet.Toolchains.MonoWasm;
-using BenchmarkDotNet.Toolchains.NativeAot;
-using CommandLine;
-using Perfolizer.Horology;
-using Perfolizer.Mathematics.OutlierDetection;
-using Perfolizer.Mathematics.SignificanceTesting;
-using Perfolizer.Mathematics.Thresholds;
-using BenchmarkDotNet.Toolchains.Mono;
-
-namespace BenchmarkDotNet.ConsoleArguments
-{
- public static class ConfigParser
- {
- private const int MinimumDisplayWidth = 80;
- private const char EnvVarKeyValueSeparator = ':';
-
- private static readonly IReadOnlyDictionary AvailableJobs = new Dictionary(StringComparer.InvariantCultureIgnoreCase)
- {
- { "default", Job.Default },
- { "dry", Job.Dry },
- { "short", Job.ShortRun },
- { "medium", Job.MediumRun },
- { "long", Job.LongRun },
- { "verylong", Job.VeryLongRun }
- };
-
- [SuppressMessage("ReSharper", "StringLiteralTypo")]
- [SuppressMessage("ReSharper", "CoVariantArrayConversion")]
- private static readonly IReadOnlyDictionary AvailableExporters =
- new Dictionary(StringComparer.InvariantCultureIgnoreCase)
- {
- { "csv", new[] { CsvExporter.Default } },
- { "csvmeasurements", new[] { CsvMeasurementsExporter.Default } },
- { "html", new[] { HtmlExporter.Default } },
- { "markdown", new[] { MarkdownExporter.Default } },
- { "atlassian", new[] { MarkdownExporter.Atlassian } },
- { "stackoverflow", new[] { MarkdownExporter.StackOverflow } },
- { "github", new[] { MarkdownExporter.GitHub } },
- { "plain", new[] { PlainExporter.Default } },
- { "rplot", new[] { CsvMeasurementsExporter.Default, RPlotExporter.Default } }, // R Plots depends on having the full measurements available
- { "json", new[] { JsonExporter.Default } },
- { "briefjson", new[] { JsonExporter.Brief } },
- { "fulljson", new[] { JsonExporter.Full } },
- { "asciidoc", new[] { AsciiDocExporter.Default } },
- { "xml", new[] { XmlExporter.Default } },
- { "briefxml", new[] { XmlExporter.Brief } },
- { "fullxml", new[] { XmlExporter.Full } }
- };
-
- public static (bool isSuccess, IConfig config, CommandLineOptions options) Parse(string[] args, ILogger logger, IConfig? globalConfig = null)
- {
- (bool isSuccess, IConfig config, CommandLineOptions options) result = default;
-
- var (expandSuccess, expandedArgs) = ExpandResponseFile(args, logger);
- if (!expandSuccess)
- {
- return (false, default, default);
- }
-
- args = expandedArgs;
- using (var parser = CreateParser(logger))
- {
- parser
- .ParseArguments(args)
- .WithParsed(options => result = Validate(options, logger) ? (true, CreateConfig(options, globalConfig, args), options) : (false, default, default))
- .WithNotParsed(errors => result = (false, default, default));
- }
-
- return result;
- }
-
- private static (bool Success, string[] ExpandedTokens) ExpandResponseFile(string[] args, ILogger logger)
- {
- List result = new ();
- foreach (var arg in args)
- {
- if (arg.StartsWith("@"))
- {
- var fileName = arg.Substring(1);
- try
- {
- if (File.Exists(fileName))
- {
- var lines = File.ReadAllLines(fileName);
- foreach (var line in lines)
- {
- result.AddRange(ConsumeTokens(line));
- }
- }
- else
- {
- logger.WriteLineError($"Response file {fileName} does not exists.");
- return (false, Array.Empty());
- }
- }
- catch (Exception ex)
- {
- logger.WriteLineError($"Failed to parse RSP file: {fileName}, {ex.Message}");
- return (false, Array.Empty());
- }
- }
- else
- {
- result.Add(arg);
- }
- }
-
- return (true, result.ToArray());
- }
-
- private static IEnumerable ConsumeTokens(string line)
- {
- bool insideQuotes = false;
- var token = new StringBuilder();
- for (int i = 0; i < line.Length; i++)
- {
- char currentChar = line[i];
- if (currentChar == ' ' && !insideQuotes)
- {
- if (token.Length > 0)
- {
- yield return GetToken();
- token = new StringBuilder();
- }
-
- continue;
- }
-
- if (currentChar == '"')
- {
- insideQuotes = !insideQuotes;
- continue;
- }
-
- if (currentChar == '\\' && insideQuotes)
- {
- if (line[i + 1] == '"')
- {
- insideQuotes = false;
- i++;
- continue;
- }
-
- if (line[i + 1] == '\\')
- {
- token.Append('\\');
- i++;
- continue;
- }
- }
-
- token.Append(currentChar);
- }
-
- if (token.Length > 0)
- {
- yield return GetToken();
- }
-
- string GetToken()
- {
- var result = token.ToString();
- if (result.Contains(' '))
- {
- // Workaround for CommandLine library issue with parsing these kind of args.
- return " " + result;
- }
-
- return result;
- }
- }
-
- internal static bool TryUpdateArgs(string[] args, out string[]? updatedArgs, Action updater)
- {
- (bool isSuccess, CommandLineOptions options) result = default;
-
- ILogger logger = NullLogger.Instance;
- using (var parser = CreateParser(logger))
- {
- parser
- .ParseArguments(args)
- .WithParsed(options => result = Validate(options, logger) ? (true, options) : (false, default))
- .WithNotParsed(errors => result = (false, default));
-
- if (!result.isSuccess)
- {
- updatedArgs = null;
- return false;
- }
-
- updater(result.options);
-
- updatedArgs = parser.FormatCommandLine(result.options, settings => settings.SkipDefault = true).Split();
- return true;
- }
- }
-
- private static Parser CreateParser(ILogger logger)
- => new Parser(settings =>
- {
- settings.CaseInsensitiveEnumValues = true;
- settings.CaseSensitive = false;
- settings.EnableDashDash = true;
- settings.IgnoreUnknownArguments = false;
- settings.HelpWriter = new LoggerWrapper(logger);
- settings.MaximumDisplayWidth = Math.Max(MinimumDisplayWidth, GetMaximumDisplayWidth());
- });
-
- private static bool Validate(CommandLineOptions options, ILogger logger)
- {
- if (!AvailableJobs.ContainsKey(options.BaseJob))
- {
- logger.WriteLineError($"The provided base job \"{options.BaseJob}\" is invalid. Available options are: {string.Join(", ", AvailableJobs.Keys)}.");
- return false;
- }
-
- foreach (string runtime in options.Runtimes)
- {
- if (!TryParse(runtime, out RuntimeMoniker runtimeMoniker))
- {
- logger.WriteLineError($"The provided runtime \"{runtime}\" is invalid. Available options are: {string.Join(", ", Enum.GetNames(typeof(RuntimeMoniker)).Select(name => name.ToLower()))}.");
- return false;
- }
- else if (runtimeMoniker == RuntimeMoniker.MonoAOTLLVM && (options.AOTCompilerPath == null || options.AOTCompilerPath.IsNotNullButDoesNotExist()))
- {
- logger.WriteLineError($"The provided {nameof(options.AOTCompilerPath)} \"{options.AOTCompilerPath}\" does NOT exist. It MUST be provided.");
- }
- }
-
- foreach (string exporter in options.Exporters)
- if (!AvailableExporters.ContainsKey(exporter))
- {
- logger.WriteLineError($"The provided exporter \"{exporter}\" is invalid. Available options are: {string.Join(", ", AvailableExporters.Keys)}.");
- return false;
- }
-
- if (options.CliPath.IsNotNullButDoesNotExist())
- {
- logger.WriteLineError($"The provided {nameof(options.CliPath)} \"{options.CliPath}\" does NOT exist.");
- return false;
- }
-
- foreach (var coreRunPath in options.CoreRunPaths)
- if (coreRunPath.IsNotNullButDoesNotExist())
- {
- if (Directory.Exists(coreRunPath.FullName))
- {
- logger.WriteLineError($"The provided path to CoreRun: \"{coreRunPath}\" exists but it's a directory, not an executable. You need to include CoreRun.exe (corerun on Unix) in the path.");
- }
- else
- {
- logger.WriteLineError($"The provided path to CoreRun: \"{coreRunPath}\" does NOT exist.");
- }
-
- return false;
- }
-
- if (options.MonoPath.IsNotNullButDoesNotExist())
- {
- logger.WriteLineError($"The provided {nameof(options.MonoPath)} \"{options.MonoPath}\" does NOT exist.");
- return false;
- }
-
- if (options.WasmJavascriptEngine.IsNotNullButDoesNotExist())
- {
- logger.WriteLineError($"The provided {nameof(options.WasmJavascriptEngine)} \"{options.WasmJavascriptEngine}\" does NOT exist.");
- return false;
- }
-
- if (options.IlcPackages.IsNotNullButDoesNotExist())
- {
- logger.WriteLineError($"The provided {nameof(options.IlcPackages)} \"{options.IlcPackages}\" does NOT exist.");
- return false;
- }
-
- if (options.HardwareCounters.Count() > 3)
- {
- logger.WriteLineError("You can't use more than 3 HardwareCounters at the same time.");
- return false;
- }
-
- foreach (var counterName in options.HardwareCounters)
- if (!Enum.TryParse(counterName, ignoreCase: true, out HardwareCounter _))
- {
- logger.WriteLineError($"The provided hardware counter \"{counterName}\" is invalid. Available options are: {string.Join("+", Enum.GetNames(typeof(HardwareCounter)))}.");
- return false;
- }
-
- if (!string.IsNullOrEmpty(options.StatisticalTestThreshold) && !Threshold.TryParse(options.StatisticalTestThreshold, out _))
- {
- logger.WriteLineError("Invalid Threshold for Statistical Test. Use --help to see examples.");
- return false;
- }
-
- if (options.EnvironmentVariables.Any(envVar => envVar.IndexOf(EnvVarKeyValueSeparator) <= 0))
- {
- logger.WriteLineError($"Environment variable value must be separated from the key using '{EnvVarKeyValueSeparator}'. Use --help to see examples.");
- return false;
- }
-
- return true;
- }
-
- private static IConfig CreateConfig(CommandLineOptions options, IConfig globalConfig, string[] args)
- {
- var config = new ManualConfig();
-
- var baseJob = GetBaseJob(options, globalConfig);
- var expanded = Expand(baseJob.UnfreezeCopy(), options, args).ToArray(); // UnfreezeCopy ensures that each of the expanded jobs will have it's own ID
- if (expanded.Length > 1)
- expanded[0] = expanded[0].AsBaseline(); // if the user provides multiple jobs, then the first one should be a baseline
- config.AddJob(expanded);
- if (config.GetJobs().IsEmpty() && baseJob != Job.Default)
- config.AddJob(baseJob);
-
- config.AddExporter(options.Exporters.SelectMany(exporter => AvailableExporters[exporter]).ToArray());
-
- config.AddHardwareCounters(options.HardwareCounters
- .Select(counterName => (HardwareCounter)Enum.Parse(typeof(HardwareCounter), counterName, ignoreCase: true))
- .ToArray());
-
- if (options.UseMemoryDiagnoser)
- config.AddDiagnoser(MemoryDiagnoser.Default);
- if (options.UseThreadingDiagnoser)
- config.AddDiagnoser(ThreadingDiagnoser.Default);
- if (options.UseExceptionDiagnoser)
- config.AddDiagnoser(ExceptionDiagnoser.Default);
- if (options.UseDisassemblyDiagnoser)
- config.AddDiagnoser(new DisassemblyDiagnoser(new DisassemblyDiagnoserConfig(
- maxDepth: options.DisassemblerRecursiveDepth,
- filters: options.DisassemblerFilters.ToArray(),
- exportDiff: options.DisassemblerDiff)));
- if (!string.IsNullOrEmpty(options.Profiler))
- config.AddDiagnoser(DiagnosersLoader.GetImplementation(profiler => profiler.ShortName.EqualsWithIgnoreCase(options.Profiler)));
-
- 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));
-
- if (options.ArtifactsDirectory != null)
- config.ArtifactsPath = options.ArtifactsDirectory.FullName;
-
- var filters = GetFilters(options).ToArray();
- if (filters.Length > 1)
- config.AddFilter(new UnionFilter(filters));
- else
- config.AddFilter(filters);
-
- config.HideColumns(options.HiddenColumns.ToArray());
-
- config.WithOption(ConfigOptions.JoinSummary, options.Join);
- config.WithOption(ConfigOptions.KeepBenchmarkFiles, options.KeepBenchmarkFiles);
- config.WithOption(ConfigOptions.DontOverwriteResults, options.DontOverwriteResults);
- config.WithOption(ConfigOptions.StopOnFirstError, options.StopOnFirstError);
- config.WithOption(ConfigOptions.DisableLogFile, options.DisableLogFile);
- config.WithOption(ConfigOptions.LogBuildOutput, options.LogBuildOutput);
- config.WithOption(ConfigOptions.GenerateMSBuildBinLog, options.GenerateMSBuildBinLog);
- config.WithOption(ConfigOptions.ApplesToApples, options.ApplesToApples);
- config.WithOption(ConfigOptions.Resume, options.Resume);
-
- if (config.Options.IsSet(ConfigOptions.GenerateMSBuildBinLog))
- config.Options |= ConfigOptions.KeepBenchmarkFiles;
-
- if (options.MaxParameterColumnWidth.HasValue)
- config.WithSummaryStyle(SummaryStyle.Default.WithMaxParameterColumnWidth(options.MaxParameterColumnWidth.Value));
-
- if (options.TimeOutInSeconds.HasValue)
- config.WithBuildTimeout(TimeSpan.FromSeconds(options.TimeOutInSeconds.Value));
-
- return config;
- }
-
- private static Job GetBaseJob(CommandLineOptions options, IConfig globalConfig)
- {
- var baseJob =
- globalConfig?.GetJobs().SingleOrDefault(job => job.Meta.IsDefault) // global config might define single custom Default job
- ?? AvailableJobs[options.BaseJob.ToLowerInvariant()];
-
- if (baseJob != Job.Dry && options.Outliers != OutlierMode.RemoveUpper)
- baseJob = baseJob.WithOutlierMode(options.Outliers);
-
- if (options.Affinity.HasValue)
- baseJob = baseJob.WithAffinity((IntPtr)options.Affinity.Value);
-
- if (options.LaunchCount.HasValue)
- baseJob = baseJob.WithLaunchCount(options.LaunchCount.Value);
- if (options.WarmupIterationCount.HasValue)
- baseJob = baseJob.WithWarmupCount(options.WarmupIterationCount.Value);
- if (options.MinWarmupIterationCount.HasValue)
- baseJob = baseJob.WithMinWarmupCount(options.MinWarmupIterationCount.Value);
- if (options.MaxWarmupIterationCount.HasValue)
- baseJob = baseJob.WithMaxWarmupCount(options.MaxWarmupIterationCount.Value);
- if (options.IterationTimeInMilliseconds.HasValue)
- baseJob = baseJob.WithIterationTime(TimeInterval.FromMilliseconds(options.IterationTimeInMilliseconds.Value));
- if (options.IterationCount.HasValue)
- baseJob = baseJob.WithIterationCount(options.IterationCount.Value);
- if (options.MinIterationCount.HasValue)
- baseJob = baseJob.WithMinIterationCount(options.MinIterationCount.Value);
- if (options.MaxIterationCount.HasValue)
- baseJob = baseJob.WithMaxIterationCount(options.MaxIterationCount.Value);
- if (options.InvocationCount.HasValue)
- baseJob = baseJob.WithInvocationCount(options.InvocationCount.Value);
- if (options.UnrollFactor.HasValue)
- baseJob = baseJob.WithUnrollFactor(options.UnrollFactor.Value);
- if (options.RunStrategy.HasValue)
- baseJob = baseJob.WithStrategy(options.RunStrategy.Value);
- if (options.Platform.HasValue)
- baseJob = baseJob.WithPlatform(options.Platform.Value);
- if (options.RunOncePerIteration)
- baseJob = baseJob.RunOncePerIteration();
- if (options.MemoryRandomization)
- baseJob = baseJob.WithMemoryRandomization();
- if (options.NoForcedGCs)
- baseJob = baseJob.WithGcForce(false);
- if (options.NoEvaluationOverhead)
- baseJob = baseJob.WithEvaluateOverhead(false);
-
- if (options.EnvironmentVariables.Any())
- {
- baseJob = baseJob.WithEnvironmentVariables(options.EnvironmentVariables.Select(text =>
- {
- var separated = text.Split(new[] { EnvVarKeyValueSeparator }, 2);
- return new EnvironmentVariable(separated[0], separated[1]);
- }).ToArray());
- }
-
- if (AvailableJobs.Values.Contains(baseJob)) // no custom settings
- return baseJob;
-
- return baseJob
- .AsDefault(false) // after applying all settings from console args the base job is not default anymore
- .AsMutator(); // we mark it as mutator so it will be applied to other jobs defined via attributes and merged later in GetRunnableJobs method
- }
-
- private static IEnumerable Expand(Job baseJob, CommandLineOptions options, string[] args)
- {
- if (options.RunInProcess)
- {
- yield return baseJob.WithToolchain(InProcessEmitToolchain.Instance);
- }
- else if (!string.IsNullOrEmpty(options.ClrVersion))
- {
- yield return baseJob.WithRuntime(ClrRuntime.CreateForLocalFullNetFrameworkBuild(options.ClrVersion)); // local builds of .NET Runtime
- }
- else if (options.CliPath != null && options.Runtimes.IsEmpty() && options.CoreRunPaths.IsEmpty())
- {
- yield return CreateCoreJobWithCli(baseJob, options);
- }
- else
- {
- // in case both --runtimes and --corerun are specified, the first one is returned first and becomes a baseline job
- string first = args.FirstOrDefault(arg =>
- arg.Equals("--runtimes", StringComparison.OrdinalIgnoreCase)
- || arg.Equals("-r", StringComparison.OrdinalIgnoreCase)
-
- || arg.Equals("--corerun", StringComparison.OrdinalIgnoreCase));
-
- if (first is null || first.Equals("--corerun", StringComparison.OrdinalIgnoreCase))
- {
- foreach (var coreRunPath in options.CoreRunPaths)
- yield return CreateCoreRunJob(baseJob, options, coreRunPath); // local dotnet/runtime builds
-
- foreach (string runtime in options.Runtimes) // known runtimes
- yield return CreateJobForGivenRuntime(baseJob, runtime, options);
- }
- else
- {
- foreach (string runtime in options.Runtimes) // known runtimes
- yield return CreateJobForGivenRuntime(baseJob, runtime, options);
-
- foreach (var coreRunPath in options.CoreRunPaths)
- yield return CreateCoreRunJob(baseJob, options, coreRunPath); // local dotnet/runtime builds
- }
- }
- }
-
- private static Job CreateJobForGivenRuntime(Job baseJob, string runtimeId, CommandLineOptions options)
- {
- if (!TryParse(runtimeId, out RuntimeMoniker runtimeMoniker))
- {
- throw new InvalidOperationException("Impossible, already validated by the Validate method");
- }
-
- switch (runtimeMoniker)
- {
- case RuntimeMoniker.Net461:
- case RuntimeMoniker.Net462:
- case RuntimeMoniker.Net47:
- case RuntimeMoniker.Net471:
- case RuntimeMoniker.Net472:
- case RuntimeMoniker.Net48:
- case RuntimeMoniker.Net481:
- return baseJob
- .WithRuntime(runtimeMoniker.GetRuntime())
- .WithToolchain(CsProjClassicNetToolchain.From(runtimeId, options.RestorePath?.FullName, options.CliPath?.FullName));
-
- case RuntimeMoniker.NetCoreApp20:
- case RuntimeMoniker.NetCoreApp21:
- case RuntimeMoniker.NetCoreApp22:
- case RuntimeMoniker.NetCoreApp30:
- case RuntimeMoniker.NetCoreApp31:
-#pragma warning disable CS0618 // Type or member is obsolete
- case RuntimeMoniker.NetCoreApp50:
-#pragma warning restore CS0618 // Type or member is obsolete
- case RuntimeMoniker.Net50:
- case RuntimeMoniker.Net60:
- case RuntimeMoniker.Net70:
- case RuntimeMoniker.Net80:
- case RuntimeMoniker.Net90:
- return baseJob
- .WithRuntime(runtimeMoniker.GetRuntime())
- .WithToolchain(CsProjCoreToolchain.From(new NetCoreAppSettings(runtimeId, null, runtimeId, options.CliPath?.FullName, options.RestorePath?.FullName)));
-
- case RuntimeMoniker.Mono:
- return baseJob.WithRuntime(new MonoRuntime("Mono", options.MonoPath?.FullName));
-
- case RuntimeMoniker.NativeAot60:
- 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");
-
- case RuntimeMoniker.NativeAot80:
- return CreateAotJob(baseJob, options, runtimeMoniker, "", "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet8/nuget/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.Wasm:
- return MakeWasmJob(baseJob, options, RuntimeInformation.IsNetCore ? CoreRuntime.GetCurrentVersion().MsBuildMoniker : "net5.0", runtimeMoniker);
-
- case RuntimeMoniker.WasmNet50:
- return MakeWasmJob(baseJob, options, "net5.0", runtimeMoniker);
-
- case RuntimeMoniker.WasmNet60:
- return MakeWasmJob(baseJob, options, "net6.0", runtimeMoniker);
-
- case RuntimeMoniker.WasmNet70:
- return MakeWasmJob(baseJob, options, "net7.0", runtimeMoniker);
-
- case RuntimeMoniker.WasmNet80:
- return MakeWasmJob(baseJob, options, "net8.0", runtimeMoniker);
-
- case RuntimeMoniker.WasmNet90:
- return MakeWasmJob(baseJob, options, "net9.0", runtimeMoniker);
-
- case RuntimeMoniker.MonoAOTLLVM:
- return MakeMonoAOTLLVMJob(baseJob, options, RuntimeInformation.IsNetCore ? CoreRuntime.GetCurrentVersion().MsBuildMoniker : "net6.0");
-
- case RuntimeMoniker.MonoAOTLLVMNet60:
- return MakeMonoAOTLLVMJob(baseJob, options, "net6.0");
-
- case RuntimeMoniker.MonoAOTLLVMNet70:
- return MakeMonoAOTLLVMJob(baseJob, options, "net7.0");
-
- case RuntimeMoniker.MonoAOTLLVMNet80:
- return MakeMonoAOTLLVMJob(baseJob, options, "net8.0");
-
- case RuntimeMoniker.MonoAOTLLVMNet90:
- return MakeMonoAOTLLVMJob(baseJob, options, "net9.0");
-
- case RuntimeMoniker.Mono60:
- return MakeMonoJob(baseJob, options, MonoRuntime.Mono60);
-
- case RuntimeMoniker.Mono70:
- return MakeMonoJob(baseJob, options, MonoRuntime.Mono70);
-
- case RuntimeMoniker.Mono80:
- return MakeMonoJob(baseJob, options, MonoRuntime.Mono80);
-
- case RuntimeMoniker.Mono90:
- return MakeMonoJob(baseJob, options, MonoRuntime.Mono90);
-
- default:
- throw new NotSupportedException($"Runtime {runtimeId} is not supported");
- }
- }
-
- private static Job CreateAotJob(Job baseJob, CommandLineOptions options, RuntimeMoniker runtimeMoniker, string ilCompilerVersion, string nuGetFeedUrl)
- {
- var builder = NativeAotToolchain.CreateBuilder();
-
- if (options.CliPath != null)
- builder.DotNetCli(options.CliPath.FullName);
- if (options.RestorePath != null)
- builder.PackagesRestorePath(options.RestorePath.FullName);
-
- if (options.IlcPackages != null)
- builder.UseLocalBuild(options.IlcPackages);
- else if (!string.IsNullOrEmpty(options.ILCompilerVersion))
- builder.UseNuGet(options.ILCompilerVersion, nuGetFeedUrl);
- else
- builder.UseNuGet(ilCompilerVersion, nuGetFeedUrl);
-
- var runtime = runtimeMoniker.GetRuntime();
- builder.TargetFrameworkMoniker(runtime.MsBuildMoniker);
-
- return baseJob.WithRuntime(runtime).WithToolchain(builder.ToToolchain());
- }
-
- private static Job MakeMonoJob(Job baseJob, CommandLineOptions options, MonoRuntime runtime)
- {
- return baseJob
- .WithRuntime(runtime)
- .WithToolchain(MonoToolchain.From(
- new NetCoreAppSettings(
- targetFrameworkMoniker: runtime.MsBuildMoniker,
- runtimeFrameworkVersion: null,
- name: runtime.Name,
- customDotNetCliPath: options.CliPath?.FullName,
- packagesPath: options.RestorePath?.FullName)));
- }
-
- private static Job MakeMonoAOTLLVMJob(Job baseJob, CommandLineOptions options, string msBuildMoniker)
- {
- var monoAotLLVMRuntime = new MonoAotLLVMRuntime(aotCompilerPath: options.AOTCompilerPath, aotCompilerMode: options.AOTCompilerMode, msBuildMoniker: msBuildMoniker);
-
- var toolChain = MonoAotLLVMToolChain.From(
- new NetCoreAppSettings(
- targetFrameworkMoniker: monoAotLLVMRuntime.MsBuildMoniker,
- runtimeFrameworkVersion: null,
- name: monoAotLLVMRuntime.Name,
- customDotNetCliPath: options.CliPath?.FullName,
- packagesPath: options.RestorePath?.FullName,
- customRuntimePack: options.CustomRuntimePack,
- aotCompilerPath: options.AOTCompilerPath.ToString(),
- aotCompilerMode: options.AOTCompilerMode));
-
- return baseJob.WithRuntime(monoAotLLVMRuntime).WithToolchain(toolChain);
- }
-
- private static Job MakeWasmJob(Job baseJob, CommandLineOptions options, string msBuildMoniker, RuntimeMoniker moniker)
- {
- bool wasmAot = options.AOTCompilerMode == MonoAotCompilerMode.wasm;
-
- var wasmRuntime = new WasmRuntime(
- msBuildMoniker: msBuildMoniker,
- javaScriptEngine: options.WasmJavascriptEngine?.FullName ?? "v8",
- javaScriptEngineArguments: options.WasmJavaScriptEngineArguments,
- aot: wasmAot,
- wasmDataDir: options.WasmDataDirectory?.FullName,
- moniker: moniker);
-
- var toolChain = WasmToolchain.From(new NetCoreAppSettings(
- targetFrameworkMoniker: wasmRuntime.MsBuildMoniker,
- runtimeFrameworkVersion: null,
- name: wasmRuntime.Name,
- customDotNetCliPath: options.CliPath?.FullName,
- packagesPath: options.RestorePath?.FullName,
- customRuntimePack: options.CustomRuntimePack,
- aotCompilerMode: options.AOTCompilerMode));
-
- return baseJob.WithRuntime(wasmRuntime).WithToolchain(toolChain);
- }
-
- private static IEnumerable GetFilters(CommandLineOptions options)
- {
- if (options.Filters.Any())
- yield return new GlobFilter(options.Filters.ToArray());
- if (options.AllCategories.Any())
- yield return new AllCategoriesFilter(options.AllCategories.ToArray());
- if (options.AnyCategories.Any())
- yield return new AnyCategoriesFilter(options.AnyCategories.ToArray());
- if (options.AttributeNames.Any())
- yield return new AttributesFilter(options.AttributeNames.ToArray());
- }
-
- private static int GetMaximumDisplayWidth()
- {
- try
- {
- return Console.WindowWidth;
- }
- catch (IOException)
- {
- return MinimumDisplayWidth;
- }
- }
-
- private static Job CreateCoreRunJob(Job baseJob, CommandLineOptions options, FileInfo coreRunPath)
- => baseJob
- .WithToolchain(new CoreRunToolchain(
- coreRunPath,
- createCopy: true,
- targetFrameworkMoniker:
- RuntimeInformation.IsNetCore
- ? RuntimeInformation.GetCurrentRuntime().MsBuildMoniker
- : CoreRuntime.Latest.MsBuildMoniker, // use most recent tfm, as the toolchain is being used only by dotnet/runtime contributors
- customDotNetCliPath: options.CliPath,
- restorePath: options.RestorePath,
- displayName: GetCoreRunToolchainDisplayName(options.CoreRunPaths, coreRunPath)));
-
- private static Job CreateCoreJobWithCli(Job baseJob, CommandLineOptions options)
- => baseJob
- .WithToolchain(CsProjCoreToolchain.From(
- new NetCoreAppSettings(
- targetFrameworkMoniker: RuntimeInformation.GetCurrentRuntime().MsBuildMoniker,
- customDotNetCliPath: options.CliPath?.FullName,
- runtimeFrameworkVersion: null,
- name: RuntimeInformation.GetCurrentRuntime().Name,
- packagesPath: options.RestorePath?.FullName)));
-
- ///
- /// we have a limited amount of space when printing the output to the console, so we try to keep things small and simple
- ///
- /// for following paths:
- /// C:\Projects\coreclr_upstream\bin\tests\Windows_NT.x64.Release\Tests\Core_Root\CoreRun.exe
- /// C:\Projects\coreclr_upstream\bin\tests\Windows_NT.x64.Release\Tests\Core_Root_beforeMyChanges\CoreRun.exe
- ///
- /// we get:
- ///
- /// \Core_Root\CoreRun.exe
- /// \Core_Root_beforeMyChanges\CoreRun.exe
- ///
- private static string GetCoreRunToolchainDisplayName(IReadOnlyList paths, FileInfo coreRunPath)
- {
- if (paths.Count <= 1)
- return "CoreRun";
-
- int commonLongestPrefixIndex = paths[0].FullName.Length;
- for (int i = 1; i < paths.Count; i++)
- {
- commonLongestPrefixIndex = Math.Min(commonLongestPrefixIndex, paths[i].FullName.Length);
- for (int j = 0; j < commonLongestPrefixIndex; j++)
- if (paths[i].FullName[j] != paths[0].FullName[j])
- {
- commonLongestPrefixIndex = j;
- break;
- }
- }
-
- if (commonLongestPrefixIndex <= 1)
- return coreRunPath.FullName;
-
- var lastCommonDirectorySeparatorIndex = coreRunPath.FullName.LastIndexOf(Path.DirectorySeparatorChar, commonLongestPrefixIndex - 1);
-
- return coreRunPath.FullName.Substring(lastCommonDirectorySeparatorIndex);
- }
-
- private static bool TryParse(string runtime, out RuntimeMoniker runtimeMoniker)
- {
- int index = runtime.IndexOf('-');
-
- 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);
- }
- }
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.IO;
+using System.Linq;
+using System.Text;
+using BenchmarkDotNet.Columns;
+using BenchmarkDotNet.Configs;
+using BenchmarkDotNet.Diagnosers;
+using BenchmarkDotNet.Environments;
+using BenchmarkDotNet.Exporters;
+using BenchmarkDotNet.Exporters.Csv;
+using BenchmarkDotNet.Exporters.Json;
+using BenchmarkDotNet.Exporters.Xml;
+using BenchmarkDotNet.Extensions;
+using BenchmarkDotNet.Filters;
+using BenchmarkDotNet.Jobs;
+using BenchmarkDotNet.Loggers;
+using BenchmarkDotNet.Portability;
+using BenchmarkDotNet.Reports;
+using BenchmarkDotNet.Toolchains.CoreRun;
+using BenchmarkDotNet.Toolchains.CsProj;
+using BenchmarkDotNet.Toolchains.DotNetCli;
+using BenchmarkDotNet.Toolchains.InProcess.Emit;
+using BenchmarkDotNet.Toolchains.MonoAotLLVM;
+using BenchmarkDotNet.Toolchains.MonoWasm;
+using BenchmarkDotNet.Toolchains.NativeAot;
+using CommandLine;
+using Perfolizer.Horology;
+using Perfolizer.Mathematics.OutlierDetection;
+using BenchmarkDotNet.Toolchains.Mono;
+using Perfolizer.Metrology;
+
+namespace BenchmarkDotNet.ConsoleArguments
+{
+ public static class ConfigParser
+ {
+ private const int MinimumDisplayWidth = 80;
+ private const char EnvVarKeyValueSeparator = ':';
+
+ private static readonly IReadOnlyDictionary AvailableJobs = new Dictionary(StringComparer.InvariantCultureIgnoreCase)
+ {
+ { "default", Job.Default },
+ { "dry", Job.Dry },
+ { "short", Job.ShortRun },
+ { "medium", Job.MediumRun },
+ { "long", Job.LongRun },
+ { "verylong", Job.VeryLongRun }
+ };
+
+ [SuppressMessage("ReSharper", "StringLiteralTypo")]
+ [SuppressMessage("ReSharper", "CoVariantArrayConversion")]
+ private static readonly IReadOnlyDictionary AvailableExporters =
+ new Dictionary(StringComparer.InvariantCultureIgnoreCase)
+ {
+ { "csv", new[] { CsvExporter.Default } },
+ { "csvmeasurements", new[] { CsvMeasurementsExporter.Default } },
+ { "html", new[] { HtmlExporter.Default } },
+ { "markdown", new[] { MarkdownExporter.Default } },
+ { "atlassian", new[] { MarkdownExporter.Atlassian } },
+ { "stackoverflow", new[] { MarkdownExporter.StackOverflow } },
+ { "github", new[] { MarkdownExporter.GitHub } },
+ { "plain", new[] { PlainExporter.Default } },
+ { "rplot", new[] { CsvMeasurementsExporter.Default, RPlotExporter.Default } }, // R Plots depends on having the full measurements available
+ { "json", new[] { JsonExporter.Default } },
+ { "briefjson", new[] { JsonExporter.Brief } },
+ { "fulljson", new[] { JsonExporter.Full } },
+ { "asciidoc", new[] { AsciiDocExporter.Default } },
+ { "xml", new[] { XmlExporter.Default } },
+ { "briefxml", new[] { XmlExporter.Brief } },
+ { "fullxml", new[] { XmlExporter.Full } }
+ };
+
+ public static (bool isSuccess, IConfig config, CommandLineOptions options) Parse(string[] args, ILogger logger, IConfig? globalConfig = null)
+ {
+ (bool isSuccess, IConfig config, CommandLineOptions options) result = default;
+
+ var (expandSuccess, expandedArgs) = ExpandResponseFile(args, logger);
+ if (!expandSuccess)
+ {
+ return (false, default, default);
+ }
+
+ args = expandedArgs;
+ using (var parser = CreateParser(logger))
+ {
+ parser
+ .ParseArguments(args)
+ .WithParsed(options => result = Validate(options, logger) ? (true, CreateConfig(options, globalConfig, args), options) : (false, default, default))
+ .WithNotParsed(errors => result = (false, default, default));
+ }
+
+ return result;
+ }
+
+ private static (bool Success, string[] ExpandedTokens) ExpandResponseFile(string[] args, ILogger logger)
+ {
+ List result = new();
+ foreach (var arg in args)
+ {
+ if (arg.StartsWith("@"))
+ {
+ var fileName = arg.Substring(1);
+ try
+ {
+ if (File.Exists(fileName))
+ {
+ var lines = File.ReadAllLines(fileName);
+ foreach (var line in lines)
+ {
+ result.AddRange(ConsumeTokens(line));
+ }
+ }
+ else
+ {
+ logger.WriteLineError($"Response file {fileName} does not exists.");
+ return (false, Array.Empty());
+ }
+ }
+ catch (Exception ex)
+ {
+ logger.WriteLineError($"Failed to parse RSP file: {fileName}, {ex.Message}");
+ return (false, Array.Empty());
+ }
+ }
+ else
+ {
+ result.Add(arg);
+ }
+ }
+
+ return (true, result.ToArray());
+ }
+
+ private static IEnumerable ConsumeTokens(string line)
+ {
+ bool insideQuotes = false;
+ var token = new StringBuilder();
+ for (int i = 0; i < line.Length; i++)
+ {
+ char currentChar = line[i];
+ if (currentChar == ' ' && !insideQuotes)
+ {
+ if (token.Length > 0)
+ {
+ yield return GetToken();
+ token = new StringBuilder();
+ }
+
+ continue;
+ }
+
+ if (currentChar == '"')
+ {
+ insideQuotes = !insideQuotes;
+ continue;
+ }
+
+ if (currentChar == '\\' && insideQuotes)
+ {
+ if (line[i + 1] == '"')
+ {
+ insideQuotes = false;
+ i++;
+ continue;
+ }
+
+ if (line[i + 1] == '\\')
+ {
+ token.Append('\\');
+ i++;
+ continue;
+ }
+ }
+
+ token.Append(currentChar);
+ }
+
+ if (token.Length > 0)
+ {
+ yield return GetToken();
+ }
+
+ string GetToken()
+ {
+ var result = token.ToString();
+ if (result.Contains(' '))
+ {
+ // Workaround for CommandLine library issue with parsing these kind of args.
+ return " " + result;
+ }
+
+ return result;
+ }
+ }
+
+ internal static bool TryUpdateArgs(string[] args, out string[]? updatedArgs, Action updater)
+ {
+ (bool isSuccess, CommandLineOptions options) result = default;
+
+ ILogger logger = NullLogger.Instance;
+ using (var parser = CreateParser(logger))
+ {
+ parser
+ .ParseArguments(args)
+ .WithParsed(options => result = Validate(options, logger) ? (true, options) : (false, default))
+ .WithNotParsed(errors => result = (false, default));
+
+ if (!result.isSuccess)
+ {
+ updatedArgs = null;
+ return false;
+ }
+
+ updater(result.options);
+
+ updatedArgs = parser.FormatCommandLine(result.options, settings => settings.SkipDefault = true).Split();
+ return true;
+ }
+ }
+
+ private static Parser CreateParser(ILogger logger)
+ => new Parser(settings =>
+ {
+ settings.CaseInsensitiveEnumValues = true;
+ settings.CaseSensitive = false;
+ settings.EnableDashDash = true;
+ settings.IgnoreUnknownArguments = false;
+ settings.HelpWriter = new LoggerWrapper(logger);
+ settings.MaximumDisplayWidth = Math.Max(MinimumDisplayWidth, GetMaximumDisplayWidth());
+ });
+
+ private static bool Validate(CommandLineOptions options, ILogger logger)
+ {
+ if (!AvailableJobs.ContainsKey(options.BaseJob))
+ {
+ logger.WriteLineError($"The provided base job \"{options.BaseJob}\" is invalid. Available options are: {string.Join(", ", AvailableJobs.Keys)}.");
+ return false;
+ }
+
+ foreach (string runtime in options.Runtimes)
+ {
+ if (!TryParse(runtime, out RuntimeMoniker runtimeMoniker))
+ {
+ logger.WriteLineError($"The provided runtime \"{runtime}\" is invalid. Available options are: {string.Join(", ", Enum.GetNames(typeof(RuntimeMoniker)).Select(name => name.ToLower()))}.");
+ return false;
+ }
+ else if (runtimeMoniker == RuntimeMoniker.MonoAOTLLVM && (options.AOTCompilerPath == null || options.AOTCompilerPath.IsNotNullButDoesNotExist()))
+ {
+ logger.WriteLineError($"The provided {nameof(options.AOTCompilerPath)} \"{options.AOTCompilerPath}\" does NOT exist. It MUST be provided.");
+ }
+ }
+
+ foreach (string exporter in options.Exporters)
+ if (!AvailableExporters.ContainsKey(exporter))
+ {
+ logger.WriteLineError($"The provided exporter \"{exporter}\" is invalid. Available options are: {string.Join(", ", AvailableExporters.Keys)}.");
+ return false;
+ }
+
+ if (options.CliPath.IsNotNullButDoesNotExist())
+ {
+ logger.WriteLineError($"The provided {nameof(options.CliPath)} \"{options.CliPath}\" does NOT exist.");
+ return false;
+ }
+
+ foreach (var coreRunPath in options.CoreRunPaths)
+ if (coreRunPath.IsNotNullButDoesNotExist())
+ {
+ if (Directory.Exists(coreRunPath.FullName))
+ {
+ logger.WriteLineError($"The provided path to CoreRun: \"{coreRunPath}\" exists but it's a directory, not an executable. You need to include CoreRun.exe (corerun on Unix) in the path.");
+ }
+ else
+ {
+ logger.WriteLineError($"The provided path to CoreRun: \"{coreRunPath}\" does NOT exist.");
+ }
+
+ return false;
+ }
+
+ if (options.MonoPath.IsNotNullButDoesNotExist())
+ {
+ logger.WriteLineError($"The provided {nameof(options.MonoPath)} \"{options.MonoPath}\" does NOT exist.");
+ return false;
+ }
+
+ if (options.WasmJavascriptEngine.IsNotNullButDoesNotExist())
+ {
+ logger.WriteLineError($"The provided {nameof(options.WasmJavascriptEngine)} \"{options.WasmJavascriptEngine}\" does NOT exist.");
+ return false;
+ }
+
+ if (options.IlcPackages.IsNotNullButDoesNotExist())
+ {
+ logger.WriteLineError($"The provided {nameof(options.IlcPackages)} \"{options.IlcPackages}\" does NOT exist.");
+ return false;
+ }
+
+ if (options.HardwareCounters.Count() > 3)
+ {
+ logger.WriteLineError("You can't use more than 3 HardwareCounters at the same time.");
+ return false;
+ }
+
+ foreach (var counterName in options.HardwareCounters)
+ if (!Enum.TryParse(counterName, ignoreCase: true, out HardwareCounter _))
+ {
+ logger.WriteLineError($"The provided hardware counter \"{counterName}\" is invalid. Available options are: {string.Join("+", Enum.GetNames(typeof(HardwareCounter)))}.");
+ return false;
+ }
+
+ if (!string.IsNullOrEmpty(options.StatisticalTestThreshold) && !Threshold.TryParse(options.StatisticalTestThreshold, out _))
+ {
+ logger.WriteLineError("Invalid Threshold for Statistical Test. Use --help to see examples.");
+ return false;
+ }
+
+ if (options.EnvironmentVariables.Any(envVar => envVar.IndexOf(EnvVarKeyValueSeparator) <= 0))
+ {
+ logger.WriteLineError($"Environment variable value must be separated from the key using '{EnvVarKeyValueSeparator}'. Use --help to see examples.");
+ return false;
+ }
+
+ return true;
+ }
+
+ private static IConfig CreateConfig(CommandLineOptions options, IConfig globalConfig, string[] args)
+ {
+ var config = new ManualConfig();
+
+ var baseJob = GetBaseJob(options, globalConfig);
+ var expanded = Expand(baseJob.UnfreezeCopy(), options, args).ToArray(); // UnfreezeCopy ensures that each of the expanded jobs will have it's own ID
+ if (expanded.Length > 1)
+ expanded[0] = expanded[0].AsBaseline(); // if the user provides multiple jobs, then the first one should be a baseline
+ config.AddJob(expanded);
+ if (config.GetJobs().IsEmpty() && baseJob != Job.Default)
+ config.AddJob(baseJob);
+
+ config.AddExporter(options.Exporters.SelectMany(exporter => AvailableExporters[exporter]).ToArray());
+
+ config.AddHardwareCounters(options.HardwareCounters
+ .Select(counterName => (HardwareCounter)Enum.Parse(typeof(HardwareCounter), counterName, ignoreCase: true))
+ .ToArray());
+
+ if (options.UseMemoryDiagnoser)
+ config.AddDiagnoser(MemoryDiagnoser.Default);
+ if (options.UseThreadingDiagnoser)
+ config.AddDiagnoser(ThreadingDiagnoser.Default);
+ if (options.UseExceptionDiagnoser)
+ config.AddDiagnoser(ExceptionDiagnoser.Default);
+ if (options.UseDisassemblyDiagnoser)
+ config.AddDiagnoser(new DisassemblyDiagnoser(new DisassemblyDiagnoserConfig(
+ maxDepth: options.DisassemblerRecursiveDepth,
+ filters: options.DisassemblerFilters.ToArray(),
+ exportDiff: options.DisassemblerDiff)));
+ if (!string.IsNullOrEmpty(options.Profiler))
+ config.AddDiagnoser(DiagnosersLoader.GetImplementation(profiler => profiler.ShortName.EqualsWithIgnoreCase(options.Profiler)));
+
+ if (options.DisplayAllStatistics)
+ config.AddColumn(StatisticColumn.AllStatistics);
+ if (!string.IsNullOrEmpty(options.StatisticalTestThreshold) && Threshold.TryParse(options.StatisticalTestThreshold, out var threshold))
+ config.AddColumn(new StatisticalTestColumn(threshold));
+
+ if (options.ArtifactsDirectory != null)
+ config.ArtifactsPath = options.ArtifactsDirectory.FullName;
+
+ var filters = GetFilters(options).ToArray();
+ if (filters.Length > 1)
+ config.AddFilter(new UnionFilter(filters));
+ else
+ config.AddFilter(filters);
+
+ config.HideColumns(options.HiddenColumns.ToArray());
+
+ config.WithOption(ConfigOptions.JoinSummary, options.Join);
+ config.WithOption(ConfigOptions.KeepBenchmarkFiles, options.KeepBenchmarkFiles);
+ config.WithOption(ConfigOptions.DontOverwriteResults, options.DontOverwriteResults);
+ config.WithOption(ConfigOptions.StopOnFirstError, options.StopOnFirstError);
+ config.WithOption(ConfigOptions.DisableLogFile, options.DisableLogFile);
+ config.WithOption(ConfigOptions.LogBuildOutput, options.LogBuildOutput);
+ config.WithOption(ConfigOptions.GenerateMSBuildBinLog, options.GenerateMSBuildBinLog);
+ config.WithOption(ConfigOptions.ApplesToApples, options.ApplesToApples);
+ config.WithOption(ConfigOptions.Resume, options.Resume);
+
+ if (config.Options.IsSet(ConfigOptions.GenerateMSBuildBinLog))
+ config.Options |= ConfigOptions.KeepBenchmarkFiles;
+
+ if (options.MaxParameterColumnWidth.HasValue)
+ config.WithSummaryStyle(SummaryStyle.Default.WithMaxParameterColumnWidth(options.MaxParameterColumnWidth.Value));
+
+ if (options.TimeOutInSeconds.HasValue)
+ config.WithBuildTimeout(TimeSpan.FromSeconds(options.TimeOutInSeconds.Value));
+
+ if (options.WakeLock.HasValue)
+ config.WithWakeLock(options.WakeLock.Value);
+
+ return config;
+ }
+
+ private static Job GetBaseJob(CommandLineOptions options, IConfig globalConfig)
+ {
+ var baseJob =
+ globalConfig?.GetJobs().SingleOrDefault(job => job.Meta.IsDefault) // global config might define single custom Default job
+ ?? AvailableJobs[options.BaseJob.ToLowerInvariant()];
+
+ if (baseJob != Job.Dry && options.Outliers != OutlierMode.RemoveUpper)
+ baseJob = baseJob.WithOutlierMode(options.Outliers);
+
+ if (options.Affinity.HasValue)
+ baseJob = baseJob.WithAffinity((IntPtr)options.Affinity.Value);
+
+ if (options.LaunchCount.HasValue)
+ baseJob = baseJob.WithLaunchCount(options.LaunchCount.Value);
+ if (options.WarmupIterationCount.HasValue)
+ baseJob = baseJob.WithWarmupCount(options.WarmupIterationCount.Value);
+ if (options.MinWarmupIterationCount.HasValue)
+ baseJob = baseJob.WithMinWarmupCount(options.MinWarmupIterationCount.Value);
+ if (options.MaxWarmupIterationCount.HasValue)
+ baseJob = baseJob.WithMaxWarmupCount(options.MaxWarmupIterationCount.Value);
+ if (options.IterationTimeInMilliseconds.HasValue)
+ baseJob = baseJob.WithIterationTime(TimeInterval.FromMilliseconds(options.IterationTimeInMilliseconds.Value));
+ if (options.IterationCount.HasValue)
+ baseJob = baseJob.WithIterationCount(options.IterationCount.Value);
+ if (options.MinIterationCount.HasValue)
+ baseJob = baseJob.WithMinIterationCount(options.MinIterationCount.Value);
+ if (options.MaxIterationCount.HasValue)
+ baseJob = baseJob.WithMaxIterationCount(options.MaxIterationCount.Value);
+ if (options.InvocationCount.HasValue)
+ baseJob = baseJob.WithInvocationCount(options.InvocationCount.Value);
+ if (options.UnrollFactor.HasValue)
+ baseJob = baseJob.WithUnrollFactor(options.UnrollFactor.Value);
+ if (options.RunStrategy.HasValue)
+ baseJob = baseJob.WithStrategy(options.RunStrategy.Value);
+ if (options.Platform.HasValue)
+ baseJob = baseJob.WithPlatform(options.Platform.Value);
+ if (options.RunOncePerIteration)
+ baseJob = baseJob.RunOncePerIteration();
+ if (options.MemoryRandomization)
+ baseJob = baseJob.WithMemoryRandomization();
+ if (options.NoForcedGCs)
+ baseJob = baseJob.WithGcForce(false);
+ if (options.NoEvaluationOverhead)
+ baseJob = baseJob.WithEvaluateOverhead(false);
+
+ if (options.EnvironmentVariables.Any())
+ {
+ baseJob = baseJob.WithEnvironmentVariables(options.EnvironmentVariables.Select(text =>
+ {
+ var separated = text.Split(new[] { EnvVarKeyValueSeparator }, 2);
+ return new EnvironmentVariable(separated[0], separated[1]);
+ }).ToArray());
+ }
+
+ if (AvailableJobs.Values.Contains(baseJob)) // no custom settings
+ return baseJob;
+
+ return baseJob
+ .AsDefault(false) // after applying all settings from console args the base job is not default anymore
+ .AsMutator(); // we mark it as mutator so it will be applied to other jobs defined via attributes and merged later in GetRunnableJobs method
+ }
+
+ private static IEnumerable Expand(Job baseJob, CommandLineOptions options, string[] args)
+ {
+ if (options.RunInProcess)
+ {
+ yield return baseJob.WithToolchain(InProcessEmitToolchain.Instance);
+ }
+ else if (!string.IsNullOrEmpty(options.ClrVersion))
+ {
+ yield return baseJob.WithRuntime(ClrRuntime.CreateForLocalFullNetFrameworkBuild(options.ClrVersion)); // local builds of .NET Runtime
+ }
+ else if (options.CliPath != null && options.Runtimes.IsEmpty() && options.CoreRunPaths.IsEmpty())
+ {
+ yield return CreateCoreJobWithCli(baseJob, options);
+ }
+ else
+ {
+ // in case both --runtimes and --corerun are specified, the first one is returned first and becomes a baseline job
+ string first = args.FirstOrDefault(arg =>
+ arg.Equals("--runtimes", StringComparison.OrdinalIgnoreCase)
+ || arg.Equals("-r", StringComparison.OrdinalIgnoreCase)
+
+ || arg.Equals("--corerun", StringComparison.OrdinalIgnoreCase));
+
+ if (first is null || first.Equals("--corerun", StringComparison.OrdinalIgnoreCase))
+ {
+ foreach (var coreRunPath in options.CoreRunPaths)
+ yield return CreateCoreRunJob(baseJob, options, coreRunPath); // local dotnet/runtime builds
+
+ foreach (string runtime in options.Runtimes) // known runtimes
+ yield return CreateJobForGivenRuntime(baseJob, runtime, options);
+ }
+ else
+ {
+ foreach (string runtime in options.Runtimes) // known runtimes
+ yield return CreateJobForGivenRuntime(baseJob, runtime, options);
+
+ foreach (var coreRunPath in options.CoreRunPaths)
+ yield return CreateCoreRunJob(baseJob, options, coreRunPath); // local dotnet/runtime builds
+ }
+ }
+ }
+
+ private static Job CreateJobForGivenRuntime(Job baseJob, string runtimeId, CommandLineOptions options)
+ {
+ if (!TryParse(runtimeId, out RuntimeMoniker runtimeMoniker))
+ {
+ throw new InvalidOperationException("Impossible, already validated by the Validate method");
+ }
+
+ switch (runtimeMoniker)
+ {
+ case RuntimeMoniker.Net461:
+ case RuntimeMoniker.Net462:
+ case RuntimeMoniker.Net47:
+ case RuntimeMoniker.Net471:
+ case RuntimeMoniker.Net472:
+ case RuntimeMoniker.Net48:
+ case RuntimeMoniker.Net481:
+ return baseJob
+ .WithRuntime(runtimeMoniker.GetRuntime())
+ .WithToolchain(CsProjClassicNetToolchain.From(runtimeId, options.RestorePath?.FullName, options.CliPath?.FullName));
+
+ case RuntimeMoniker.NetCoreApp20:
+ case RuntimeMoniker.NetCoreApp21:
+ case RuntimeMoniker.NetCoreApp22:
+ case RuntimeMoniker.NetCoreApp30:
+ case RuntimeMoniker.NetCoreApp31:
+#pragma warning disable CS0618 // Type or member is obsolete
+ case RuntimeMoniker.NetCoreApp50:
+#pragma warning restore CS0618 // Type or member is obsolete
+ case RuntimeMoniker.Net50:
+ 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)));
+
+ case RuntimeMoniker.Mono:
+ return baseJob.WithRuntime(new MonoRuntime("Mono", options.MonoPath?.FullName));
+
+ case RuntimeMoniker.NativeAot60:
+ 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://api.nuget.org/v3/index.json");
+
+ case RuntimeMoniker.NativeAot80:
+ 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);
+
+ case RuntimeMoniker.WasmNet50:
+ return MakeWasmJob(baseJob, options, "net5.0", runtimeMoniker);
+
+ case RuntimeMoniker.WasmNet60:
+ return MakeWasmJob(baseJob, options, "net6.0", runtimeMoniker);
+
+ case RuntimeMoniker.WasmNet70:
+ return MakeWasmJob(baseJob, options, "net7.0", runtimeMoniker);
+
+ 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", runtimeMoniker);
+
+ case RuntimeMoniker.MonoAOTLLVMNet60:
+ return MakeMonoAOTLLVMJob(baseJob, options, "net6.0", runtimeMoniker);
+
+ case RuntimeMoniker.MonoAOTLLVMNet70:
+ return MakeMonoAOTLLVMJob(baseJob, options, "net7.0", runtimeMoniker);
+
+ case RuntimeMoniker.MonoAOTLLVMNet80:
+ 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);
+
+ case RuntimeMoniker.Mono70:
+ return MakeMonoJob(baseJob, options, MonoRuntime.Mono70);
+
+ 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");
+ }
+ }
+
+ private static Job CreateAotJob(Job baseJob, CommandLineOptions options, RuntimeMoniker runtimeMoniker, string ilCompilerVersion, string nuGetFeedUrl)
+ {
+ var builder = NativeAotToolchain.CreateBuilder();
+
+ if (options.CliPath != null)
+ builder.DotNetCli(options.CliPath.FullName);
+ if (options.RestorePath != null)
+ builder.PackagesRestorePath(options.RestorePath.FullName);
+
+ if (options.IlcPackages != null)
+ builder.UseLocalBuild(options.IlcPackages);
+ else if (!string.IsNullOrEmpty(options.ILCompilerVersion))
+ builder.UseNuGet(options.ILCompilerVersion, nuGetFeedUrl);
+ else
+ builder.UseNuGet(ilCompilerVersion, nuGetFeedUrl);
+
+ var runtime = runtimeMoniker.GetRuntime();
+ builder.TargetFrameworkMoniker(runtime.MsBuildMoniker);
+
+ return baseJob.WithRuntime(runtime).WithToolchain(builder.ToToolchain());
+ }
+
+ private static Job MakeMonoJob(Job baseJob, CommandLineOptions options, MonoRuntime runtime)
+ {
+ return baseJob
+ .WithRuntime(runtime)
+ .WithToolchain(MonoToolchain.From(
+ new NetCoreAppSettings(
+ targetFrameworkMoniker: runtime.MsBuildMoniker,
+ runtimeFrameworkVersion: null,
+ name: runtime.Name,
+ customDotNetCliPath: options.CliPath?.FullName,
+ packagesPath: options.RestorePath?.FullName)));
+ }
+
+ private static Job MakeMonoAOTLLVMJob(Job baseJob, CommandLineOptions options, string msBuildMoniker, RuntimeMoniker moniker)
+ {
+ var monoAotLLVMRuntime = new MonoAotLLVMRuntime(aotCompilerPath: options.AOTCompilerPath, aotCompilerMode: options.AOTCompilerMode, msBuildMoniker: msBuildMoniker, moniker: moniker);
+
+ var toolChain = MonoAotLLVMToolChain.From(
+ new NetCoreAppSettings(
+ targetFrameworkMoniker: monoAotLLVMRuntime.MsBuildMoniker,
+ runtimeFrameworkVersion: null,
+ name: monoAotLLVMRuntime.Name,
+ customDotNetCliPath: options.CliPath?.FullName,
+ packagesPath: options.RestorePath?.FullName,
+ customRuntimePack: options.CustomRuntimePack,
+ aotCompilerPath: options.AOTCompilerPath.ToString(),
+ aotCompilerMode: options.AOTCompilerMode));
+
+ return baseJob.WithRuntime(monoAotLLVMRuntime).WithToolchain(toolChain);
+ }
+
+ private static Job MakeWasmJob(Job baseJob, CommandLineOptions options, string msBuildMoniker, RuntimeMoniker moniker)
+ {
+ bool wasmAot = options.AOTCompilerMode == MonoAotCompilerMode.wasm;
+
+ var wasmRuntime = new WasmRuntime(
+ msBuildMoniker: msBuildMoniker,
+ javaScriptEngine: options.WasmJavascriptEngine?.FullName ?? "v8",
+ javaScriptEngineArguments: options.WasmJavaScriptEngineArguments,
+ aot: wasmAot,
+ wasmDataDir: options.WasmDataDirectory?.FullName,
+ moniker: moniker);
+
+ var toolChain = WasmToolchain.From(new NetCoreAppSettings(
+ targetFrameworkMoniker: wasmRuntime.MsBuildMoniker,
+ runtimeFrameworkVersion: null,
+ name: wasmRuntime.Name,
+ customDotNetCliPath: options.CliPath?.FullName,
+ packagesPath: options.RestorePath?.FullName,
+ customRuntimePack: options.CustomRuntimePack,
+ aotCompilerMode: options.AOTCompilerMode));
+
+ return baseJob.WithRuntime(wasmRuntime).WithToolchain(toolChain);
+ }
+
+ private static IEnumerable GetFilters(CommandLineOptions options)
+ {
+ if (options.Filters.Any())
+ yield return new GlobFilter(options.Filters.ToArray());
+ if (options.AllCategories.Any())
+ yield return new AllCategoriesFilter(options.AllCategories.ToArray());
+ if (options.AnyCategories.Any())
+ yield return new AnyCategoriesFilter(options.AnyCategories.ToArray());
+ if (options.AttributeNames.Any())
+ yield return new AttributesFilter(options.AttributeNames.ToArray());
+ }
+
+ private static int GetMaximumDisplayWidth()
+ {
+ try
+ {
+ return Console.WindowWidth;
+ }
+ catch (IOException)
+ {
+ return MinimumDisplayWidth;
+ }
+ }
+
+ private static Job CreateCoreRunJob(Job baseJob, CommandLineOptions options, FileInfo coreRunPath)
+ => baseJob
+ .WithToolchain(new CoreRunToolchain(
+ coreRunPath,
+ createCopy: true,
+ targetFrameworkMoniker:
+ RuntimeInformation.IsNetCore
+ ? RuntimeInformation.GetCurrentRuntime().MsBuildMoniker
+ : CoreRuntime.Latest.MsBuildMoniker, // use most recent tfm, as the toolchain is being used only by dotnet/runtime contributors
+ customDotNetCliPath: options.CliPath,
+ restorePath: options.RestorePath,
+ displayName: GetCoreRunToolchainDisplayName(options.CoreRunPaths, coreRunPath)));
+
+ private static Job CreateCoreJobWithCli(Job baseJob, CommandLineOptions options)
+ => baseJob
+ .WithToolchain(CsProjCoreToolchain.From(
+ new NetCoreAppSettings(
+ targetFrameworkMoniker: RuntimeInformation.GetCurrentRuntime().MsBuildMoniker,
+ customDotNetCliPath: options.CliPath?.FullName,
+ runtimeFrameworkVersion: null,
+ name: RuntimeInformation.GetCurrentRuntime().Name,
+ packagesPath: options.RestorePath?.FullName)));
+
+ ///
+ /// we have a limited amount of space when printing the output to the console, so we try to keep things small and simple
+ ///
+ /// for following paths:
+ /// C:\Projects\coreclr_upstream\bin\tests\Windows_NT.x64.Release\Tests\Core_Root\CoreRun.exe
+ /// C:\Projects\coreclr_upstream\bin\tests\Windows_NT.x64.Release\Tests\Core_Root_beforeMyChanges\CoreRun.exe
+ ///
+ /// we get:
+ ///
+ /// \Core_Root\CoreRun.exe
+ /// \Core_Root_beforeMyChanges\CoreRun.exe
+ ///
+ private static string GetCoreRunToolchainDisplayName(IReadOnlyList paths, FileInfo coreRunPath)
+ {
+ if (paths.Count <= 1)
+ return "CoreRun";
+
+ int commonLongestPrefixIndex = paths[0].FullName.Length;
+ for (int i = 1; i < paths.Count; i++)
+ {
+ commonLongestPrefixIndex = Math.Min(commonLongestPrefixIndex, paths[i].FullName.Length);
+ for (int j = 0; j < commonLongestPrefixIndex; j++)
+ if (paths[i].FullName[j] != paths[0].FullName[j])
+ {
+ commonLongestPrefixIndex = j;
+ break;
+ }
+ }
+
+ if (commonLongestPrefixIndex <= 1)
+ return coreRunPath.FullName;
+
+ var lastCommonDirectorySeparatorIndex = coreRunPath.FullName.LastIndexOf(Path.DirectorySeparatorChar, commonLongestPrefixIndex - 1);
+
+ return coreRunPath.FullName.Substring(lastCommonDirectorySeparatorIndex);
+ }
+
+ internal static bool TryParse(string runtime, out RuntimeMoniker runtimeMoniker)
+ {
+ int index = runtime.IndexOf('-');
+ if (index >= 0)
+ {
+ runtime = runtime.Substring(0, index);
+ }
+
+ // 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 52%
rename from src/BenchmarkDotNet/Portability/Cpu/HardwareIntrinsics.cs
rename to src/BenchmarkDotNet/Detectors/Cpu/HardwareIntrinsics.cs
index 68c48809b7..5b2815cc43 100644
--- a/src/BenchmarkDotNet/Portability/Cpu/HardwareIntrinsics.cs
+++ b/src/BenchmarkDotNet/Detectors/Cpu/HardwareIntrinsics.cs
@@ -1,45 +1,50 @@
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
{
+ // based on https://github.com/dotnet/runtime/tree/v10.0.0-rc.1.25451.107/src/coreclr/tools/Common/JitInterface/ThunkGenerator/InstructionSetDesc.txt
internal static class HardwareIntrinsics
{
internal static string GetVectorSize() => Vector.IsHardwareAccelerated ? $"VectorSize={Vector.Count * 8}" : string.Empty;
internal static string GetShortInfo()
{
- if (IsX86Avx2Supported)
- return "AVX2";
- else if (IsX86AvxSupported)
- return "AVX";
- else if (IsX86Sse42Supported)
- return "SSE4.2";
- else if (IsX86Sse41Supported)
- return "SSE4.1";
- else if (IsX86Ssse3Supported)
- return "SSSE3";
- else if (IsX86Sse3Supported)
- return "SSE3";
- else if (IsX86Sse2Supported)
- return "SSE2";
- else if (IsX86SseSupported)
- return "SSE";
- else if (IsX86BaseSupported)
- return "X86Base";
- else if (IsArmAdvSimdSupported)
- return "AdvSIMD";
+ if (IsX86BaseSupported)
+ {
+ if (IsX86Avx512Supported)
+ {
+ return "x86-64-v4";
+ }
+ else if (IsX86Avx2Supported)
+ {
+ return "x86-64-v3";
+ }
+ else if (IsX86Sse42Supported)
+ {
+ return "x86-64-v2";
+ }
+ else
+ {
+ return "x86-64-v1";
+ }
+ }
else if (IsArmBaseSupported)
- return "ArmBase";
+ {
+ return "armv8.0-a";
+ }
else
+ {
return GetVectorSize(); // Runtimes prior to .NET Core 3.0 (APIs did not exist so we print non-exact Vector info)
+ }
}
internal static string GetFullInfo(Platform platform)
@@ -52,30 +57,31 @@ static IEnumerable GetCurrentProcessInstructionSets(Platform platform)
{
case Platform.X86:
case Platform.X64:
- 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";
- else if (IsX86Ssse3Supported) yield return "SSSE3";
- else if (IsX86Sse3Supported) yield return "SSE3";
- else if (IsX86Sse2Supported) yield return "SSE2";
- else if (IsX86SseSupported) yield return "SSE";
- else if (IsX86BaseSupported) yield return "X86Base";
-
- if (IsX86AesSupported) yield return "AES";
- if (IsX86Bmi1Supported) yield return "BMI1";
- if (IsX86Bmi2Supported) yield return "BMI2";
- if (IsX86FmaSupported) yield return "FMA";
- if (IsX86LzcntSupported) yield return "LZCNT";
- if (IsX86PclmulqdqSupported) yield return "PCLMUL";
- if (IsX86PopcntSupported) yield return "POPCNT";
+ {
+ if (IsX86Avx10v2Supported) yield return "AVX10v2";
+ if (IsX86Avx10v1Supported)
+ {
+ yield return "AVX10v1";
+ yield return "AVX512 BF16+FP16";
+ }
+ if (IsX86Avx512v3Supported) yield return "AVX512 BITALG+VBMI2+VNNI+VPOPCNTDQ";
+ if (IsX86Avx512v2Supported) yield return "AVX512 IFMA+VBMI";
+ if (IsX86Avx512Supported) yield return "AVX512 F+BW+CD+DQ+VL";
+ if (IsX86Avx2Supported) yield return "AVX2+BMI1+BMI2+F16C+FMA+LZCNT+MOVBE";
+ if (IsX86AvxSupported) yield return "AVX";
+ if (IsX86Sse42Supported) yield return "SSE3+SSSE3+SSE4.1+SSE4.2+POPCNT";
+ if (IsX86BaseSupported) yield return "X86Base+SSE+SSE2";
+ if (IsX86AesSupported) yield return "AES+PCLMUL";
if (IsX86AvxVnniSupported) yield return "AvxVnni";
if (IsX86SerializeSupported) yield return "SERIALIZE";
- // TODO: Add MOVBE when API is added.
break;
+ }
case Platform.Arm64:
- if (IsArmAdvSimdSupported) yield return "AdvSIMD";
- else if (IsArmBaseSupported) yield return "ArmBase";
+ {
+ if (IsArmBaseSupported)
+ {
+ yield return "ArmBase+AdvSimd";
+ }
if (IsArmAesSupported) yield return "AES";
if (IsArmCrc32Supported) yield return "CRC32";
@@ -84,59 +90,39 @@ static IEnumerable GetCurrentProcessInstructionSets(Platform platform)
if (IsArmSha1Supported) yield return "SHA1";
if (IsArmSha256Supported) yield return "SHA256";
break;
+ }
+
default:
yield break;
}
}
}
+#pragma warning disable CA2252 // Some APIs require opting into preview features
internal static bool IsX86BaseSupported =>
#if NET6_0_OR_GREATER
- X86Base.IsSupported;
-#elif NETSTANDARD
- GetIsSupported("System.Runtime.Intrinsics.X86.X86Base");
-#endif
-
- internal static bool IsX86SseSupported =>
-#if NET6_0_OR_GREATER
- Sse.IsSupported;
-#elif NETSTANDARD
- GetIsSupported("System.Runtime.Intrinsics.X86.Sse");
-#endif
-
- internal static bool IsX86Sse2Supported =>
-#if NET6_0_OR_GREATER
+ X86Base.IsSupported &&
+ Sse.IsSupported &&
Sse2.IsSupported;
#elif NETSTANDARD
+ GetIsSupported("System.Runtime.Intrinsics.X86.X86Base") &&
+ GetIsSupported("System.Runtime.Intrinsics.X86.Sse") &&
GetIsSupported("System.Runtime.Intrinsics.X86.Sse2");
#endif
- internal static bool IsX86Sse3Supported =>
-#if NET6_0_OR_GREATER
- Sse3.IsSupported;
-#elif NETSTANDARD
- GetIsSupported("System.Runtime.Intrinsics.X86.Sse3");
-#endif
-
- internal static bool IsX86Ssse3Supported =>
-#if NET6_0_OR_GREATER
- Ssse3.IsSupported;
-#elif NETSTANDARD
- GetIsSupported("System.Runtime.Intrinsics.X86.Ssse3");
-#endif
-
- internal static bool IsX86Sse41Supported =>
-#if NET6_0_OR_GREATER
- Sse41.IsSupported;
-#elif NETSTANDARD
- GetIsSupported("System.Runtime.Intrinsics.X86.Sse41");
-#endif
-
internal static bool IsX86Sse42Supported =>
#if NET6_0_OR_GREATER
- Sse42.IsSupported;
+ Sse3.IsSupported &&
+ Ssse3.IsSupported &&
+ Sse41.IsSupported &&
+ Sse42.IsSupported &&
+ Popcnt.IsSupported;
#elif NETSTANDARD
- GetIsSupported("System.Runtime.Intrinsics.X86.Sse42");
+ GetIsSupported("System.Runtime.Intrinsics.X86.Sse3") &&
+ GetIsSupported("System.Runtime.Intrinsics.X86.Ssse3") &&
+ GetIsSupported("System.Runtime.Intrinsics.X86.Sse41") &&
+ GetIsSupported("System.Runtime.Intrinsics.X86.Sse42") &&
+ GetIsSupported("System.Runtime.Intrinsics.X86.Popcnt");
#endif
internal static bool IsX86AvxSupported =>
@@ -148,83 +134,105 @@ static IEnumerable GetCurrentProcessInstructionSets(Platform platform)
internal static bool IsX86Avx2Supported =>
#if NET6_0_OR_GREATER
- Avx2.IsSupported;
+ Avx2.IsSupported &&
+ Bmi1.IsSupported &&
+ Bmi2.IsSupported &&
+ Fma.IsSupported &&
+ Lzcnt.IsSupported;
#elif NETSTANDARD
- GetIsSupported("System.Runtime.Intrinsics.X86.Avx2");
+ GetIsSupported("System.Runtime.Intrinsics.X86.Avx2") &&
+ GetIsSupported("System.Runtime.Intrinsics.X86.Bmi1") &&
+ GetIsSupported("System.Runtime.Intrinsics.X86.Bmi2") &&
+ GetIsSupported("System.Runtime.Intrinsics.X86.Fma") &&
+ GetIsSupported("System.Runtime.Intrinsics.X86.Lzcnt");
#endif
- internal static bool IsX86AesSupported =>
-#if NET6_0_OR_GREATER
- System.Runtime.Intrinsics.X86.Aes.IsSupported;
-#elif NETSTANDARD
- GetIsSupported("System.Runtime.Intrinsics.X86.Aes");
+ internal static bool IsX86Avx512Supported =>
+#if NET8_0_OR_GREATER
+ Avx512F.IsSupported &&
+ Avx512F.VL.IsSupported &&
+ Avx512BW.IsSupported &&
+ Avx512BW.VL.IsSupported &&
+ Avx512CD.IsSupported &&
+ Avx512CD.VL.IsSupported &&
+ Avx512DQ.IsSupported &&
+ Avx512DQ.VL.IsSupported;
+#else
+ GetIsSupported("System.Runtime.Intrinsics.X86.Avx512F") &&
+ GetIsSupported("System.Runtime.Intrinsics.X86.Avx512F+VL") &&
+ GetIsSupported("System.Runtime.Intrinsics.X86.Avx512BW") &&
+ GetIsSupported("System.Runtime.Intrinsics.X86.Avx512BW+VL") &&
+ GetIsSupported("System.Runtime.Intrinsics.X86.Avx512CD") &&
+ GetIsSupported("System.Runtime.Intrinsics.X86.Avx512CD+VL") &&
+ GetIsSupported("System.Runtime.Intrinsics.X86.Avx512DQ") &&
+ GetIsSupported("System.Runtime.Intrinsics.X86.Avx512DQ+VL");
#endif
- internal static bool IsX86Bmi1Supported =>
-#if NET6_0_OR_GREATER
- Bmi1.IsSupported;
-#elif NETSTANDARD
- GetIsSupported("System.Runtime.Intrinsics.X86.Bmi1");
+ internal static bool IsX86Avx512v2Supported =>
+#if NET8_0_OR_GREATER
+ Avx512Vbmi.IsSupported &&
+ Avx512Vbmi.VL.IsSupported;
+#else
+ GetIsSupported("System.Runtime.Intrinsics.X86.Avx512Vbmi") &&
+ GetIsSupported("System.Runtime.Intrinsics.X86.Avx512Vbmi+VL");
#endif
- internal static bool IsX86Bmi2Supported =>
-#if NET6_0_OR_GREATER
- Bmi2.IsSupported;
-#elif NETSTANDARD
- GetIsSupported("System.Runtime.Intrinsics.X86.Bmi2");
+ internal static bool IsX86Avx512v3Supported =>
+#if NET10_0_OR_GREATER
+ Avx512Vbmi2.IsSupported &&
+ Avx512Vbmi2.VL.IsSupported;
+#else
+ GetIsSupported("System.Runtime.Intrinsics.X86.Avx512Vbmi2") &&
+ GetIsSupported("System.Runtime.Intrinsics.X86.Avx512Vbmi2+VL");
#endif
- internal static bool IsX86FmaSupported =>
-#if NET6_0_OR_GREATER
- Fma.IsSupported;
-#elif NETSTANDARD
- GetIsSupported("System.Runtime.Intrinsics.X86.Fma");
+ internal static bool IsX86Avx10v1Supported =>
+#if NET9_0_OR_GREATER
+ Avx10v1.IsSupported &&
+ Avx10v1.V512.IsSupported;
+#else
+ GetIsSupported("System.Runtime.Intrinsics.X86.Avx10v1") &&
+ GetIsSupported("System.Runtime.Intrinsics.X86.Avx10v1+V512");
#endif
- internal static bool IsX86LzcntSupported =>
-#if NET6_0_OR_GREATER
- Lzcnt.IsSupported;
-#elif NETSTANDARD
- GetIsSupported("System.Runtime.Intrinsics.X86.Lzcnt");
+ internal static bool IsX86Avx10v2Supported =>
+#if NET10_0_OR_GREATER
+ Avx10v2.IsSupported &&
+ Avx10v2.V512.IsSupported;
+#else
+ GetIsSupported("System.Runtime.Intrinsics.X86.Avx10v2") &&
+ GetIsSupported("System.Runtime.Intrinsics.X86.Avx10v2+V512");
#endif
- internal static bool IsX86PclmulqdqSupported =>
+ internal static bool IsX86AesSupported =>
#if NET6_0_OR_GREATER
+ System.Runtime.Intrinsics.X86.Aes.IsSupported &&
Pclmulqdq.IsSupported;
#elif NETSTANDARD
+ GetIsSupported("System.Runtime.Intrinsics.X86.Aes") &&
GetIsSupported("System.Runtime.Intrinsics.X86.Pclmulqdq");
#endif
- internal static bool IsX86PopcntSupported =>
-#if NET6_0_OR_GREATER
- Popcnt.IsSupported;
-#elif NETSTANDARD
- GetIsSupported("System.Runtime.Intrinsics.X86.Popcnt");
-#endif
-
internal static bool IsX86AvxVnniSupported =>
#if NET6_0_OR_GREATER
-#pragma warning disable CA2252 // This API requires opting into preview features
AvxVnni.IsSupported;
-#pragma warning restore CA2252 // This API requires opting into preview features
#elif NETSTANDARD
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 IsArmBaseSupported =>
-#if NET6_0_OR_GREATER
- ArmBase.IsSupported;
-#elif NETSTANDARD
- GetIsSupported("System.Runtime.Intrinsics.Arm.ArmBase");
+ internal static bool IsX86SerializeSupported =>
+#if NET7_0_OR_GREATER
+ X86Serialize.IsSupported;
+#else
+ GetIsSupported("System.Runtime.Intrinsics.X86.X86Serialize");
#endif
- internal static bool IsArmAdvSimdSupported =>
+ internal static bool IsArmBaseSupported =>
#if NET6_0_OR_GREATER
+ ArmBase.IsSupported &&
AdvSimd.IsSupported;
#elif NETSTANDARD
+ GetIsSupported("System.Runtime.Intrinsics.Arm.ArmBase") &&
GetIsSupported("System.Runtime.Intrinsics.Arm.AdvSimd");
#endif
@@ -269,6 +277,7 @@ static IEnumerable GetCurrentProcessInstructionSets(Platform platform)
#elif NETSTANDARD
GetIsSupported("System.Runtime.Intrinsics.Arm.Sha256");
#endif
+#pragma warning restore CA2252 // Some APIs require opting into preview features
private static bool GetIsSupported([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] string typeName)
{
diff --git a/src/BenchmarkDotNet/Detectors/Cpu/ICpuDetector.cs b/src/BenchmarkDotNet/Detectors/Cpu/ICpuDetector.cs
new file mode 100644
index 0000000000..72355b60e5
--- /dev/null
+++ b/src/BenchmarkDotNet/Detectors/Cpu/ICpuDetector.cs
@@ -0,0 +1,12 @@
+using Perfolizer.Models;
+
+namespace BenchmarkDotNet.Detectors.Cpu;
+
+///
+/// Loads the for the current hardware
+///
+public interface ICpuDetector
+{
+ bool IsApplicable();
+ CpuInfo? 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..44973b27e9
--- /dev/null
+++ b/src/BenchmarkDotNet/Detectors/Cpu/Linux/LinuxCpuDetector.cs
@@ -0,0 +1,35 @@
+using System.Collections.Generic;
+using BenchmarkDotNet.Helpers;
+using Perfolizer.Models;
+
+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 CpuInfo? 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.Empty;
+ string? lscpu = ProcessHelper.RunAndReadOutput("/bin/bash", "-c \"lscpu\"", environmentVariables: languageInvariantEnvironment) ?? string.Empty;
+
+ if (cpuInfo == string.Empty && lscpu == string.Empty)
+ return null;
+
+ 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..7d79c3262f
--- /dev/null
+++ b/src/BenchmarkDotNet/Detectors/Cpu/Linux/LinuxCpuInfoParser.cs
@@ -0,0 +1,160 @@
+using System;
+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.Models;
+
+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";
+ internal const string NominalFrequencyBackup = "nominal freq";
+ internal const string NominalFrequency = "cpu MHz";
+ }
+
+ 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 CpuInfo Parse(string cpuInfo, string lscpu)
+ {
+ var processorModelNames = new HashSet();
+ var processorsToPhysicalCoreCount = new Dictionary();
+ int logicalCoreCount = 0;
+ double maxFrequency = 0.0;
+ double nominalFrequency = 0.0;
+
+ 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.Replace(',', '.'), out Frequency maxCpuFreq)
+ && maxCpuFreq > 0)
+ {
+ maxFrequency = Math.Max(maxFrequency, maxCpuFreq.ToMHz());
+ }
+
+ bool nominalFrequencyHasValue = logicalCore.TryGetValue(ProcCpu.NominalFrequency, out string nominalFreqValue);
+ bool nominalFrequencyBackupHasValue = logicalCore.TryGetValue(ProcCpu.NominalFrequencyBackup, out string nominalFreqBackupValue);
+
+ double nominalCpuFreq = 0.0;
+ double nominalCpuBackupFreq = 0.0;
+
+ if (nominalFrequencyHasValue &&
+ double.TryParse(nominalFreqValue, out nominalCpuFreq)
+ && nominalCpuFreq > 0)
+ {
+ nominalCpuFreq = nominalFrequency == 0 ? nominalCpuFreq : Math.Min(nominalFrequency, nominalCpuFreq);
+ }
+ if (nominalFrequencyBackupHasValue &&
+ double.TryParse(nominalFreqBackupValue, out nominalCpuBackupFreq)
+ && nominalCpuBackupFreq > 0)
+ {
+ nominalCpuBackupFreq = nominalFrequency == 0 ? nominalCpuBackupFreq : Math.Min(nominalFrequency, nominalCpuBackupFreq);
+ }
+
+ if (nominalFrequencyHasValue && nominalFrequencyBackupHasValue)
+ {
+ nominalFrequency = Math.Min(nominalCpuFreq, nominalCpuBackupFreq);
+ }
+ else
+ {
+ nominalFrequency = nominalCpuFreq == 0.0 ? nominalCpuBackupFreq : nominalCpuFreq;
+ }
+ }
+
+ int? coresPerSocket = null;
+ if (string.IsNullOrEmpty(lscpu) == false)
+ {
+ 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 Frequency maxFrequencyParsed)) // Example: `CPU max MHz: 3200,0000`
+ maxFrequency = Math.Max(maxFrequency, maxFrequencyParsed.ToMHz());
+
+ if (name.EqualsWithIgnoreCase(Lscpu.ModelName))
+ processorModelNames.Add(value);
+
+ if (name.EqualsWithIgnoreCase(Lscpu.CoresPerSocket) &&
+ int.TryParse(value, out int coreCount))
+ coresPerSocket = coreCount;
+ }
+ }
+
+ 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;
+
+ Frequency? maxFrequencyActual = maxFrequency > 0 && physicalProcessorCount > 0
+ ? Frequency.FromMHz(maxFrequency) : null;
+
+ Frequency? nominalFrequencyActual = nominalFrequency > 0 && physicalProcessorCount > 0
+ ? Frequency.FromMHz(nominalFrequency) : null;
+
+ if (nominalFrequencyActual is null)
+ {
+ bool nominalFrequencyInBrandString = processorModelNames.Any(x => ParseFrequencyFromBrandString(x) is not null);
+
+ if (nominalFrequencyInBrandString)
+ nominalFrequencyActual = processorModelNames.Select(x => ParseFrequencyFromBrandString(x))
+ .First(x => x is not null);
+ }
+
+ return new CpuInfo
+ {
+ ProcessorName = processorName,
+ PhysicalProcessorCount = physicalProcessorCount,
+ PhysicalCoreCount = physicalCoreCount,
+ LogicalCoreCount = logicalCoreCount > 0 ? logicalCoreCount : null,
+ NominalFrequencyHz = nominalFrequencyActual?.Hertz.RoundToLong(),
+ MaxFrequencyHz = maxFrequencyActual?.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 Frequency 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..edc48ba25d
--- /dev/null
+++ b/src/BenchmarkDotNet/Detectors/Cpu/Windows/MosCpuDetector.cs
@@ -0,0 +1,75 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Management;
+using BenchmarkDotNet.Extensions;
+using BenchmarkDotNet.Portability;
+using Perfolizer.Horology;
+using Perfolizer.Models;
+
+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 CpuInfo? Detect()
+ {
+ if (!IsApplicable()) return null;
+
+ var processorModelNames = new HashSet();
+ int physicalCoreCount = 0;
+ int logicalCoreCount = 0;
+ int processorsCount = 0;
+ double maxFrequency = 0;
+ double nominalFrequency = 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];
+ double tempMaxFrequency = (uint)moProcessor[WmicCpuInfoKeyNames.MaxClockSpeed];
+
+ if (tempMaxFrequency > 0)
+ {
+ nominalFrequency = nominalFrequency == 0 ? tempMaxFrequency : Math.Min(nominalFrequency, tempMaxFrequency);
+ }
+ maxFrequency = Math.Max(maxFrequency, tempMaxFrequency);
+ }
+ }
+ }
+
+ string processorName = processorModelNames.Count > 0 ? string.Join(", ", processorModelNames) : null;
+ Frequency? maxFrequencyActual = maxFrequency > 0 && processorsCount > 0
+ ? Frequency.FromMHz(maxFrequency)
+ : null;
+ Frequency? nominalFrequencyActual = nominalFrequency > 0 && processorsCount > 0
+ ? Frequency.FromMHz(nominalFrequency)
+ : null;
+
+ return new CpuInfo
+ {
+ ProcessorName = processorName,
+ PhysicalProcessorCount = processorsCount > 0 ? processorsCount : null,
+ PhysicalCoreCount = physicalCoreCount > 0 ? physicalCoreCount : null,
+ LogicalCoreCount = logicalCoreCount > 0 ? logicalCoreCount : null,
+ NominalFrequencyHz = nominalFrequencyActual?.Hertz.RoundToLong(),
+ MaxFrequencyHz = maxFrequencyActual?.Hertz.RoundToLong()
+ };
+ }
+}
\ No newline at end of file
diff --git a/src/BenchmarkDotNet/Detectors/Cpu/Windows/PowershellWmiCpuDetector.cs b/src/BenchmarkDotNet/Detectors/Cpu/Windows/PowershellWmiCpuDetector.cs
new file mode 100644
index 0000000000..7f61c1b8fc
--- /dev/null
+++ b/src/BenchmarkDotNet/Detectors/Cpu/Windows/PowershellWmiCpuDetector.cs
@@ -0,0 +1,44 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Runtime.Versioning;
+using System.Text.RegularExpressions;
+using BenchmarkDotNet.Helpers;
+using Perfolizer.Models;
+
+namespace BenchmarkDotNet.Detectors.Cpu.Windows;
+
+///
+/// CPU information from output of the `wmic cpu get Name, NumberOfCores, NumberOfLogicalProcessors /Format:List` command.
+/// Windows only.
+///
+internal class PowershellWmiCpuDetector : ICpuDetector
+{
+ private readonly string windowsPowershellPath =
+ $"{Environment.SystemDirectory}{Path.DirectorySeparatorChar}WindowsPowerShell{Path.DirectorySeparatorChar}" +
+ $"v1.0{Path.DirectorySeparatorChar}powershell.exe";
+
+ public bool IsApplicable() => OsDetector.IsWindows();
+
+ #if NET6_0_OR_GREATER
+ [SupportedOSPlatform("windows")]
+ #endif
+ public CpuInfo? Detect()
+ {
+ if (!IsApplicable()) return null;
+
+ const string argList = $"{WmicCpuInfoKeyNames.Name}, " +
+ $"{WmicCpuInfoKeyNames.NumberOfCores}, " +
+ $"{WmicCpuInfoKeyNames.NumberOfLogicalProcessors}, " +
+ $"{WmicCpuInfoKeyNames.MaxClockSpeed}";
+
+ string output = ProcessHelper.RunAndReadOutput(PowerShellLocator.LocateOnWindows() ?? "PowerShell",
+ "Get-CimInstance Win32_Processor -Property " + argList);
+
+ if (string.IsNullOrEmpty(output))
+ return null;
+
+ return PowershellWmiCpuInfoParser.Parse(output);
+ }
+}
\ No newline at end of file
diff --git a/src/BenchmarkDotNet/Detectors/Cpu/Windows/PowershellWmiCpuInfoParser.cs b/src/BenchmarkDotNet/Detectors/Cpu/Windows/PowershellWmiCpuInfoParser.cs
new file mode 100644
index 0000000000..15ad4d6c77
--- /dev/null
+++ b/src/BenchmarkDotNet/Detectors/Cpu/Windows/PowershellWmiCpuInfoParser.cs
@@ -0,0 +1,67 @@
+using System;
+using System.Collections.Generic;
+using BenchmarkDotNet.Extensions;
+using BenchmarkDotNet.Helpers;
+using Perfolizer.Horology;
+using Perfolizer.Models;
+
+namespace BenchmarkDotNet.Detectors.Cpu.Windows;
+
+internal static class PowershellWmiCpuInfoParser
+{
+ internal static CpuInfo Parse(string powershellWmiOutput)
+ {
+ HashSet processorModelNames = new HashSet();
+
+ int physicalCoreCount = 0;
+ int logicalCoreCount = 0;
+ int processorCount = 0;
+ double maxFrequency = 0.0;
+ double nominalFrequency = 0.0;
+
+ List> processors = SectionsHelper.ParseSectionsForPowershellWmi(powershellWmiOutput, ':');
+ foreach (Dictionary 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);
+ processorCount++;
+ }
+
+ if (processor.TryGetValue(WmicCpuInfoKeyNames.MaxClockSpeed, out string frequencyValue)
+ && double.TryParse(frequencyValue, out double frequency)
+ && frequency > 0)
+ {
+ nominalFrequency = nominalFrequency == 0 ? frequency : Math.Min(nominalFrequency, frequency);
+ maxFrequency = Math.Max(maxFrequency, frequency);
+ }
+ }
+
+ string? processorName = processorModelNames.Count > 0 ? string.Join(", ", processorModelNames) : null;
+ Frequency? maxFrequencyActual = maxFrequency > 0 && processorCount > 0
+ ? Frequency.FromMHz(maxFrequency) : null;
+
+ Frequency? nominalFrequencyActual = nominalFrequency > 0 && processorCount > 0 ?
+ Frequency.FromMHz(nominalFrequency) : null;
+
+ return new CpuInfo
+ {
+ ProcessorName = processorName,
+ PhysicalProcessorCount = processorCount > 0 ? processorCount : null,
+ PhysicalCoreCount = physicalCoreCount > 0 ? physicalCoreCount : null,
+ LogicalCoreCount = logicalCoreCount > 0 ? logicalCoreCount : null,
+ NominalFrequencyHz = nominalFrequencyActual?.Hertz.RoundToLong(),
+ MaxFrequencyHz = maxFrequencyActual?.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..1de238b93f
--- /dev/null
+++ b/src/BenchmarkDotNet/Detectors/Cpu/Windows/WindowsCpuDetector.cs
@@ -0,0 +1,4 @@
+namespace BenchmarkDotNet.Detectors.Cpu.Windows;
+
+internal class WindowsCpuDetector() : CpuDetector(new MosCpuDetector(), new PowershellWmiCpuDetector(),
+ 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..fe3434ae7f
--- /dev/null
+++ b/src/BenchmarkDotNet/Detectors/Cpu/Windows/WmicCpuDetector.cs
@@ -0,0 +1,36 @@
+using System.IO;
+using BenchmarkDotNet.Helpers;
+using BenchmarkDotNet.Portability;
+using Perfolizer.Models;
+
+namespace BenchmarkDotNet.Detectors.Cpu.Windows;
+
+///
+/// CPU information from output of the `wmic cpu get Name, NumberOfCores, NumberOfLogicalProcessors /Format:List` command.
+/// Windows only.
+///
+/// WMIC is deprecated by Microsoft starting with Windows 10 21H1 (including Windows Server), and it is not known whether it still ships with Windows by default.
+/// WMIC may be removed in a future version of Windows. See
+internal class WmicCpuDetector : ICpuDetector
+{
+ private const string DefaultWmicPath = @"C:\Windows\System32\wbem\WMIC.exe";
+
+ public bool IsApplicable() => OsDetector.IsWindows();
+
+ public CpuInfo? 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");
+
+ if (string.IsNullOrEmpty(wmicOutput))
+ return null;
+
+ 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..295d76262e
--- /dev/null
+++ b/src/BenchmarkDotNet/Detectors/Cpu/Windows/WmicCpuInfoParser.cs
@@ -0,0 +1,70 @@
+using System;
+using System.Collections.Generic;
+using BenchmarkDotNet.Extensions;
+using BenchmarkDotNet.Helpers;
+using Perfolizer.Horology;
+using Perfolizer.Models;
+
+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 CpuInfo Parse(string wmicOutput)
+ {
+ HashSet processorModelNames = new HashSet();
+ int physicalCoreCount = 0;
+ int logicalCoreCount = 0;
+ int processorsCount = 0;
+ double maxFrequency = 0.0;
+ double nominalFrequency = 0.0;
+
+ List> 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)
+ && double.TryParse(frequencyValue, out double frequency)
+ && frequency > 0)
+ {
+ nominalFrequency = nominalFrequency == 0 ? frequency : Math.Min(nominalFrequency, frequency);
+ maxFrequency = Math.Max(maxFrequency, frequency);
+ }
+ }
+
+ string? processorName = processorModelNames.Count > 0 ? string.Join(", ", processorModelNames) : null;
+ Frequency? maxFrequencyActual = maxFrequency > 0 && processorsCount > 0
+ ? Frequency.FromMHz(maxFrequency)
+ : null;
+
+ Frequency? nominalFrequencyActual = nominalFrequency > 0 && processorsCount > 0 ? Frequency.FromMHz(nominalFrequency) : null;
+
+ return new CpuInfo
+ {
+ ProcessorName = processorName,
+ PhysicalProcessorCount = processorsCount > 0 ? processorsCount : null,
+ PhysicalCoreCount = physicalCoreCount > 0 ? physicalCoreCount : null,
+ LogicalCoreCount = logicalCoreCount > 0 ? logicalCoreCount : null,
+ NominalFrequencyHz = nominalFrequencyActual?.Hertz.RoundToLong(),
+ MaxFrequencyHz = maxFrequencyActual?.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..2f540e82d3
--- /dev/null
+++ b/src/BenchmarkDotNet/Detectors/Cpu/macOS/MacOsCpuDetector.cs
@@ -0,0 +1,25 @@
+using BenchmarkDotNet.Helpers;
+using Perfolizer.Models;
+
+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 CpuInfo? Detect()
+ {
+ if (!IsApplicable()) return null;
+
+ string? sysctlOutput = ProcessHelper.RunAndReadOutput("sysctl", "-a");
+
+ if (sysctlOutput is null)
+ return null;
+
+ 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..5cd4d11c8a
--- /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.Models;
+
+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 CpuInfo 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 CpuInfo
+ {
+ 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..0cb583a95a
--- /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.Models;
+
+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 CpuInfo? Cpu => LazyCpu.Value;
+
+ public bool IsApplicable() => detectors.Any(loader => loader.IsApplicable());
+
+ public CpuInfo? 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..5deeea03a6
--- /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 System.Runtime.InteropServices;
+using BenchmarkDotNet.Extensions;
+using Perfolizer.Models;
+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 OsInfo GetOs() => Instance.os.Value;
+
+ private static OsInfo ResolveOs()
+ {
+ if (IsMacOS())
+ {
+ string systemVersion = ExternalToolsHelper.MacSystemProfilerData.Value.GetValueOrDefault("System Version") ?? "";
+ string kernelVersion = ExternalToolsHelper.MacSystemProfilerData.Value.GetValueOrDefault("Kernel Version") ?? "";
+ return new OsInfo
+ {
+ Name = "macOS",
+ Version = systemVersion,
+ KernelVersion = kernelVersion
+ };
+ }
+
+ if (IsLinux())
+ {
+ try
+ {
+ string version = LinuxOsReleaseHelper.GetNameByOsRelease(File.ReadAllLines("/etc/os-release"));
+ bool wsl = IsUnderWsl();
+ return new OsInfo
+ {
+ 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 OsInfo
+ {
+ 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/DiagnoserResults.cs b/src/BenchmarkDotNet/Diagnosers/DiagnoserResults.cs
index 68591ad8c8..6d7fb7b1db 100644
--- a/src/BenchmarkDotNet/Diagnosers/DiagnoserResults.cs
+++ b/src/BenchmarkDotNet/Diagnosers/DiagnoserResults.cs
@@ -2,6 +2,7 @@
using BenchmarkDotNet.Reports;
using BenchmarkDotNet.Running;
using BenchmarkDotNet.Toolchains.Results;
+using System.Collections.Generic;
using System.Linq;
namespace BenchmarkDotNet.Diagnosers
@@ -16,6 +17,7 @@ public DiagnoserResults(BenchmarkCase benchmarkCase, ExecuteResult executeResult
ThreadingStats = executeResult.ThreadingStats;
BuildResult = buildResult;
ExceptionFrequency = executeResult.ExceptionFrequency;
+ Measurements = executeResult.Measurements;
}
public BenchmarkCase BenchmarkCase { get; }
@@ -29,5 +31,7 @@ public DiagnoserResults(BenchmarkCase benchmarkCase, ExecuteResult executeResult
public double ExceptionFrequency { get; }
public BuildResult BuildResult { get; }
+
+ public IReadOnlyList Measurements { get; }
}
}
\ No newline at end of file
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/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..9a3d348665 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;
@@ -31,8 +32,8 @@ public class PerfCollectProfiler : IProfiler
private readonly PerfCollectProfilerConfig config;
private readonly DateTime creationTime = DateTime.Now;
- private readonly Dictionary benchmarkToTraceFile = new ();
- private readonly HashSet cliPathWithSymbolsInstalled = new ();
+ private readonly Dictionary benchmarkToTraceFile = new();
+ private readonly HashSet cliPathWithSymbolsInstalled = new();
private FileInfo perfCollectFile;
private Process perfCollectProcess;
@@ -53,7 +54,7 @@ public class PerfCollectProfiler : IProfiler
public IEnumerable Validate(ValidationParameters validationParameters)
{
- if (!RuntimeInformation.IsLinux())
+ if (!OsDetector.IsLinux())
{
yield return new ValidationError(true, "The PerfCollectProfiler works only on Linux!");
yield break;
@@ -227,7 +228,7 @@ private void EnsureSymbolsForNativeRuntime(DiagnoserActionParameters parameters)
ILogger logger = parameters.Config.GetCompositeLogger();
// We install the tool in a dedicated directory in order to always use latest version and avoid issues with broken existing configs.
string toolPath = Path.Combine(Path.GetTempPath(), "BenchmarkDotNet", "symbols");
- DotNetCliCommand cliCommand = new (
+ DotNetCliCommand cliCommand = new(
cliPath: cliPath,
arguments: $"tool install dotnet-symbol --tool-path \"{toolPath}\"",
generateResult: null,
@@ -252,7 +253,7 @@ private void EnsureSymbolsForNativeRuntime(DiagnoserActionParameters parameters)
}
private FileInfo GetTraceFile(DiagnoserActionParameters parameters, string extension)
- => new (ArtifactFileNameHelper.GetTraceFilePath(parameters, creationTime, extension)
+ => new(ArtifactFileNameHelper.GetTraceFilePath(parameters, creationTime, extension)
.Replace(" ", "_")); // perfcollect does not allow for spaces in the trace file name
}
}
diff --git a/src/BenchmarkDotNet/Diagnosers/SnapshotProfilerBase.cs b/src/BenchmarkDotNet/Diagnosers/SnapshotProfilerBase.cs
new file mode 100644
index 0000000000..3105ceeeb9
--- /dev/null
+++ b/src/BenchmarkDotNet/Diagnosers/SnapshotProfilerBase.cs
@@ -0,0 +1,195 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using BenchmarkDotNet.Analysers;
+using BenchmarkDotNet.Engines;
+using BenchmarkDotNet.Exporters;
+using BenchmarkDotNet.Jobs;
+using BenchmarkDotNet.Loggers;
+using BenchmarkDotNet.Reports;
+using BenchmarkDotNet.Running;
+using BenchmarkDotNet.Validators;
+
+namespace BenchmarkDotNet.Diagnosers;
+
+public abstract class SnapshotProfilerBase : IProfiler
+{
+ public abstract string ShortName { get; }
+
+ protected abstract void InitTool(Progress progress);
+ protected abstract void AttachToCurrentProcess(string snapshotFile);
+ protected abstract void AttachToProcessByPid(int pid, string snapshotFile);
+ protected abstract void TakeSnapshot();
+ protected abstract void Detach();
+
+ protected abstract string CreateSnapshotFilePath(DiagnoserActionParameters parameters);
+ protected abstract string GetRunnerPath();
+ internal abstract bool IsSupported(RuntimeMoniker runtimeMoniker);
+
+ private readonly List snapshotFilePaths = [];
+
+ public IEnumerable Ids => [ShortName];
+ public IEnumerable Exporters => [];
+ public IEnumerable Analysers => [];
+
+ public RunMode GetRunMode(BenchmarkCase benchmarkCase) =>
+ IsSupported(benchmarkCase.Job.Environment.GetRuntime().RuntimeMoniker) ? RunMode.ExtraRun : RunMode.None;
+
+ public void Handle(HostSignal signal, DiagnoserActionParameters parameters)
+ {
+ var logger = parameters.Config.GetCompositeLogger();
+ var job = parameters.BenchmarkCase.Job;
+
+ var runtimeMoniker = job.Environment.GetRuntime().RuntimeMoniker;
+ if (!IsSupported(runtimeMoniker))
+ {
+ logger.WriteLineError($"Runtime '{runtimeMoniker}' is not supported by dotMemory");
+ return;
+ }
+
+ switch (signal)
+ {
+ case HostSignal.BeforeAnythingElse:
+ Init(logger);
+ break;
+ case HostSignal.BeforeActualRun:
+ string snapshotFilePath = Start(logger, parameters);
+ snapshotFilePaths.Add(snapshotFilePath);
+ break;
+ case HostSignal.AfterActualRun:
+ Stop(logger);
+ break;
+ }
+ }
+
+ public IEnumerable Validate(ValidationParameters validationParameters)
+ {
+ var runtimeMonikers = validationParameters.Benchmarks.Select(b => b.Job.Environment.GetRuntime().RuntimeMoniker).Distinct();
+ foreach (var runtimeMoniker in runtimeMonikers)
+ if (!IsSupported(runtimeMoniker))
+ yield return new ValidationError(true, $"Runtime '{runtimeMoniker}' is not supported by dotMemory");
+ }
+
+ public IEnumerable ProcessResults(DiagnoserResults results) => ImmutableArray.Empty;
+
+ public void DisplayResults(ILogger logger)
+ {
+ if (snapshotFilePaths.Count != 0)
+ {
+ logger.WriteLineInfo($"The following {ShortName} snapshots were generated:");
+ foreach (string snapshotFilePath in snapshotFilePaths)
+ logger.WriteLineInfo($"* {snapshotFilePath}");
+ }
+ }
+
+ private void Init(ILogger logger)
+ {
+ try
+ {
+ logger.WriteLineInfo($"Ensuring that {ShortName} prerequisite is installed...");
+ var progress = new Progress(logger, $"Installing {ShortName}");
+ InitTool(progress);
+ logger.WriteLineInfo($"{ShortName} prerequisite is installed");
+ logger.WriteLineInfo($"{ShortName} runner path: {GetRunnerPath()}");
+ }
+ catch (Exception e)
+ {
+ logger.WriteLineError(e.ToString());
+ }
+ }
+
+ private string Start(ILogger logger, DiagnoserActionParameters parameters)
+ {
+ string snapshotFilePath = CreateSnapshotFilePath(parameters);
+ string? snapshotDirectory = Path.GetDirectoryName(snapshotFilePath);
+ logger.WriteLineInfo($"Target snapshot file: {snapshotFilePath}");
+ if (!Directory.Exists(snapshotDirectory) && snapshotDirectory != null)
+ {
+ try
+ {
+ Directory.CreateDirectory(snapshotDirectory);
+ }
+ catch (Exception e)
+ {
+ logger.WriteLineError($"Failed to create directory: {snapshotDirectory}");
+ logger.WriteLineError(e.ToString());
+ }
+ }
+
+ try
+ {
+ logger.WriteLineInfo($"Attaching {ShortName} to the process...");
+ Attach(parameters, snapshotFilePath);
+ logger.WriteLineInfo($"{ShortName} is successfully attached");
+ }
+ catch (Exception e)
+ {
+ logger.WriteLineError(e.ToString());
+ return snapshotFilePath;
+ }
+
+ return snapshotFilePath;
+ }
+
+ private void Stop(ILogger logger)
+ {
+ try
+ {
+ logger.WriteLineInfo($"Taking {ShortName} snapshot...");
+ TakeSnapshot();
+ logger.WriteLineInfo($"{ShortName} snapshot is successfully taken");
+ }
+ catch (Exception e)
+ {
+ logger.WriteLineError(e.ToString());
+ }
+
+ try
+ {
+ logger.WriteLineInfo($"Detaching {ShortName} from the process...");
+ Detach();
+ logger.WriteLineInfo($"{ShortName} is successfully detached");
+ }
+ catch (Exception e)
+ {
+ logger.WriteLineError(e.ToString());
+ }
+ }
+
+
+ private void Attach(DiagnoserActionParameters parameters, string snapshotFile)
+ {
+ int pid = parameters.Process.Id;
+ int currentPid = Process.GetCurrentProcess().Id;
+ if (pid != currentPid)
+ AttachToProcessByPid(pid, snapshotFile);
+ else
+ AttachToCurrentProcess(snapshotFile);
+ }
+
+ protected class Progress(ILogger logger, string title) : IProgress
+ {
+ private static readonly TimeSpan ReportInterval = TimeSpan.FromSeconds(0.1);
+
+ 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/Diagnosers/ThreadingDiagnoser.cs b/src/BenchmarkDotNet/Diagnosers/ThreadingDiagnoser.cs
index 057a0bb624..36b2f942c3 100644
--- a/src/BenchmarkDotNet/Diagnosers/ThreadingDiagnoser.cs
+++ b/src/BenchmarkDotNet/Diagnosers/ThreadingDiagnoser.cs
@@ -15,9 +15,10 @@ namespace BenchmarkDotNet.Diagnosers
{
public class ThreadingDiagnoser : IDiagnoser
{
- public static readonly ThreadingDiagnoser Default = new ThreadingDiagnoser();
+ public static readonly ThreadingDiagnoser Default = new ThreadingDiagnoser(new ThreadingDiagnoserConfig(displayCompletedWorkItemCountWhenZero: true, displayLockContentionWhenZero: true));
- private ThreadingDiagnoser() { }
+ public ThreadingDiagnoser(ThreadingDiagnoserConfig config) => Config = config;
+ public ThreadingDiagnoserConfig Config { get; }
public IEnumerable Ids => new[] { nameof(ThreadingDiagnoser) };
@@ -33,8 +34,9 @@ public void Handle(HostSignal signal, DiagnoserActionParameters parameters) { }
public IEnumerable ProcessResults(DiagnoserResults results)
{
- yield return new Metric(CompletedWorkItemCountMetricDescriptor.Instance, results.ThreadingStats.CompletedWorkItemCount / (double)results.ThreadingStats.TotalOperations);
- yield return new Metric(LockContentionCountMetricDescriptor.Instance, results.ThreadingStats.LockContentionCount / (double)results.ThreadingStats.TotalOperations);
+
+ yield return new Metric(new CompletedWorkItemCountMetricDescriptor(Config), results.ThreadingStats.CompletedWorkItemCount / (double)results.ThreadingStats.TotalOperations);
+ yield return new Metric(new LockContentionCountMetricDescriptor(Config), results.ThreadingStats.LockContentionCount / (double)results.ThreadingStats.TotalOperations);
}
public IEnumerable Validate(ValidationParameters validationParameters)
@@ -50,10 +52,15 @@ public IEnumerable Validate(ValidationParameters validationPara
}
}
- private class CompletedWorkItemCountMetricDescriptor : IMetricDescriptor
+ internal class CompletedWorkItemCountMetricDescriptor : IMetricDescriptor
{
internal static readonly IMetricDescriptor Instance = new CompletedWorkItemCountMetricDescriptor();
+ private ThreadingDiagnoserConfig Config { get; }
+ public CompletedWorkItemCountMetricDescriptor(ThreadingDiagnoserConfig config = null)
+ {
+ Config = config;
+ }
public string Id => "CompletedWorkItemCount";
public string DisplayName => Column.CompletedWorkItems;
public string Legend => "The number of work items that have been processed in ThreadPool (per single operation)";
@@ -62,13 +69,26 @@ private class CompletedWorkItemCountMetricDescriptor : 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.DisplayCompletedWorkItemCountWhenZero || metric.Value > 0;
+ }
}
- private class LockContentionCountMetricDescriptor : IMetricDescriptor
+ internal class LockContentionCountMetricDescriptor : IMetricDescriptor
{
internal static readonly IMetricDescriptor Instance = new LockContentionCountMetricDescriptor();
+ private ThreadingDiagnoserConfig Config { get; }
+
+ public LockContentionCountMetricDescriptor(ThreadingDiagnoserConfig config = null)
+ {
+ Config = config;
+ }
+
public string Id => "LockContentionCount";
public string DisplayName => Column.LockContentions;
public string Legend => "The number of times there was contention upon trying to take a Monitor's lock (per single operation)";
@@ -77,7 +97,13 @@ private class LockContentionCountMetricDescriptor : 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.DisplayLockContentionWhenZero || metric.Value > 0;
+ }
}
}
}
\ No newline at end of file
diff --git a/src/BenchmarkDotNet/Diagnosers/ThreadingDiagnoserConfig.cs b/src/BenchmarkDotNet/Diagnosers/ThreadingDiagnoserConfig.cs
new file mode 100644
index 0000000000..6af3da25ac
--- /dev/null
+++ b/src/BenchmarkDotNet/Diagnosers/ThreadingDiagnoserConfig.cs
@@ -0,0 +1,23 @@
+using JetBrains.Annotations;
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace BenchmarkDotNet.Diagnosers
+{
+ public class ThreadingDiagnoserConfig
+ {
+ /// 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 ThreadingDiagnoserConfig(bool displayLockContentionWhenZero = true, bool displayCompletedWorkItemCountWhenZero = true)
+ {
+ DisplayLockContentionWhenZero = displayLockContentionWhenZero;
+ DisplayCompletedWorkItemCountWhenZero = displayCompletedWorkItemCountWhenZero;
+ }
+
+ public bool DisplayLockContentionWhenZero { get; }
+ public bool DisplayCompletedWorkItemCountWhenZero { get; }
+ }
+}
diff --git a/src/BenchmarkDotNet/Diagnosers/UnresolvedDiagnoser.cs b/src/BenchmarkDotNet/Diagnosers/UnresolvedDiagnoser.cs
index f00bb4ed67..43cc910559 100644
--- a/src/BenchmarkDotNet/Diagnosers/UnresolvedDiagnoser.cs
+++ b/src/BenchmarkDotNet/Diagnosers/UnresolvedDiagnoser.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using BenchmarkDotNet.Analysers;
+using BenchmarkDotNet.Detectors;
using BenchmarkDotNet.Engines;
using BenchmarkDotNet.Exporters;
using BenchmarkDotNet.Loggers;
@@ -31,10 +32,10 @@ public IEnumerable Validate(ValidationParameters validationPara
=> new[] { new ValidationError(false, GetErrorMessage()) };
private string GetErrorMessage() => $@"Unable to resolve {unresolved.Name} diagnoser using dynamic assembly loading.
- {(RuntimeInformation.IsFullFramework || RuntimeInformation.IsWindows()
+ {(RuntimeInformation.IsFullFramework || OsDetector.IsWindows()
? "Please make sure that you have installed the latest BenchmarkDotNet.Diagnostics.Windows package. " + Environment.NewLine
+ "If you are using `dotnet build` you also need to consume one of its public types to make sure that MSBuild copies it to the output directory. "
+ "The alternative is to use `true` in your project file."
- : $"Please make sure that it's supported on {RuntimeInformation.GetOsVersion()}")}";
+ : $"Please make sure that it's supported on {OsDetector.GetOs()}")}";
}
}
\ No newline at end of file
diff --git a/src/BenchmarkDotNet/Disassemblers/Arm64Disassembler.cs b/src/BenchmarkDotNet/Disassemblers/Arm64Disassembler.cs
index 16ebbde219..7c10765b8e 100644
--- a/src/BenchmarkDotNet/Disassemblers/Arm64Disassembler.cs
+++ b/src/BenchmarkDotNet/Disassemblers/Arm64Disassembler.cs
@@ -139,7 +139,7 @@ public void Feed(Arm64Instruction instruction)
public Arm64RegisterId RegisterId { get { return _registerId; } }
}
- internal class Arm64Disassembler : ClrMdV2Disassembler
+ internal class Arm64Disassembler : ClrMdV3Disassembler
{
internal sealed class RuntimeSpecificData
{
@@ -189,7 +189,7 @@ internal RuntimeSpecificData(State state)
}
}
- private static readonly Dictionary runtimeSpecificData = new ();
+ private static readonly Dictionary runtimeSpecificData = new();
protected override IEnumerable Decode(byte[] code, ulong startAddress, State state, int depth, ClrMethod currentMethod, DisassemblySyntax syntax)
{
diff --git a/src/BenchmarkDotNet/Disassemblers/Arm64InstructionFormatter.cs b/src/BenchmarkDotNet/Disassemblers/Arm64InstructionFormatter.cs
index deb557256c..5426dcfdd9 100644
--- a/src/BenchmarkDotNet/Disassemblers/Arm64InstructionFormatter.cs
+++ b/src/BenchmarkDotNet/Disassemblers/Arm64InstructionFormatter.cs
@@ -12,7 +12,7 @@ internal static class Arm64InstructionFormatter
internal static string Format(Arm64Asm asm, FormatterOptions formatterOptions,
bool printInstructionAddresses, uint pointerSize, IReadOnlyDictionary symbols)
{
- StringBuilder output = new ();
+ StringBuilder output = new();
Arm64Instruction instruction = asm.Instruction;
if (printInstructionAddresses)
diff --git a/src/BenchmarkDotNet/Disassemblers/ClrMdV2Disassembler.cs b/src/BenchmarkDotNet/Disassemblers/ClrMdV3Disassembler.cs
similarity index 75%
rename from src/BenchmarkDotNet/Disassemblers/ClrMdV2Disassembler.cs
rename to src/BenchmarkDotNet/Disassemblers/ClrMdV3Disassembler.cs
index 5a0ddccafb..6bd01426d8 100644
--- a/src/BenchmarkDotNet/Disassemblers/ClrMdV2Disassembler.cs
+++ b/src/BenchmarkDotNet/Disassemblers/ClrMdV3Disassembler.cs
@@ -7,23 +7,25 @@
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
+using BenchmarkDotNet.Detectors;
using BenchmarkDotNet.Portability;
namespace BenchmarkDotNet.Disassemblers
{
- // This Disassembler uses ClrMd v2x. Please keep it in sync with ClrMdV1Disassembler (if possible).
- internal abstract class ClrMdV2Disassembler
+ // This Disassembler uses ClrMd v3x. Please keep it in sync with ClrMdV1Disassembler (if possible).
+ internal abstract class ClrMdV3Disassembler
+
{
private static readonly ulong MinValidAddress = GetMinValidAddress();
private static ulong GetMinValidAddress()
{
// https://github.com/dotnet/BenchmarkDotNet/pull/2413#issuecomment-1688100117
- if (RuntimeInformation.IsWindows())
+ if (OsDetector.IsWindows())
return ushort.MaxValue + 1;
- if (RuntimeInformation.IsLinux())
+ if (OsDetector.IsLinux())
return (ulong) Environment.SystemPageSize;
- if (RuntimeInformation.IsMacOS())
+ if (OsDetector.IsMacOS())
return RuntimeInformation.GetCurrentPlatform() switch
{
Environments.Platform.X86 or Environments.Platform.X64 => 4096,
@@ -64,7 +66,7 @@ internal DisassemblyResult AttachAndDisassemble(Settings settings)
state.Todo.Enqueue(
new MethodInfo(
// the Disassembler Entry Method is always parameterless, so check by name is enough
- typeWithBenchmark.Methods.Single(method => method.IsPublic && method.Name == settings.MethodName),
+ typeWithBenchmark.Methods.Single(method => method.Attributes.HasFlag(System.Reflection.MethodAttributes.Public) && method.Name == settings.MethodName),
0));
}
@@ -126,6 +128,7 @@ private DisassembledMethod[] Disassemble(Settings settings, State state)
var result = new List();
DisassemblySyntax syntax = (DisassemblySyntax)Enum.Parse(typeof(DisassemblySyntax), settings.Syntax);
+ using var sourceCodeProvider = new SourceCodeProvider();
while (state.Todo.Count != 0)
{
var methodInfo = state.Todo.Dequeue();
@@ -134,7 +137,7 @@ private DisassembledMethod[] Disassemble(Settings settings, State state)
continue; // already handled
if (settings.MaxDepth >= methodInfo.Depth)
- result.Add(DisassembleMethod(methodInfo, state, settings, syntax));
+ result.Add(DisassembleMethod(methodInfo, state, settings, syntax, sourceCodeProvider));
}
return result.ToArray();
@@ -142,15 +145,16 @@ private DisassembledMethod[] Disassemble(Settings settings, State state)
private static bool CanBeDisassembled(ClrMethod method) => method.ILOffsetMap.Length > 0 && method.NativeCode > 0;
- private DisassembledMethod DisassembleMethod(MethodInfo methodInfo, State state, Settings settings, DisassemblySyntax syntax)
+ private DisassembledMethod DisassembleMethod(MethodInfo methodInfo, State state, Settings settings, DisassemblySyntax syntax, SourceCodeProvider sourceCodeProvider)
{
var method = methodInfo.Method;
if (!CanBeDisassembled(method))
{
- if (method.IsPInvoke)
+ if (method.Attributes.HasFlag(System.Reflection.MethodAttributes.PinvokeImpl))
return CreateEmpty(method, "PInvoke method");
- if (method.IL is null || method.IL.Length == 0)
+ var ilInfo = method.GetILInfo();
+ if (ilInfo is null || ilInfo.Length == 0)
return CreateEmpty(method, "Extern method");
if (method.CompilationType == MethodCompilationType.None)
return CreateEmpty(method, "Method was not JITted yet.");
@@ -165,7 +169,7 @@ private DisassembledMethod DisassembleMethod(MethodInfo methodInfo, State 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);
@@ -213,60 +217,30 @@ private IEnumerable Decode(ILToNativeMap map, State state, int depth, ClrMe
private static ILToNativeMap[] GetCompleteNativeMap(ClrMethod method, ClrRuntime runtime)
{
- if (!TryReadNativeCodeAddresses(runtime, method, out ulong startAddress, out ulong endAddress))
+ // it's better to use one single map rather than few small ones
+ // it's simply easier to get next instruction when decoding ;)
+
+ var hotColdInfo = method.HotColdInfo;
+ if (hotColdInfo.HotSize > 0 && hotColdInfo.HotStart > 0)
{
- startAddress = method.NativeCode;
- endAddress = ulong.MaxValue;
+ return hotColdInfo.ColdSize <= 0
+ ? new[] { new ILToNativeMap() { StartAddress = hotColdInfo.HotStart, EndAddress = hotColdInfo.HotStart + hotColdInfo.HotSize, ILOffset = -1 } }
+ : new[]
+ {
+ new ILToNativeMap() { StartAddress = hotColdInfo.HotStart, EndAddress = hotColdInfo.HotStart + hotColdInfo.HotSize, ILOffset = -1 },
+ new ILToNativeMap() { StartAddress = hotColdInfo.ColdStart, EndAddress = hotColdInfo.ColdStart + hotColdInfo.ColdSize, ILOffset = -1 }
+ };
}
- ILToNativeMap[] sortedMaps = method.ILOffsetMap // CanBeDisassembled ensures that there is at least one map in ILOffsetMap
- .Where(map => map.StartAddress >= startAddress && map.StartAddress < endAddress) // can be false for Tier 0 maps, EndAddress is not checked on purpose here
- .Where(map => map.StartAddress < map.EndAddress) // some maps have 0 length (they don't have corresponding assembly code?)
+ return method.ILOffsetMap
+ .Where(map => map.StartAddress < map.EndAddress) // some maps have 0 length?
.OrderBy(map => map.StartAddress) // we need to print in the machine code order, not IL! #536
- .Select(map => new ILToNativeMap()
- {
- StartAddress = map.StartAddress,
- // some maps have EndAddress > codeHeaderData.MethodStart + codeHeaderData.MethodSize and contain garbage (#2074). They need to be fixed!
- EndAddress = Math.Min(map.EndAddress, endAddress),
- ILOffset = map.ILOffset
- })
.ToArray();
-
- if (sortedMaps.Length == 0)
- {
- // In such situation ILOffsetMap most likely describes Tier 0, while CodeHeaderData Tier 1.
- // Since we care about Tier 1 (if it's present), we "fake" a Tier 1 map.
- return new[] { new ILToNativeMap() { StartAddress = startAddress, EndAddress = endAddress } };
- }
- else if (sortedMaps[0].StartAddress != startAddress || (sortedMaps[sortedMaps.Length - 1].EndAddress != endAddress && endAddress != ulong.MaxValue))
- {
- // In such situation ILOffsetMap most likely is missing few bytes. We just "extend" it to avoid producing "bad" instructions.
- return new[] { new ILToNativeMap() { StartAddress = startAddress, EndAddress = endAddress } };
- }
-
- return sortedMaps;
}
private static DisassembledMethod CreateEmpty(ClrMethod method, string reason)
=> DisassembledMethod.Empty(method.Signature, method.NativeCode, reason);
- protected static bool TryReadNativeCodeAddresses(ClrRuntime runtime, ClrMethod method, out ulong startAddress, out ulong endAddress)
- {
- if (method is not null
- && runtime.DacLibrary.SOSDacInterface.GetCodeHeaderData(method.NativeCode, out var codeHeaderData) == HResult.S_OK
- && codeHeaderData.MethodSize > 0) // false for extern methods!
- {
- // HotSize can be missing or be invalid (https://github.com/microsoft/clrmd/issues/1036).
- // So we fetch the method size on our own.
- startAddress = codeHeaderData.MethodStart;
- endAddress = codeHeaderData.MethodStart + codeHeaderData.MethodSize;
- return true;
- }
-
- startAddress = endAddress = 0;
- return false;
- }
-
protected void TryTranslateAddressToName(ulong address, bool isAddressPrecodeMD, State state, int depth, ClrMethod currentMethod)
{
if (!IsValidAddress(address) || state.AddressToNameMapping.ContainsKey(address))
@@ -282,18 +256,10 @@ protected void TryTranslateAddressToName(ulong address, bool isAddressPrecodeMD,
}
var method = runtime.GetMethodByInstructionPointer(address);
- if (method is null && (address & ((uint) runtime.DataTarget.DataReader.PointerSize - 1)) == 0)
- {
- if (runtime.DataTarget.DataReader.ReadPointer(address, out ulong newAddress) && IsValidAddress(newAddress))
- {
- method = runtime.GetMethodByInstructionPointer(newAddress);
-
- method = WorkaroundGetMethodByInstructionPointerBug(runtime, method, newAddress);
- }
- }
- else
+ if (method is null && (address & ((uint) runtime.DataTarget.DataReader.PointerSize - 1)) == 0
+ && runtime.DataTarget.DataReader.ReadPointer(address, out ulong newAddress) && IsValidAddress(newAddress))
{
- method = WorkaroundGetMethodByInstructionPointerBug(runtime, method, address);
+ method = runtime.GetMethodByInstructionPointer(newAddress);
}
if (method is null)
@@ -312,7 +278,7 @@ protected void TryTranslateAddressToName(ulong address, bool isAddressPrecodeMD,
return;
}
- var methodTableName = runtime.DacLibrary.SOSDacInterface.GetMethodTableName(address);
+ var methodTableName = runtime.GetTypeByMethodTable(address)?.Name;
if (!string.IsNullOrEmpty(methodTableName))
{
state.AddressToNameMapping.Add(address, $"MT_{methodTableName}");
@@ -334,7 +300,7 @@ protected void TryTranslateAddressToName(ulong address, bool isAddressPrecodeMD,
protected void FlushCachedDataIfNeeded(IDataReader dataTargetDataReader, ulong address, byte[] buffer)
{
- if (!RuntimeInformation.IsWindows())
+ if (!OsDetector.IsWindows())
{
if (dataTargetDataReader.Read(address, buffer) <= 0)
{
@@ -348,13 +314,6 @@ protected void FlushCachedDataIfNeeded(IDataReader dataTargetDataReader, ulong a
}
}
- // GetMethodByInstructionPointer sometimes returns wrong methods.
- // In case given address does not belong to the methods range, null is returned.
- private static ClrMethod WorkaroundGetMethodByInstructionPointerBug(ClrRuntime runtime, ClrMethod method, ulong newAddress)
- => TryReadNativeCodeAddresses(runtime, method, out ulong startAddress, out ulong endAddress) && !(startAddress >= newAddress && newAddress <= endAddress)
- ? null
- : method;
-
private class SharpComparer : IEqualityComparer
{
public bool Equals(Sharp x, Sharp y)
diff --git a/src/BenchmarkDotNet/Disassemblers/DisassemblyDiagnoser.cs b/src/BenchmarkDotNet/Disassemblers/DisassemblyDiagnoser.cs
index 31e7570e86..7d0f95326c 100644
--- a/src/BenchmarkDotNet/Disassemblers/DisassemblyDiagnoser.cs
+++ b/src/BenchmarkDotNet/Disassemblers/DisassemblyDiagnoser.cs
@@ -3,6 +3,7 @@
using System.Linq;
using BenchmarkDotNet.Analysers;
using BenchmarkDotNet.Columns;
+using BenchmarkDotNet.Detectors;
using BenchmarkDotNet.Disassemblers;
using BenchmarkDotNet.Disassemblers.Exporters;
using BenchmarkDotNet.Engines;
@@ -16,6 +17,7 @@
using BenchmarkDotNet.Running;
using BenchmarkDotNet.Toolchains.InProcess.NoEmit;
using BenchmarkDotNet.Validators;
+using Perfolizer.Metrology;
namespace BenchmarkDotNet.Diagnosers
{
@@ -74,7 +76,7 @@ public void Handle(HostSignal signal, DiagnoserActionParameters parameters)
case HostSignal.AfterAll when ShouldUseSameArchitectureDisassembler(benchmark, parameters):
results.Add(benchmark, sameArchitectureDisassembler.Disassemble(parameters));
break;
- case HostSignal.AfterAll when RuntimeInformation.IsWindows() && !ShouldUseMonoDisassembler(benchmark):
+ case HostSignal.AfterAll when OsDetector.IsWindows() && !ShouldUseMonoDisassembler(benchmark):
results.Add(benchmark, windowsDifferentArchitectureDisassembler.Disassemble(parameters));
break;
case HostSignal.SeparateLogic when ShouldUseMonoDisassembler(benchmark):
@@ -111,7 +113,7 @@ public IEnumerable Validate(ValidationParameters validationPara
if (ShouldUseClrMdDisassembler(benchmark))
{
- if (RuntimeInformation.IsLinux())
+ if (OsDetector.IsLinux())
{
var runtime = benchmark.Job.ResolveValue(EnvironmentMode.RuntimeCharacteristic, EnvironmentResolver.Instance);
@@ -142,13 +144,13 @@ private static bool ShouldUseMonoDisassembler(BenchmarkCase benchmarkCase)
// when we add macOS support, RuntimeInformation.IsMacOS() needs to be added here
private static bool ShouldUseClrMdDisassembler(BenchmarkCase benchmarkCase)
- => !ShouldUseMonoDisassembler(benchmarkCase) && (RuntimeInformation.IsWindows() || RuntimeInformation.IsLinux());
+ => !ShouldUseMonoDisassembler(benchmarkCase) && (OsDetector.IsWindows() || OsDetector.IsLinux());
private static bool ShouldUseSameArchitectureDisassembler(BenchmarkCase benchmarkCase, DiagnoserActionParameters parameters)
{
if (ShouldUseClrMdDisassembler(benchmarkCase))
{
- if (RuntimeInformation.IsWindows())
+ if (OsDetector.IsWindows())
{
return WindowsDisassembler.GetDisassemblerArchitecture(parameters.Process, benchmarkCase.Job.Environment.Platform)
== RuntimeInformation.GetCurrentPlatform();
@@ -194,7 +196,7 @@ private class NativeCodeSizeMetricDescriptor : IMetricDescriptor
public string Legend => "Native code size of the disassembled method(s)";
public string NumberFormat => "N0";
public UnitType UnitType => UnitType.CodeSize;
- 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/Disassemblers/IntelDisassembler.cs b/src/BenchmarkDotNet/Disassemblers/IntelDisassembler.cs
index f29faae037..eb0d5ce3f6 100644
--- a/src/BenchmarkDotNet/Disassemblers/IntelDisassembler.cs
+++ b/src/BenchmarkDotNet/Disassemblers/IntelDisassembler.cs
@@ -7,7 +7,7 @@
namespace BenchmarkDotNet.Disassemblers
{
- internal class IntelDisassembler : ClrMdV2Disassembler
+ internal class IntelDisassembler : ClrMdV3Disassembler
{
internal sealed class RuntimeSpecificData
{
@@ -46,7 +46,7 @@ internal RuntimeSpecificData(State state)
}
}
- private static readonly Dictionary runtimeSpecificData = new ();
+ private static readonly Dictionary runtimeSpecificData = new();
protected override IEnumerable Decode(byte[] code, ulong startAddress, State state, int depth, ClrMethod currentMethod, DisassemblySyntax syntax)
{
diff --git a/src/BenchmarkDotNet/Disassemblers/SameArchitectureDisassembler.cs b/src/BenchmarkDotNet/Disassemblers/SameArchitectureDisassembler.cs
index cf810cdd86..1a4835484d 100644
--- a/src/BenchmarkDotNet/Disassemblers/SameArchitectureDisassembler.cs
+++ b/src/BenchmarkDotNet/Disassemblers/SameArchitectureDisassembler.cs
@@ -8,16 +8,16 @@ namespace BenchmarkDotNet.Disassemblers
internal class SameArchitectureDisassembler
{
private readonly DisassemblyDiagnoserConfig config;
- private ClrMdV2Disassembler? clrMdV2Disassembler;
+ private ClrMdV3Disassembler? clrMdV3Disassembler;
internal SameArchitectureDisassembler(DisassemblyDiagnoserConfig config) => this.config = config;
internal DisassemblyResult Disassemble(DiagnoserActionParameters parameters)
// delay the creation to avoid exceptions at DisassemblyDiagnoser ctor
- => (clrMdV2Disassembler ??= CreateDisassemblerForCurrentArchitecture())
+ => (clrMdV3Disassembler ??= CreateDisassemblerForCurrentArchitecture())
.AttachAndDisassemble(BuildDisassemblerSettings(parameters));
- private static ClrMdV2Disassembler CreateDisassemblerForCurrentArchitecture()
+ private static ClrMdV3Disassembler CreateDisassemblerForCurrentArchitecture()
=> RuntimeInformation.GetCurrentPlatform() switch
{
Platform.X86 or Platform.X64 => new IntelDisassembler(),
@@ -26,7 +26,7 @@ private static ClrMdV2Disassembler CreateDisassemblerForCurrentArchitecture()
};
private Settings BuildDisassemblerSettings(DiagnoserActionParameters parameters)
- => new (
+ => new(
processId: parameters.Process.Id,
typeName: $"BenchmarkDotNet.Autogenerated.Runnable_{parameters.BenchmarkId.Value}",
methodName: DisassemblerConstants.DisassemblerEntryMethodName,
diff --git a/src/BenchmarkDotNet/Disassemblers/SourceCodeProvider.cs b/src/BenchmarkDotNet/Disassemblers/SourceCodeProvider.cs
index 777c24370e..beaeebfaa7 100644
--- a/src/BenchmarkDotNet/Disassemblers/SourceCodeProvider.cs
+++ b/src/BenchmarkDotNet/Disassemblers/SourceCodeProvider.cs
@@ -7,14 +7,21 @@
namespace BenchmarkDotNet.Disassemblers
{
- internal static class SourceCodeProvider
+ internal class SourceCodeProvider : IDisposable
{
- private static readonly Dictionary SourceFileCache = new Dictionary();
- private static readonly Dictionary SourceFilePathsCache = new Dictionary();
+ private readonly Dictionary sourceFileCache = new Dictionary();
+ private readonly Dictionary sourceFilePathsCache = new Dictionary();
+ private readonly Dictionary pdbReaders = new Dictionary();
+ private readonly SymbolReader symbolReader = new SymbolReader(TextWriter.Null) { SymbolPath = SymbolPath.MicrosoftSymbolServerPath };
- internal static IEnumerable GetSource(ClrMethod method, ILToNativeMap map)
+ public void Dispose()
{
- var sourceLocation = method.GetSourceLocation(map.ILOffset);
+ symbolReader.Dispose();
+ }
+
+ internal IEnumerable GetSource(ClrMethod method, ILToNativeMap map)
+ {
+ var sourceLocation = GetSourceLocation(method, map.ILOffset);
if (sourceLocation == null)
yield break;
@@ -39,12 +46,12 @@ internal static IEnumerable GetSource(ClrMethod method, ILToNativeMap map
}
}
- private static string GetFilePath(SourceFile sourceFile)
- => SourceFilePathsCache.TryGetValue(sourceFile, out string filePath) ? filePath : sourceFile.Url;
+ private string GetFilePath(SourceFile sourceFile)
+ => sourceFilePathsCache.TryGetValue(sourceFile, out string filePath) ? filePath : sourceFile.Url;
- private static string ReadSourceLine(SourceFile file, int line)
+ private string ReadSourceLine(SourceFile file, int line)
{
- if (!SourceFileCache.TryGetValue(file, out string[] contents))
+ if (!sourceFileCache.TryGetValue(file, out string[] contents))
{
// GetSourceFile method returns path when file is stored on the same machine
// otherwise it downloads it from the Symbol Server and returns the source code ;)
@@ -56,14 +63,14 @@ private static string ReadSourceLine(SourceFile file, int line)
if (File.Exists(wholeFileOrJustPath))
{
contents = File.ReadAllLines(wholeFileOrJustPath);
- SourceFilePathsCache.Add(file, wholeFileOrJustPath);
+ sourceFilePathsCache.Add(file, wholeFileOrJustPath);
}
else
{
contents = wholeFileOrJustPath.Split(new string[] { Environment.NewLine }, StringSplitOptions.None);
}
- SourceFileCache.Add(file, contents);
+ sourceFileCache.Add(file, contents);
}
return line - 1 < contents.Length
@@ -99,17 +106,8 @@ private static string GetSmartPointer(string sourceLine, int? start, int? end)
return new string(prefix);
}
- }
-
- 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();
- private static readonly SymbolReader symbolReader = new SymbolReader(TextWriter.Null) { SymbolPath = SymbolPath.MicrosoftSymbolServerPath };
- internal static SourceLocation GetSourceLocation(this ClrMethod method, int ilOffset)
+ internal SourceLocation GetSourceLocation(ClrMethod method, int ilOffset)
{
var reader = GetReaderForMethod(method);
if (reader == null)
@@ -118,7 +116,7 @@ internal static SourceLocation GetSourceLocation(this ClrMethod method, int ilOf
return reader.SourceLocationForManagedCode((uint)method.MetadataToken, ilOffset);
}
- internal static SourceLocation GetSourceLocation(this ClrStackFrame frame)
+ internal SourceLocation GetSourceLocation(ClrStackFrame frame)
{
var reader = GetReaderForMethod(frame.Method);
if (reader == null)
@@ -145,7 +143,7 @@ private static int FindIlOffset(ClrStackFrame frame)
return last;
}
- private static ManagedSymbolModule GetReaderForMethod(ClrMethod method)
+ private ManagedSymbolModule GetReaderForMethod(ClrMethod method)
{
ClrModule module = method?.Type?.Module;
PdbInfo info = module?.Pdb;
@@ -153,7 +151,7 @@ private static ManagedSymbolModule GetReaderForMethod(ClrMethod method)
ManagedSymbolModule? reader = null;
if (info != null)
{
- if (!s_pdbReaders.TryGetValue(info, out reader))
+ if (!pdbReaders.TryGetValue(info, out reader))
{
string pdbPath = info.Path;
if (pdbPath != null)
@@ -173,7 +171,7 @@ private static ManagedSymbolModule GetReaderForMethod(ClrMethod method)
}
}
- s_pdbReaders[info] = reader;
+ pdbReaders[info] = reader;
}
}
diff --git a/src/BenchmarkDotNet/Disassemblers/WindowsDisassembler.cs b/src/BenchmarkDotNet/Disassemblers/WindowsDisassembler.cs
index 6bec70162a..8051ba3463 100644
--- a/src/BenchmarkDotNet/Disassemblers/WindowsDisassembler.cs
+++ b/src/BenchmarkDotNet/Disassemblers/WindowsDisassembler.cs
@@ -7,6 +7,7 @@
using System.Text;
using System.Xml;
using System.Xml.Serialization;
+using BenchmarkDotNet.Detectors;
using BenchmarkDotNet.Diagnosers;
using BenchmarkDotNet.Environments;
using BenchmarkDotNet.Extensions;
@@ -165,7 +166,7 @@ public static bool Is64Bit(Process process)
if (Environment.GetEnvironmentVariable("PROCESSOR_ARCHITECTURE") == "x86")
return false;
- if (RuntimeInformation.IsWindows())
+ if (OsDetector.IsWindows())
{
IsWow64Process(process.Handle, out bool isWow64);
diff --git a/src/BenchmarkDotNet/Engines/Engine.cs b/src/BenchmarkDotNet/Engines/Engine.cs
index 29d2e0ddd7..1fc1b96dfd 100644
--- a/src/BenchmarkDotNet/Engines/Engine.cs
+++ b/src/BenchmarkDotNet/Engines/Engine.cs
@@ -1,10 +1,14 @@
using System;
using System.Collections.Generic;
+using System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Runtime.CompilerServices;
+using System.Threading;
using BenchmarkDotNet.Characteristics;
+using BenchmarkDotNet.Environments;
using BenchmarkDotNet.Jobs;
+using BenchmarkDotNet.Mathematics;
using BenchmarkDotNet.Portability;
using BenchmarkDotNet.Reports;
using JetBrains.Annotations;
@@ -15,8 +19,6 @@ namespace BenchmarkDotNet.Engines
[UsedImplicitly]
public class Engine : IEngine
{
- public const int MinInvokeCount = 4;
-
[PublicAPI] public IHost Host { get; }
[PublicAPI] public Action WorkloadAction { get; }
[PublicAPI] public Action Dummy1Action { get; }
@@ -40,10 +42,7 @@ public class Engine : IEngine
private bool EvaluateOverhead { get; }
private bool MemoryRandomization { get; }
- private readonly List jittingMeasurements = new (10);
- private readonly EnginePilotStage pilotStage;
- private readonly EngineWarmupStage warmupStage;
- private readonly EngineActualStage actualStage;
+ private readonly List jittingMeasurements = new(10);
private readonly bool includeExtraStats;
private readonly Random random;
@@ -79,10 +78,6 @@ internal Engine(
EvaluateOverhead = targetJob.ResolveValue(AccuracyMode.EvaluateOverheadCharacteristic, Resolver);
MemoryRandomization = targetJob.ResolveValue(RunMode.MemoryRandomizationCharacteristic, Resolver);
- warmupStage = new EngineWarmupStage(this);
- pilotStage = new EnginePilotStage(this);
- actualStage = new EngineActualStage(this);
-
random = new Random(12345); // we are using constant seed to try to get repeatable results
}
@@ -102,6 +97,9 @@ public void Dispose()
}
}
+ // AggressiveOptimization forces the method to go straight to tier1 JIT, and will never be re-jitted,
+ // eliminating tiered JIT as a potential variable in measurements.
+ [MethodImpl(CodeGenHelper.AggressiveOptimizationOption)]
public RunResults Run()
{
var measurements = new List();
@@ -112,29 +110,33 @@ public RunResults Run()
if (EngineEventSource.Log.IsEnabled())
EngineEventSource.Log.BenchmarkStart(BenchmarkName);
- if (Strategy != RunStrategy.ColdStart)
+ // Enumerate the stages and run iterations in a loop to ensure each benchmark invocation is called with a constant stack size.
+ // #1120
+ foreach (var stage in EngineStage.EnumerateStages(this, Strategy, EvaluateOverhead))
{
- if (Strategy != RunStrategy.Monitoring)
+ if (stage.Stage == IterationStage.Actual && stage.Mode == IterationMode.Workload)
{
- var pilotStageResult = pilotStage.Run();
- invokeCount = pilotStageResult.PerfectInvocationCount;
- measurements.AddRange(pilotStageResult.Measurements);
-
- if (EvaluateOverhead)
- {
- measurements.AddRange(warmupStage.RunOverhead(invokeCount, UnrollFactor));
- measurements.AddRange(actualStage.RunOverhead(invokeCount, UnrollFactor));
- }
+ Host.BeforeMainRun();
}
- measurements.AddRange(warmupStage.RunWorkload(invokeCount, UnrollFactor, Strategy));
- }
-
- Host.BeforeMainRun();
+ var stageMeasurements = stage.GetMeasurementList();
+ // 1-based iterationIndex
+ int iterationIndex = 1;
+ while (stage.GetShouldRunIteration(stageMeasurements, ref invokeCount))
+ {
+ var measurement = RunIteration(new IterationData(stage.Mode, stage.Stage, iterationIndex, invokeCount, UnrollFactor));
+ stageMeasurements.Add(measurement);
+ ++iterationIndex;
+ }
+ measurements.AddRange(stageMeasurements);
- measurements.AddRange(actualStage.RunWorkload(invokeCount, UnrollFactor, forceSpecific: Strategy == RunStrategy.Monitoring));
+ WriteLine();
- Host.AfterMainRun();
+ if (stage.Stage == IterationStage.Actual && stage.Mode == IterationMode.Workload)
+ {
+ Host.AfterMainRun();
+ }
+ }
(GcStats workGcHasDone, ThreadingStats threadingStats, double exceptionFrequency) = includeExtraStats
? GetExtraStats(new IterationData(IterationMode.Workload, IterationStage.Actual, 0, invokeCount, UnrollFactor))
@@ -148,11 +150,15 @@ public RunResults Run()
return new RunResults(measurements, outlierMode, workGcHasDone, threadingStats, exceptionFrequency);
}
+ [MethodImpl(CodeGenHelper.AggressiveOptimizationOption)]
public Measurement RunIteration(IterationData data)
{
// Initialization
long invokeCount = data.InvokeCount;
int unrollFactor = data.UnrollFactor;
+ if (invokeCount % unrollFactor != 0)
+ throw new ArgumentOutOfRangeException(nameof(data), $"InvokeCount({invokeCount}) should be a multiple of UnrollFactor({unrollFactor}).");
+
long totalOperations = invokeCount * OperationsPerInvoke;
bool isOverhead = data.IterationMode == IterationMode.Overhead;
bool randomizeMemory = !isOverhead && MemoryRandomization;
@@ -166,12 +172,9 @@ public Measurement RunIteration(IterationData data)
if (EngineEventSource.Log.IsEnabled())
EngineEventSource.Log.IterationStart(data.IterationMode, data.IterationStage, totalOperations);
- Span stackMemory = randomizeMemory ? stackalloc byte[random.Next(32)] : Span.Empty;
-
- // Measure
- var clock = Clock.Start();
- action(invokeCount / unrollFactor);
- var clockSpan = clock.GetElapsed();
+ var clockSpan = randomizeMemory
+ ? MeasureWithRandomStack(action, invokeCount / unrollFactor)
+ : Measure(action, invokeCount / unrollFactor);
if (EngineEventSource.Log.IsEnabled())
EngineEventSource.Log.IterationStop(data.IterationMode, data.IterationStage, totalOperations);
@@ -190,42 +193,85 @@ public Measurement RunIteration(IterationData data)
if (measurement.IterationStage == IterationStage.Jitting)
jittingMeasurements.Add(measurement);
+ return measurement;
+ }
+
+ // This is in a separate method, because stackalloc can affect code alignment,
+ // resulting in unexpected measurements on some AMD cpus,
+ // even if the stackalloc branch isn't executed. (#2366)
+ [MethodImpl(MethodImplOptions.NoInlining | CodeGenHelper.AggressiveOptimizationOption)]
+ private unsafe ClockSpan MeasureWithRandomStack(Action action, long invokeCount)
+ {
+ byte* stackMemory = stackalloc byte[random.Next(32)];
+ var clockSpan = Measure(action, invokeCount);
Consume(stackMemory);
+ return clockSpan;
+ }
- return measurement;
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ private unsafe void Consume(byte* _) { }
+
+ [MethodImpl(MethodImplOptions.NoInlining | CodeGenHelper.AggressiveOptimizationOption)]
+ private ClockSpan Measure(Action action, long invokeCount)
+ {
+ var clock = Clock.Start();
+ action(invokeCount);
+ return clock.GetElapsed();
}
private (GcStats, ThreadingStats, double) GetExtraStats(IterationData data)
{
- // we enable monitoring after main target run, for this single iteration which is executed at the end
- // so even if we enable AppDomain monitoring in separate process
- // it does not matter, because we have already obtained the results!
- EnableMonitoring();
+ // Warm up the measurement functions before starting the actual measurement.
+ DeadCodeEliminationHelper.KeepAliveWithoutBoxing(GcStats.ReadInitial());
+ DeadCodeEliminationHelper.KeepAliveWithoutBoxing(GcStats.ReadFinal());
IterationSetupAction(); // we run iteration setup first, so even if it allocates, it is not included in the results
var initialThreadingStats = ThreadingStats.ReadInitial(); // this method might allocate
var exceptionsStats = new ExceptionsStats(); // allocates
exceptionsStats.StartListening(); // this method might allocate
- var initialGcStats = GcStats.ReadInitial();
- WorkloadAction(data.InvokeCount / data.UnrollFactor);
+#if !NET7_0_OR_GREATER
+ if (RuntimeInformation.IsNetCore && Environment.Version.Major is >= 3 and <= 6 && RuntimeInformation.IsTieredJitEnabled)
+ {
+ // #1542
+ // We put the current thread to sleep so tiered jit can kick in, compile its stuff,
+ // and NOT allocate anything on the background thread when we are measuring allocations.
+ // This is only an issue on netcoreapp3.0 to net6.0. Tiered jit allocations were "fixed" in net7.0
+ // (maybe not completely eliminated forever, but at least reduced to a point where measurements are much more stable),
+ // and netcoreapp2.X uses only GetAllocatedBytesForCurrentThread which doesn't capture the tiered jit allocations.
+ Thread.Sleep(TimeSpan.FromMilliseconds(500));
+ }
+#endif
+
+ // GC collect before measuring allocations.
+ ForceGcCollect();
+ GcStats gcStats;
+ using (FinalizerBlocker.MaybeStart())
+ {
+ gcStats = MeasureWithGc(data.InvokeCount / data.UnrollFactor);
+ }
- exceptionsStats.Stop();
- var finalGcStats = GcStats.ReadFinal();
+ exceptionsStats.Stop(); // this method might (de)allocate
var finalThreadingStats = ThreadingStats.ReadFinal();
IterationCleanupAction(); // we run iteration cleanup after collecting GC stats
var totalOperationsCount = data.InvokeCount * OperationsPerInvoke;
- GcStats gcStats = (finalGcStats - initialGcStats).WithTotalOperations(totalOperationsCount);
- ThreadingStats threadingStats = (finalThreadingStats - initialThreadingStats).WithTotalOperations(data.InvokeCount * OperationsPerInvoke);
-
- return (gcStats, threadingStats, exceptionsStats.ExceptionsCount / (double)totalOperationsCount);
+ return (gcStats.WithTotalOperations(totalOperationsCount),
+ (finalThreadingStats - initialThreadingStats).WithTotalOperations(totalOperationsCount),
+ exceptionsStats.ExceptionsCount / (double)totalOperationsCount);
}
- [MethodImpl(MethodImplOptions.NoInlining)]
- private void Consume(in Span _) { }
+ // Isolate the allocation measurement and skip tier0 jit to make sure we don't get any unexpected allocations.
+ [MethodImpl(MethodImplOptions.NoInlining | CodeGenHelper.AggressiveOptimizationOption)]
+ private GcStats MeasureWithGc(long invokeCount)
+ {
+ var initialGcStats = GcStats.ReadInitial();
+ WorkloadAction(invokeCount);
+ var finalGcStats = GcStats.ReadFinal();
+ return finalGcStats - initialGcStats;
+ }
private void RandomizeManagedHeapMemory()
{
@@ -253,7 +299,7 @@ private void GcCollect()
ForceGcCollect();
}
- private static void ForceGcCollect()
+ internal static void ForceGcCollect()
{
GC.Collect();
GC.WaitForPendingFinalizers();
@@ -264,15 +310,6 @@ private static void ForceGcCollect()
public void WriteLine() => Host.WriteLine();
- private static void EnableMonitoring()
- {
- if (RuntimeInformation.IsOldMono) // Monitoring is not available in Mono, see http://stackoverflow.com/questions/40234948/how-to-get-the-number-of-allocated-bytes-in-mono
- return;
-
- if (RuntimeInformation.IsFullFramework)
- AppDomain.MonitoringIsEnabled = true;
- }
-
[UsedImplicitly]
public static class Signals
{
@@ -295,5 +332,71 @@ private static readonly Dictionary MessagesToSignals
public static bool TryGetSignal(string message, out HostSignal signal)
=> MessagesToSignals.TryGetValue(message, out signal);
}
+
+ // Very long key and value so this shouldn't be used outside of unit tests.
+ internal const string UnitTestBlockFinalizerEnvKey = "BENCHMARKDOTNET_UNITTEST_BLOCK_FINALIZER_FOR_MEMORYDIAGNOSER";
+ internal const string UnitTestBlockFinalizerEnvValue = UnitTestBlockFinalizerEnvKey + "_ACTIVE";
+
+ // To prevent finalizers interfering with allocation measurements for unit tests,
+ // we block the finalizer thread until we've completed the measurement.
+ // https://github.com/dotnet/runtime/issues/101536#issuecomment-2077647417
+ private readonly struct FinalizerBlocker : IDisposable
+ {
+ private readonly object hangLock;
+
+ private FinalizerBlocker(object hangLock) => this.hangLock = hangLock;
+
+ private sealed class Impl
+ {
+ // ManualResetEvent(Slim) allocates when it is waited and yields the thread,
+ // so we use Monitor.Wait instead which does not allocate managed memory.
+ // This behavior is not documented, but was observed with the VS Profiler.
+ private readonly object hangLock = new();
+ private readonly ManualResetEventSlim enteredFinalizerEvent = new(false);
+
+ ~Impl()
+ {
+ lock (hangLock)
+ {
+ enteredFinalizerEvent.Set();
+ Monitor.Wait(hangLock);
+ }
+ }
+
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ internal static (object hangLock, ManualResetEventSlim enteredFinalizerEvent) CreateWeakly()
+ {
+ var impl = new Impl();
+ return (impl.hangLock, impl.enteredFinalizerEvent);
+ }
+ }
+
+ internal static FinalizerBlocker MaybeStart()
+ {
+ if (Environment.GetEnvironmentVariable(UnitTestBlockFinalizerEnvKey) != UnitTestBlockFinalizerEnvValue)
+ {
+ return default;
+ }
+ var (hangLock, enteredFinalizerEvent) = Impl.CreateWeakly();
+ do
+ {
+ GC.Collect();
+ // Do NOT call GC.WaitForPendingFinalizers.
+ }
+ while (!enteredFinalizerEvent.IsSet);
+ return new FinalizerBlocker(hangLock);
+ }
+
+ public void Dispose()
+ {
+ if (hangLock is not null)
+ {
+ lock (hangLock)
+ {
+ Monitor.Pulse(hangLock);
+ }
+ }
+ }
+ }
}
}
\ No newline at end of file
diff --git a/src/BenchmarkDotNet/Engines/EngineActualStage.cs b/src/BenchmarkDotNet/Engines/EngineActualStage.cs
new file mode 100644
index 0000000000..173f4e3432
--- /dev/null
+++ b/src/BenchmarkDotNet/Engines/EngineActualStage.cs
@@ -0,0 +1,96 @@
+using System;
+using System.Collections.Generic;
+using BenchmarkDotNet.Characteristics;
+using BenchmarkDotNet.Jobs;
+using BenchmarkDotNet.Mathematics;
+using BenchmarkDotNet.Reports;
+using Perfolizer.Horology;
+using Perfolizer.Mathematics.OutlierDetection;
+
+namespace BenchmarkDotNet.Engines
+{
+ internal abstract class EngineActualStage(IterationMode iterationMode) : EngineStage(IterationStage.Actual, iterationMode)
+ {
+ internal const int MaxOverheadIterationCount = 20;
+
+ internal static EngineActualStage GetOverhead(IEngine engine)
+ => new EngineActualStageAuto(engine.TargetJob, engine.Resolver, IterationMode.Overhead);
+
+ internal static EngineActualStage GetWorkload(IEngine engine, RunStrategy strategy)
+ {
+ var targetJob = engine.TargetJob;
+ int? iterationCount = targetJob.ResolveValueAsNullable(RunMode.IterationCountCharacteristic);
+ const int DefaultWorkloadCount = 10;
+ return iterationCount == null && strategy != RunStrategy.Monitoring
+ ? new EngineActualStageAuto(targetJob, engine.Resolver, IterationMode.Workload)
+ : new EngineActualStageSpecific(iterationCount ?? DefaultWorkloadCount, IterationMode.Workload);
+ }
+ }
+
+ internal sealed class EngineActualStageAuto : EngineActualStage
+ {
+ private readonly double maxRelativeError;
+ private readonly TimeInterval? maxAbsoluteError;
+ private readonly OutlierMode outlierMode;
+ private readonly int minIterationCount;
+ private readonly int maxIterationCount;
+ private readonly List measurementsForStatistics;
+ private int iterationCounter = 0;
+
+ public EngineActualStageAuto(Job targetJob, IResolver resolver, IterationMode iterationMode) : base(iterationMode)
+ {
+ maxRelativeError = targetJob.ResolveValue(AccuracyMode.MaxRelativeErrorCharacteristic, resolver);
+ maxAbsoluteError = targetJob.ResolveValueAsNullable(AccuracyMode.MaxAbsoluteErrorCharacteristic);
+ outlierMode = targetJob.ResolveValue(AccuracyMode.OutlierModeCharacteristic, resolver);
+ minIterationCount = targetJob.ResolveValue(RunMode.MinIterationCountCharacteristic, resolver);
+ maxIterationCount = targetJob.ResolveValue(RunMode.MaxIterationCountCharacteristic, resolver);
+ measurementsForStatistics = GetMeasurementList();
+ }
+
+ internal override List GetMeasurementList() => new(maxIterationCount);
+
+ internal override bool GetShouldRunIteration(List measurements, ref long invokeCount)
+ {
+ if (measurements.Count == 0)
+ {
+ return true;
+ }
+
+ const double MaxOverheadRelativeError = 0.05;
+ bool isOverhead = Mode == IterationMode.Overhead;
+ double effectiveMaxRelativeError = isOverhead ? MaxOverheadRelativeError : maxRelativeError;
+ iterationCounter++;
+ var measurement = measurements[measurements.Count - 1];
+ measurementsForStatistics.Add(measurement);
+
+ var statistics = MeasurementsStatistics.Calculate(measurementsForStatistics, outlierMode);
+ double actualError = statistics.LegacyConfidenceInterval.Margin;
+
+ double maxError1 = effectiveMaxRelativeError * statistics.Mean;
+ double maxError2 = maxAbsoluteError?.Nanoseconds ?? double.MaxValue;
+ double maxError = Math.Min(maxError1, maxError2);
+
+ if (iterationCounter >= minIterationCount && actualError < maxError)
+ {
+ return false;
+ }
+
+ if (iterationCounter >= maxIterationCount || isOverhead && iterationCounter >= MaxOverheadIterationCount)
+ {
+ return false;
+ }
+
+ return true;
+ }
+ }
+
+ internal sealed class EngineActualStageSpecific(int maxIterationCount, IterationMode iterationMode) : EngineActualStage(iterationMode)
+ {
+ private int iterationCount = 0;
+
+ internal override List GetMeasurementList() => new(maxIterationCount);
+
+ internal override bool GetShouldRunIteration(List measurements, ref long invokeCount)
+ => ++iterationCount <= maxIterationCount;
+ }
+}
\ No newline at end of file
diff --git a/src/BenchmarkDotNet/Engines/EngineGeneralStage.cs b/src/BenchmarkDotNet/Engines/EngineGeneralStage.cs
deleted file mode 100644
index cecd779899..0000000000
--- a/src/BenchmarkDotNet/Engines/EngineGeneralStage.cs
+++ /dev/null
@@ -1,90 +0,0 @@
-using System;
-using System.Collections.Generic;
-using BenchmarkDotNet.Jobs;
-using BenchmarkDotNet.Mathematics;
-using BenchmarkDotNet.Reports;
-using Perfolizer.Horology;
-using Perfolizer.Mathematics.OutlierDetection;
-
-namespace BenchmarkDotNet.Engines
-{
- public class EngineActualStage : EngineStage
- {
- internal const int MaxOverheadIterationCount = 20;
- private const double MaxOverheadRelativeError = 0.05;
- private const int DefaultWorkloadCount = 10;
-
- private readonly int? iterationCount;
- private readonly double maxRelativeError;
- private readonly TimeInterval? maxAbsoluteError;
- private readonly OutlierMode outlierMode;
- private readonly int minIterationCount;
- private readonly int maxIterationCount;
-
- public EngineActualStage(IEngine engine) : base(engine)
- {
- iterationCount = engine.TargetJob.ResolveValueAsNullable(RunMode.IterationCountCharacteristic);
- maxRelativeError = engine.TargetJob.ResolveValue(AccuracyMode.MaxRelativeErrorCharacteristic, engine.Resolver);
- maxAbsoluteError = engine.TargetJob.ResolveValueAsNullable(AccuracyMode.MaxAbsoluteErrorCharacteristic);
- outlierMode = engine.TargetJob.ResolveValue(AccuracyMode.OutlierModeCharacteristic, engine.Resolver);
- minIterationCount = engine.TargetJob.ResolveValue(RunMode.MinIterationCountCharacteristic, engine.Resolver);
- maxIterationCount = engine.TargetJob.ResolveValue(RunMode.MaxIterationCountCharacteristic, engine.Resolver);
- }
-
- public IReadOnlyList RunOverhead(long invokeCount, int unrollFactor)
- => RunAuto(invokeCount, IterationMode.Overhead, unrollFactor);
-
- public IReadOnlyList RunWorkload(long invokeCount, int unrollFactor, bool forceSpecific = false)
- => Run(invokeCount, IterationMode.Workload, false, unrollFactor, forceSpecific);
-
- internal IReadOnlyList Run(long invokeCount, IterationMode iterationMode, bool runAuto, int unrollFactor, bool forceSpecific = false)
- => (runAuto || iterationCount == null) && !forceSpecific
- ? RunAuto(invokeCount, iterationMode, unrollFactor)
- : RunSpecific(invokeCount, iterationMode, iterationCount ?? DefaultWorkloadCount, unrollFactor);
-
- private List