├── .gitattributes ├── .github ├── dependabot.yml └── workflows │ └── dotnet.yml ├── .gitignore ├── BenchmarkDotNetVisualizer.sln ├── LICENSE ├── README.md ├── docs ├── assets │ ├── bootstrap.min.css │ ├── cshtml-razor.min.js │ └── site.css ├── cshtml-razor.min.js ├── iteration-benchmark1.html ├── iteration-benchmark2.html ├── json-serializers-benchmark.html └── old-index.html ├── samples ├── IterationBenchmark │ ├── IterationBenchmark.cs │ ├── IterationBenchmark.csproj │ ├── Program.cs │ └── Reports │ │ ├── JoinedBenchmark-PivotBy-Method-Dark.html │ │ ├── JoinedBenchmark-PivotBy-Method-Dark.png │ │ ├── JoinedBenchmark-PivotBy-Method-Light.png │ │ ├── JoinedBenchmark-PivotBy-Runtime-Dark.html │ │ ├── JoinedBenchmark-PivotBy-Runtime-Dark.png │ │ └── JoinedBenchmark-PivotBy-Runtime-Light.png └── JsonSerializersBenchmark │ ├── JsonSerializersBenchmark.cs │ ├── JsonSerializersBenchmark.csproj │ ├── Program.cs │ └── Reports │ ├── Benchmark-Dark.html │ ├── Benchmark-Dark.png │ └── Benchmark-Light.png ├── src ├── BenchmarkDotNet │ ├── Attributes │ │ ├── RichHtmlExporterAttribute.cs │ │ ├── RichImageExporterAttribute.cs │ │ └── RichMarkdownExporterAttribute.cs │ ├── BenchmarkAutoRunner.cs │ ├── CustomTagColumn.cs │ ├── DebugInProcessConfigDry.cs │ └── Exporters │ │ ├── RichHtmlExporter.cs │ │ ├── RichImageExporter.cs │ │ └── RichMarkdownExporter.cs ├── BenchmarkDotNetVisualizer.csproj ├── Core │ ├── BenchmarkInfo.cs │ ├── BenchmarkReportProcessor.cs │ ├── BenchmarkVisualizer.ConcatReports.Options.cs │ ├── BenchmarkVisualizer.ConcatReports.cs │ ├── BenchmarkVisualizer.JoinReports.Options.cs │ ├── BenchmarkVisualizer.JoinReports.cs │ ├── BenchmarkVisualizer.SimpleReport.Options.cs │ ├── BenchmarkVisualizer.SimpleReport.cs │ └── BenchmarkVisualizer.Utils.cs ├── DataProcessor.T.cs ├── GlobalSuppressions.cs ├── Utilities │ ├── ColorHelper.cs │ ├── DirectoryHelper.cs │ ├── Enumerable │ │ ├── EnumerableExtensions.Orderer.cs │ │ └── EnumerableExtensions.cs │ ├── EnumerableExtensions.Orderer.T.cs │ ├── ExpandoObject │ │ ├── ExpandoObjectExtensions.ColumnOrdering.cs │ │ ├── ExpandoObjectExtensions.cs │ │ └── MetaPropertyExtensions.cs │ ├── FileLockHunter.cs │ ├── Guard.cs │ ├── Html │ │ ├── Browser.cs │ │ ├── HtmlDocumentWrapMode.cs │ │ ├── HtmlHelper.Image.cs │ │ ├── HtmlHelper.Render.cs │ │ ├── RenderTableDividerMode.cs │ │ └── Theme.cs │ ├── HtmlHelper.T.cs │ ├── Image │ │ ├── ImageCompressionLevel.cs │ │ ├── ImageFormat.cs │ │ ├── ImageHelper.cs │ │ └── ImageSharpShims.cs │ ├── Markdown │ │ ├── MarkdownHelper.Parse.cs │ │ ├── MarkdownHelper.Render.cs │ │ └── ParseTableDividerMode.cs │ ├── MarkdownHelper.T.cs │ ├── NETShims │ │ ├── NETShims.Exception.cs │ │ └── NETShims.LinqExtensions.cs │ ├── NumberExtensions.cs │ ├── ReflectionExtensions.cs │ └── StreamExtensions.cs └── package-icon.png └── test └── BenchmarkDotNetVisualizer.Tests ├── BenchmarkDotNet.Artifacts ├── Benchmarks.ContainsBenchmark_ClassInts_-report-github.md ├── Benchmarks.ContainsBenchmark_ClassStrings_-report-github.md ├── Benchmarks.ContainsBenchmark_Int32_-report-github.md ├── Benchmarks.ContainsBenchmark_RecordClassInts_-report-github.md ├── Benchmarks.ContainsBenchmark_RecordClassStrings_-report-github.md ├── Benchmarks.ContainsBenchmark_RecordStructInts_-report-github.md ├── Benchmarks.ContainsBenchmark_RecordStructStrings_-report-github.md ├── Benchmarks.ContainsBenchmark_String_-report-github.md ├── Benchmarks.ContainsBenchmark_StructInts_-report-github.md ├── Benchmarks.ContainsBenchmark_StructStrings_-report-github.md ├── Benchmarks.InitializeBenchmark_ClassInts_-report-github.md ├── Benchmarks.InitializeBenchmark_ClassStrings_-report-github.md ├── Benchmarks.InitializeBenchmark_Int32_-report-github.md ├── Benchmarks.InitializeBenchmark_RecordClassInts_-report-github.md ├── Benchmarks.InitializeBenchmark_RecordClassStrings_-report-github.md ├── Benchmarks.InitializeBenchmark_RecordStructInts_-report-github.md ├── Benchmarks.InitializeBenchmark_RecordStructStrings_-report-github.md ├── Benchmarks.InitializeBenchmark_String_-report-github.md ├── Benchmarks.InitializeBenchmark_StructInts_-report-github.md ├── Benchmarks.InitializeBenchmark_StructStrings_-report-github.md ├── Benchmarks.TryGetValueBenchmark_ClassInts_-report-github.md ├── Benchmarks.TryGetValueBenchmark_ClassStrings_-report-github.md ├── Benchmarks.TryGetValueBenchmark_Int32_-report-github.md ├── Benchmarks.TryGetValueBenchmark_RecordClassInts_-report-github.md ├── Benchmarks.TryGetValueBenchmark_RecordClassStrings_-report-github.md ├── Benchmarks.TryGetValueBenchmark_RecordStructInts_-report-github.md ├── Benchmarks.TryGetValueBenchmark_RecordStructStrings_-report-github.md ├── Benchmarks.TryGetValueBenchmark_String_-report-github.md ├── Benchmarks.TryGetValueBenchmark_StructInts_-report-github.md ├── Benchmarks.TryGetValueBenchmark_StructStrings_-report-github.md ├── IterationBenchmark-report-github.md └── JsonSerializersBenchmark-report-github.md ├── BenchmarkDotNetVisualizer.Tests.csproj ├── DotNetCollectionsBenchmarkTests.cs ├── ImageComparer.cs ├── IterationBenchmarkTests.cs ├── JsonSerializersBenchmarkTests.cs ├── TestBase.cs └── _snapshots ├── DotNetCollectionsBenchmarkTests.Benchmark_Initialize_Allocated.png ├── DotNetCollectionsBenchmarkTests.Benchmark_Initialize_Allocated.verified.html ├── DotNetCollectionsBenchmarkTests.Benchmark_Initialize_Mean.png ├── DotNetCollectionsBenchmarkTests.Benchmark_Initialize_Mean.verified.html ├── DotNetCollectionsBenchmarkTests.Benchmark_SearchContains_Allocated.png ├── DotNetCollectionsBenchmarkTests.Benchmark_SearchContains_Allocated.verified.html ├── DotNetCollectionsBenchmarkTests.Benchmark_SearchContains_Mean.png ├── DotNetCollectionsBenchmarkTests.Benchmark_SearchContains_Mean.verified.html ├── DotNetCollectionsBenchmarkTests.Benchmark_SearchTryGetValue_Allocated.png ├── DotNetCollectionsBenchmarkTests.Benchmark_SearchTryGetValue_Allocated.verified.html ├── DotNetCollectionsBenchmarkTests.Benchmark_SearchTryGetValue_Mean.png ├── DotNetCollectionsBenchmarkTests.Benchmark_SearchTryGetValue_Mean.verified.html ├── IterationBenchmarkTests.Joined_PivotBy_Method_Dark.png ├── IterationBenchmarkTests.Joined_PivotBy_Method_Dark.verified.html ├── IterationBenchmarkTests.Joined_PivotBy_Method_Light.png ├── IterationBenchmarkTests.Joined_PivotBy_Method_Light.verified.html ├── IterationBenchmarkTests.Joined_PivotBy_Runtime_Dark.png ├── IterationBenchmarkTests.Joined_PivotBy_Runtime_Dark.verified.html ├── IterationBenchmarkTests.Joined_PivotBy_Runtime_Light.png ├── IterationBenchmarkTests.Joined_PivotBy_Runtime_Light.verified.html ├── JsonSerializersBenchmarkTests.Simple_Dark.png ├── JsonSerializersBenchmarkTests.Simple_Dark.verified.html ├── JsonSerializersBenchmarkTests.Simple_Light.png └── JsonSerializersBenchmarkTests.Simple_Light.verified.html /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "nuget" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /.github/workflows/dotnet.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a .NET project 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-net 3 | 4 | name: .NET 5 | 6 | on: 7 | push: 8 | branches: [ "master" ] 9 | pull_request: 10 | branches: [ "master" ] 11 | workflow_dispatch: 12 | 13 | jobs: 14 | build: 15 | 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - name: Checkout 20 | uses: actions/checkout@v4 21 | 22 | - name: Setup .NET 23 | uses: actions/setup-dotnet@v4 24 | with: 25 | dotnet-version: 9.x.x 26 | 27 | - name: Restore dependencies 28 | run: dotnet restore 29 | 30 | - name: Build (Release) 31 | run: dotnet build --configuration Release --no-restore 32 | 33 | - name: Test (Release) 34 | run: dotnet test --configuration Release --no-build --no-restore 35 | 36 | - name: Upload Artifacts 37 | uses: actions/upload-artifact@v4 38 | if: failure() 39 | with: 40 | name: test-artifact 41 | path: | 42 | ./test/BenchmarkDotNetVisualizer.Tests/_reports 43 | ./test/BenchmarkDotNetVisualizer.Tests/_snapshots 44 | retention-days: 5 45 | 46 | - name: Pack (Release) 47 | run: dotnet pack --configuration Release --output ./nuget --no-build --no-restore 48 | 49 | - name: Publish 50 | if: github.event_name == 'workflow_dispatch' # manually run 51 | run: | 52 | if [[ ${{github.ref}} =~ ^refs/tags/[0-9]+\.[0-9]+\.[0-9]+$ ]] 53 | then 54 | dotnet nuget push ./nuget/*.nupkg -s nuget.org -k ${{secrets.NUGET_TOKEN}} --skip-duplicate 55 | else 56 | echo "publish is only enabled by tagging with a release tag" 57 | fi 58 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.userosscache 8 | *.sln.docstates 9 | 10 | # User-specific files (MonoDevelop/Xamarin Studio) 11 | *.userprefs 12 | 13 | # Build results 14 | [Dd]ebug/ 15 | [Dd]ebugPublic/ 16 | [Rr]elease/ 17 | [Rr]eleases/ 18 | x64/ 19 | x86/ 20 | bld/ 21 | [Bb]in/ 22 | [Oo]bj/ 23 | [Ll]og/ 24 | 25 | # Visual Studio 2015 cache/options directory 26 | .vs/ 27 | # Uncomment if you have tasks that create the project's static files in wwwroot 28 | #wwwroot/ 29 | 30 | # MSTest test Results 31 | [Tt]est[Rr]esult*/ 32 | [Bb]uild[Ll]og.* 33 | 34 | # NUNIT 35 | *.VisualState.xml 36 | TestResult.xml 37 | 38 | # Build Results of an ATL Project 39 | [Dd]ebugPS/ 40 | [Rr]eleasePS/ 41 | dlldata.c 42 | 43 | # DNX 44 | project.lock.json 45 | project.fragment.lock.json 46 | artifacts/ 47 | 48 | *_i.c 49 | *_p.c 50 | *_i.h 51 | *.ilk 52 | *.meta 53 | *.obj 54 | *.pch 55 | *.pdb 56 | *.pgc 57 | *.pgd 58 | *.rsp 59 | *.sbr 60 | *.tlb 61 | *.tli 62 | *.tlh 63 | *.tmp 64 | *.tmp_proj 65 | *.log 66 | *.vspscc 67 | *.vssscc 68 | .builds 69 | *.pidb 70 | *.svclog 71 | *.scc 72 | 73 | # Chutzpah Test files 74 | _Chutzpah* 75 | 76 | # Visual C++ cache files 77 | ipch/ 78 | *.aps 79 | *.ncb 80 | *.opendb 81 | *.opensdf 82 | *.sdf 83 | *.cachefile 84 | *.VC.db 85 | *.VC.VC.opendb 86 | 87 | # Visual Studio profiler 88 | *.psess 89 | *.vsp 90 | *.vspx 91 | *.sap 92 | 93 | # TFS 2012 Local Workspace 94 | $tf/ 95 | 96 | # Guidance Automation Toolkit 97 | *.gpState 98 | 99 | # ReSharper is a .NET coding add-in 100 | _ReSharper*/ 101 | *.[Rr]e[Ss]harper 102 | *.DotSettings.user 103 | 104 | # JustCode is a .NET coding add-in 105 | .JustCode 106 | 107 | # TeamCity is a build add-in 108 | _TeamCity* 109 | 110 | # DotCover is a Code Coverage Tool 111 | *.dotCover 112 | 113 | # NCrunch 114 | _NCrunch_* 115 | .*crunch*.local.xml 116 | nCrunchTemp_* 117 | 118 | # MightyMoose 119 | *.mm.* 120 | AutoTest.Net/ 121 | 122 | # Web workbench (sass) 123 | .sass-cache/ 124 | 125 | # Installshield output folder 126 | [Ee]xpress/ 127 | 128 | # DocProject is a documentation generator add-in 129 | DocProject/buildhelp/ 130 | DocProject/Help/*.HxT 131 | DocProject/Help/*.HxC 132 | DocProject/Help/*.hhc 133 | DocProject/Help/*.hhk 134 | DocProject/Help/*.hhp 135 | DocProject/Help/Html2 136 | DocProject/Help/html 137 | 138 | # Click-Once directory 139 | publish/ 140 | 141 | # Publish Web Output 142 | *.[Pp]ublish.xml 143 | *.azurePubxml 144 | # TODO: Comment the next line if you want to checkin your web deploy settings 145 | # but database connection strings (with potential passwords) will be unencrypted 146 | #*.pubxml 147 | *.publishproj 148 | 149 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 150 | # checkin your Azure Web App publish settings, but sensitive information contained 151 | # in these scripts will be unencrypted 152 | PublishScripts/ 153 | 154 | # NuGet Packages 155 | *.nupkg 156 | # The packages folder can be ignored because of Package Restore 157 | **/packages/* 158 | # except build/, which is used as an MSBuild target. 159 | !**/packages/build/ 160 | # Uncomment if necessary however generally it will be regenerated when needed 161 | #!**/packages/repositories.config 162 | # NuGet v3's project.json files produces more ignoreable files 163 | *.nuget.props 164 | *.nuget.targets 165 | 166 | # Microsoft Azure Build Output 167 | csx/ 168 | *.build.csdef 169 | 170 | # Microsoft Azure Emulator 171 | ecf/ 172 | rcf/ 173 | 174 | # Windows Store app package directories and files 175 | AppPackages/ 176 | BundleArtifacts/ 177 | Package.StoreAssociation.xml 178 | _pkginfo.txt 179 | 180 | # Visual Studio cache files 181 | # files ending in .cache can be ignored 182 | *.[Cc]ache 183 | # but keep track of directories ending in .cache 184 | !*.[Cc]ache/ 185 | 186 | # Others 187 | ClientBin/ 188 | ~$* 189 | *~ 190 | *.dbmdl 191 | *.dbproj.schemaview 192 | *.jfm 193 | *.pfx 194 | *.publishsettings 195 | node_modules/ 196 | orleans.codegen.cs 197 | 198 | # Since there are multiple workflows, uncomment next line to ignore bower_components 199 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 200 | #bower_components/ 201 | 202 | # RIA/Silverlight projects 203 | Generated_Code/ 204 | 205 | # Backup & report files from converting an old project file 206 | # to a newer Visual Studio version. Backup files are not needed, 207 | # because we have git ;-) 208 | _UpgradeReport_Files/ 209 | Backup*/ 210 | UpgradeLog*.XML 211 | UpgradeLog*.htm 212 | 213 | # SQL Server files 214 | *.mdf 215 | *.ldf 216 | 217 | # Business Intelligence projects 218 | *.rdl.data 219 | *.bim.layout 220 | *.bim_*.settings 221 | 222 | # Microsoft Fakes 223 | FakesAssemblies/ 224 | 225 | # GhostDoc plugin setting file 226 | *.GhostDoc.xml 227 | 228 | # Node.js Tools for Visual Studio 229 | .ntvs_analysis.dat 230 | 231 | # Visual Studio 6 build log 232 | *.plg 233 | 234 | # Visual Studio 6 workspace options file 235 | *.opt 236 | 237 | # Visual Studio LightSwitch build output 238 | **/*.HTMLClient/GeneratedArtifacts 239 | **/*.DesktopClient/GeneratedArtifacts 240 | **/*.DesktopClient/ModelManifest.xml 241 | **/*.Server/GeneratedArtifacts 242 | **/*.Server/ModelManifest.xml 243 | _Pvt_Extensions 244 | 245 | # Paket dependency manager 246 | .paket/paket.exe 247 | paket-files/ 248 | 249 | # FAKE - F# Make 250 | .fake/ 251 | 252 | # JetBrains Rider 253 | .idea/ 254 | *.sln.iml 255 | 256 | # CodeRush 257 | .cr/ 258 | 259 | # Python Tools for Visual Studio (PTVS) 260 | __pycache__/ 261 | *.pyc 262 | 263 | **/_reports/*.html 264 | **/_reports/*.png -------------------------------------------------------------------------------- /BenchmarkDotNetVisualizer.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.8.34330.188 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BenchmarkDotNetVisualizer", "src\BenchmarkDotNetVisualizer.csproj", "{BDB45959-AE1C-4D3F-B9A3-92480C93706A}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "JsonSerializersBenchmark", "samples\JsonSerializersBenchmark\JsonSerializersBenchmark.csproj", "{21CB02F6-7B14-46FB-A9CA-A6C77DB130CE}" 9 | EndProject 10 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{FC6DB147-234F-4CF9-8F34-60D869028E4F}" 11 | EndProject 12 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IterationBenchmark", "samples\IterationBenchmark\IterationBenchmark.csproj", "{898658F4-F1A1-41F6-BEA8-3B6D0465799A}" 13 | EndProject 14 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{255CC077-F37F-4240-8691-E609D876CE40}" 15 | EndProject 16 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BenchmarkDotNetVisualizer.Tests", "test\BenchmarkDotNetVisualizer.Tests\BenchmarkDotNetVisualizer.Tests.csproj", "{8E9DDEAC-0CAA-4C1E-98B7-3FFC8A0AB123}" 17 | EndProject 18 | Global 19 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 20 | Debug|Any CPU = Debug|Any CPU 21 | Release|Any CPU = Release|Any CPU 22 | EndGlobalSection 23 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 24 | {BDB45959-AE1C-4D3F-B9A3-92480C93706A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 25 | {BDB45959-AE1C-4D3F-B9A3-92480C93706A}.Debug|Any CPU.Build.0 = Debug|Any CPU 26 | {BDB45959-AE1C-4D3F-B9A3-92480C93706A}.Release|Any CPU.ActiveCfg = Release|Any CPU 27 | {BDB45959-AE1C-4D3F-B9A3-92480C93706A}.Release|Any CPU.Build.0 = Release|Any CPU 28 | {21CB02F6-7B14-46FB-A9CA-A6C77DB130CE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 29 | {21CB02F6-7B14-46FB-A9CA-A6C77DB130CE}.Debug|Any CPU.Build.0 = Debug|Any CPU 30 | {21CB02F6-7B14-46FB-A9CA-A6C77DB130CE}.Release|Any CPU.ActiveCfg = Release|Any CPU 31 | {21CB02F6-7B14-46FB-A9CA-A6C77DB130CE}.Release|Any CPU.Build.0 = Release|Any CPU 32 | {898658F4-F1A1-41F6-BEA8-3B6D0465799A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 33 | {898658F4-F1A1-41F6-BEA8-3B6D0465799A}.Debug|Any CPU.Build.0 = Debug|Any CPU 34 | {898658F4-F1A1-41F6-BEA8-3B6D0465799A}.Release|Any CPU.ActiveCfg = Release|Any CPU 35 | {898658F4-F1A1-41F6-BEA8-3B6D0465799A}.Release|Any CPU.Build.0 = Release|Any CPU 36 | {8E9DDEAC-0CAA-4C1E-98B7-3FFC8A0AB123}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 37 | {8E9DDEAC-0CAA-4C1E-98B7-3FFC8A0AB123}.Debug|Any CPU.Build.0 = Debug|Any CPU 38 | {8E9DDEAC-0CAA-4C1E-98B7-3FFC8A0AB123}.Release|Any CPU.ActiveCfg = Release|Any CPU 39 | {8E9DDEAC-0CAA-4C1E-98B7-3FFC8A0AB123}.Release|Any CPU.Build.0 = Release|Any CPU 40 | EndGlobalSection 41 | GlobalSection(SolutionProperties) = preSolution 42 | HideSolutionNode = FALSE 43 | EndGlobalSection 44 | GlobalSection(NestedProjects) = preSolution 45 | {21CB02F6-7B14-46FB-A9CA-A6C77DB130CE} = {FC6DB147-234F-4CF9-8F34-60D869028E4F} 46 | {898658F4-F1A1-41F6-BEA8-3B6D0465799A} = {FC6DB147-234F-4CF9-8F34-60D869028E4F} 47 | {8E9DDEAC-0CAA-4C1E-98B7-3FFC8A0AB123} = {255CC077-F37F-4240-8691-E609D876CE40} 48 | EndGlobalSection 49 | GlobalSection(ExtensibilityGlobals) = postSolution 50 | SolutionGuid = {F69802B0-D9DB-4D05-ACFA-287E4E26EB4B} 51 | EndGlobalSection 52 | EndGlobal 53 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Mohammad Javad Ebrahimi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /docs/assets/cshtml-razor.min.js: -------------------------------------------------------------------------------- 1 | hljs.registerLanguage("cshtml-razor",(()=>{"use strict";return n=>{ 2 | var e="built_in",s={},a={begin:"}",className:e,endsParent:!0},i={begin:"{", 3 | end:"}",contains:[n.QUOTE_STRING_MODE,"self"]},r=n.COMMENT("@\\*","\\*@",{ 4 | relevance:10}),g={begin:"@[A-Za-z0-9\\._:-]+",returnBegin:!0, 5 | end:"(\\r|\\n|<|\\s|\"|')",subLanguage:"csharp",contains:[{begin:"@",className:e 6 | },{begin:"\\[",end:"\\]",skip:!0},{begin:"\\(",end:"\\)",skip:!0}],returnEnd:!0 7 | },t={begin:"[@]{0,1}",returnBegin:!0,end:"",returnEnd:!0, 8 | subLanguage:"cshtml-razor",contains:[{begin:"[@]{0,1}",className:e},{ 9 | begin:"",className:e,endsParent:!0}]},c={begin:"@\\(",end:"\\)", 10 | returnBegin:!0,returnEnd:!0,subLanguage:"csharp",contains:[{begin:"@\\(", 11 | className:e},{begin:"\\(",end:"\\)",subLanguage:"csharp", 12 | contains:[n.QUOTE_STRING_MODE,"self",t]},t,{begin:"\\)",className:e, 13 | endsParent:!0}]},b=((n,e)=>{var s={endsWithParent:!0,illegal:/`]+/}]}]}]} 17 | ;return[{className:"meta",begin:"",relevance:10,contains:[{ 18 | begin:"\\[",end:"\\]"}]},n.COMMENT("\x3c!--","--\x3e",{relevance:10}),{ 19 | begin:"<\\!\\[CDATA\\[",end:"\\]\\]>",relevance:10},{className:"meta", 20 | begin:/<\?xml/,end:/\?>/,relevance:10},{className:"tag", 21 | begin:"|$)",end:">",keywords:{name:"style"},contains:[s],starts:{ 22 | end:"",returnEnd:!0,subLanguage:["css","xml"]}},{className:"tag", 23 | begin:"|$)",end:">",keywords:{name:"script"},contains:[s], 24 | starts:{end:"<\/script>",returnEnd:!0, 25 | subLanguage:["actionscript","javascript","handlebars","xml"]}},{className:"tag", 26 | begin:"",contains:[{className:"name",begin:/[^\/><\s]+/,relevance:0 27 | },s]}].concat(e) 28 | })(n,[g,c]),l="^\\s*@(page|model|using|inherits|inject|layout)",u={ 29 | begin:l+"[^\\r\\n{\\(]*$",end:"$",returnBegin:!0,returnEnd:!0,contains:[{ 30 | begin:l,className:e},{variants:[{begin:"\\r|\\n",endsParent:!0},{ 31 | begin:"\\s[^\\r\\n]+",end:"$"},{begin:"$"}],className:"type",endsParent:!0}] 32 | },d={variants:[{begin:"@\\{",end:"}"},{begin:"@code\\s*\\{",end:"}"}], 33 | returnBegin:!0,returnEnd:!0,subLanguage:"csharp",contains:[{ 34 | begin:"@(code\\s*)?\\{",className:e},s,{begin:"{",end:"}",contains:["self"], 35 | skip:!0},a]},o={begin:"^\\s*@helper[\\s]*[^{]+[\\s]*{",returnBegin:!0, 36 | returnEnd:!0,end:"}",subLanguage:"cshtml-razor",contains:[{begin:"@helper", 37 | className:e},{begin:"{",className:e},a]},m=[{begin:"@for[\\s]*\\([^{]+[\\s]*{", 38 | end:"}"},{begin:"@if[\\s]*\\([^{]+[\\s]*{",end:"}"},{ 39 | begin:"@switch[\\s]*\\([^{]+[\\s]*{",end:"}"},{ 40 | begin:"@while[\\s]*\\([^{]+[\\s]*{",end:"}"},{ 41 | begin:"@using[\\s]*\\([^{]+[\\s]*{",end:"}"},{ 42 | begin:"@lock[\\s]*\\([^{]+[\\s]*{",end:"}"},{ 43 | begin:"@foreach[\\s]*\\([^{]+[\\s]*{",end:"}"}],N={variants:m,returnBegin:!0, 44 | returnEnd:!0,subLanguage:"csharp",contains:[{variants:m.map((n=>({begin:n.begin 45 | }))),returnBegin:!0,contains:[{begin:"@",className:e},{variants:m.map((n=>({ 46 | begin:n.begin.substr(1,n.begin.length-2)}))),subLanguage:"csharp"},{begin:"{", 47 | className:e}]},s,{variants:[{begin:"}[\\s]*else\\sif[\\s]*\\([^{]+[\\s]*{"},{ 48 | begin:"}[\\s]*else[\\s]*{"}],returnBegin:!0,contains:[{begin:"}",className:e},{ 49 | variants:[{begin:"[\\s]*else\\sif[\\s]*\\([^{]+[\\s]*{"},{ 50 | begin:"[\\s]*else[\\s]*"}],subLanguage:"csharp"},{begin:"{",className:e}]},i,a] 51 | },h={begin:"@try[\\s]*{",end:"}",returnBegin:!0,returnEnd:!0, 52 | subLanguage:"csharp",contains:[{begin:"@",className:e},{begin:"try[\\s]*{", 53 | subLanguage:"csharp"},{variants:[{begin:"}[\\s]*catch[\\s]*\\([^\\)]+\\)[\\s]*{" 54 | },{begin:"}[\\s]*finally[\\s]*{"}],returnBegin:!0,contains:[{begin:"}", 55 | className:e},{variants:[{begin:"[\\s]*catch[\\s]*\\([^\\)]+\\)[\\s]*"},{ 56 | begin:"[\\s]*finally[\\s]*"}],subLanguage:"csharp"},{begin:"{",className:e}] 57 | },s,i,a]},p="@section[\\s]+[a-zA-Z0-9]+[\\s]*{",v=[u,o,d,N,{begin:p, 58 | returnBegin:!0,returnEnd:!0,end:"}",subLanguage:"cshtml-razor",contains:[{ 59 | begin:p,className:e},i,a]},{begin:"@await ",returnBegin:!0,subLanguage:"csharp", 60 | end:"(\\r|\\n|<|\\s)",contains:[{begin:"@await ",className:e},{ 61 | begin:"[<\\r\\n]",endsParent:!0}]},h,{variants:[{begin:"@@"},{begin:"[a-zA-Z]+@" 62 | }],skip:!0},t,r,c,{className:"meta",begin:"",relevance:10, 63 | contains:[{begin:"\\[",end:"\\]"}]},{begin:"<\\!\\[CDATA\\[",end:"\\]\\]>", 64 | relevance:10}].concat(b);return[d,N,h].forEach((n=>{ 65 | var e=v.filter((e=>e!==n)),a=n.contains.indexOf(s) 66 | ;n.contains.splice.apply(n.contains,[a,1].concat(e))})),{ 67 | aliases:["cshtml","razor","razor-cshtml","cshtml-razor"],contains:v}}})()); -------------------------------------------------------------------------------- /docs/assets/site.css: -------------------------------------------------------------------------------- 1 | /* Please see documentation at https://docs.microsoft.com/aspnet/core/client-side/bundling-and-minification 2 | for details on configuring this project to bundle and minify static web assets. */ 3 | 4 | a.navbar-brand { 5 | white-space: normal; 6 | text-align: center; 7 | word-break: break-all; 8 | } 9 | 10 | /* Provide sufficient contrast against white background */ 11 | a { 12 | color: #0366d6; 13 | } 14 | 15 | .btn-primary { 16 | color: #fff; 17 | background-color: #1b6ec2; 18 | border-color: #1861ac; 19 | } 20 | 21 | .nav-pills .nav-link.active, .nav-pills .show > .nav-link { 22 | color: #fff; 23 | background-color: #1b6ec2; 24 | border-color: #1861ac; 25 | } 26 | 27 | /* Sticky footer styles 28 | -------------------------------------------------- */ 29 | html { 30 | font-size: 14px; 31 | } 32 | @media (min-width: 768px) { 33 | html { 34 | font-size: 16px; 35 | } 36 | } 37 | 38 | .border-top { 39 | border-top: 1px solid #e5e5e5; 40 | } 41 | .border-bottom { 42 | border-bottom: 1px solid #e5e5e5; 43 | } 44 | 45 | .box-shadow { 46 | box-shadow: 0 .25rem .75rem rgba(0, 0, 0, .05); 47 | } 48 | 49 | button.accept-policy { 50 | font-size: 1rem; 51 | line-height: inherit; 52 | } 53 | 54 | /* Sticky footer styles 55 | -------------------------------------------------- */ 56 | html { 57 | position: relative; 58 | min-height: 100%; 59 | } 60 | 61 | body { 62 | /* Margin bottom by footer height */ 63 | margin-bottom: 60px; 64 | } 65 | .footer { 66 | position: absolute; 67 | bottom: 0; 68 | width: 100%; 69 | white-space: nowrap; 70 | line-height: 60px; /* Vertically center the text there */ 71 | } 72 | 73 | code{ 74 | background-color: whitesmoke !important; 75 | padding: 3px; 76 | } -------------------------------------------------------------------------------- /docs/cshtml-razor.min.js: -------------------------------------------------------------------------------- 1 | hljs.registerLanguage("cshtml-razor",(()=>{"use strict";return n=>{ 2 | var e="built_in",s={},a={begin:"}",className:e,endsParent:!0},i={begin:"{", 3 | end:"}",contains:[n.QUOTE_STRING_MODE,"self"]},r=n.COMMENT("@\\*","\\*@",{ 4 | relevance:10}),g={begin:"@[A-Za-z0-9\\._:-]+",returnBegin:!0, 5 | end:"(\\r|\\n|<|\\s|\"|')",subLanguage:"csharp",contains:[{begin:"@",className:e 6 | },{begin:"\\[",end:"\\]",skip:!0},{begin:"\\(",end:"\\)",skip:!0}],returnEnd:!0 7 | },t={begin:"[@]{0,1}",returnBegin:!0,end:"",returnEnd:!0, 8 | subLanguage:"cshtml-razor",contains:[{begin:"[@]{0,1}",className:e},{ 9 | begin:"",className:e,endsParent:!0}]},c={begin:"@\\(",end:"\\)", 10 | returnBegin:!0,returnEnd:!0,subLanguage:"csharp",contains:[{begin:"@\\(", 11 | className:e},{begin:"\\(",end:"\\)",subLanguage:"csharp", 12 | contains:[n.QUOTE_STRING_MODE,"self",t]},t,{begin:"\\)",className:e, 13 | endsParent:!0}]},b=((n,e)=>{var s={endsWithParent:!0,illegal:/`]+/}]}]}]} 17 | ;return[{className:"meta",begin:"",relevance:10,contains:[{ 18 | begin:"\\[",end:"\\]"}]},n.COMMENT("\x3c!--","--\x3e",{relevance:10}),{ 19 | begin:"<\\!\\[CDATA\\[",end:"\\]\\]>",relevance:10},{className:"meta", 20 | begin:/<\?xml/,end:/\?>/,relevance:10},{className:"tag", 21 | begin:"|$)",end:">",keywords:{name:"style"},contains:[s],starts:{ 22 | end:"",returnEnd:!0,subLanguage:["css","xml"]}},{className:"tag", 23 | begin:"|$)",end:">",keywords:{name:"script"},contains:[s], 24 | starts:{end:"<\/script>",returnEnd:!0, 25 | subLanguage:["actionscript","javascript","handlebars","xml"]}},{className:"tag", 26 | begin:"",contains:[{className:"name",begin:/[^\/><\s]+/,relevance:0 27 | },s]}].concat(e) 28 | })(n,[g,c]),l="^\\s*@(page|model|using|inherits|inject|layout)",u={ 29 | begin:l+"[^\\r\\n{\\(]*$",end:"$",returnBegin:!0,returnEnd:!0,contains:[{ 30 | begin:l,className:e},{variants:[{begin:"\\r|\\n",endsParent:!0},{ 31 | begin:"\\s[^\\r\\n]+",end:"$"},{begin:"$"}],className:"type",endsParent:!0}] 32 | },d={variants:[{begin:"@\\{",end:"}"},{begin:"@code\\s*\\{",end:"}"}], 33 | returnBegin:!0,returnEnd:!0,subLanguage:"csharp",contains:[{ 34 | begin:"@(code\\s*)?\\{",className:e},s,{begin:"{",end:"}",contains:["self"], 35 | skip:!0},a]},o={begin:"^\\s*@helper[\\s]*[^{]+[\\s]*{",returnBegin:!0, 36 | returnEnd:!0,end:"}",subLanguage:"cshtml-razor",contains:[{begin:"@helper", 37 | className:e},{begin:"{",className:e},a]},m=[{begin:"@for[\\s]*\\([^{]+[\\s]*{", 38 | end:"}"},{begin:"@if[\\s]*\\([^{]+[\\s]*{",end:"}"},{ 39 | begin:"@switch[\\s]*\\([^{]+[\\s]*{",end:"}"},{ 40 | begin:"@while[\\s]*\\([^{]+[\\s]*{",end:"}"},{ 41 | begin:"@using[\\s]*\\([^{]+[\\s]*{",end:"}"},{ 42 | begin:"@lock[\\s]*\\([^{]+[\\s]*{",end:"}"},{ 43 | begin:"@foreach[\\s]*\\([^{]+[\\s]*{",end:"}"}],N={variants:m,returnBegin:!0, 44 | returnEnd:!0,subLanguage:"csharp",contains:[{variants:m.map((n=>({begin:n.begin 45 | }))),returnBegin:!0,contains:[{begin:"@",className:e},{variants:m.map((n=>({ 46 | begin:n.begin.substr(1,n.begin.length-2)}))),subLanguage:"csharp"},{begin:"{", 47 | className:e}]},s,{variants:[{begin:"}[\\s]*else\\sif[\\s]*\\([^{]+[\\s]*{"},{ 48 | begin:"}[\\s]*else[\\s]*{"}],returnBegin:!0,contains:[{begin:"}",className:e},{ 49 | variants:[{begin:"[\\s]*else\\sif[\\s]*\\([^{]+[\\s]*{"},{ 50 | begin:"[\\s]*else[\\s]*"}],subLanguage:"csharp"},{begin:"{",className:e}]},i,a] 51 | },h={begin:"@try[\\s]*{",end:"}",returnBegin:!0,returnEnd:!0, 52 | subLanguage:"csharp",contains:[{begin:"@",className:e},{begin:"try[\\s]*{", 53 | subLanguage:"csharp"},{variants:[{begin:"}[\\s]*catch[\\s]*\\([^\\)]+\\)[\\s]*{" 54 | },{begin:"}[\\s]*finally[\\s]*{"}],returnBegin:!0,contains:[{begin:"}", 55 | className:e},{variants:[{begin:"[\\s]*catch[\\s]*\\([^\\)]+\\)[\\s]*"},{ 56 | begin:"[\\s]*finally[\\s]*"}],subLanguage:"csharp"},{begin:"{",className:e}] 57 | },s,i,a]},p="@section[\\s]+[a-zA-Z0-9]+[\\s]*{",v=[u,o,d,N,{begin:p, 58 | returnBegin:!0,returnEnd:!0,end:"}",subLanguage:"cshtml-razor",contains:[{ 59 | begin:p,className:e},i,a]},{begin:"@await ",returnBegin:!0,subLanguage:"csharp", 60 | end:"(\\r|\\n|<|\\s)",contains:[{begin:"@await ",className:e},{ 61 | begin:"[<\\r\\n]",endsParent:!0}]},h,{variants:[{begin:"@@"},{begin:"[a-zA-Z]+@" 62 | }],skip:!0},t,r,c,{className:"meta",begin:"",relevance:10, 63 | contains:[{begin:"\\[",end:"\\]"}]},{begin:"<\\!\\[CDATA\\[",end:"\\]\\]>", 64 | relevance:10}].concat(b);return[d,N,h].forEach((n=>{ 65 | var e=v.filter((e=>e!==n)),a=n.contains.indexOf(s) 66 | ;n.contains.splice.apply(n.contains,[a,1].concat(e))})),{ 67 | aliases:["cshtml","razor","razor-cshtml","cshtml-razor"],contains:v}}})()); -------------------------------------------------------------------------------- /samples/IterationBenchmark/IterationBenchmark.cs: -------------------------------------------------------------------------------- 1 | using BenchmarkDotNet.Attributes; 2 | 3 | // Using Exporter Attributes 4 | //[BenchmarkDotNetVisualizer.RichImageExporter(title: "Performance Comparison between for, foreach, and ForEeach() method", groupByColumns: ["Runtime"], spectrumColumns: ["Mean", "Allocated"], theme: Theme.Dark)] 5 | //[BenchmarkDotNetVisualizer.RichHtmlExporter(title: "Performance Comparison between for, foreach, and ForEeach() method", groupByColumns: ["Runtime"], spectrumColumns: ["Mean", "Allocated"], theme: Theme.Dark)] 6 | //[BenchmarkDotNetVisualizer.RichMarkdownExporter(title: "Performance Comparison between for, foreach, and ForEeach() method", groupByColumns: ["Runtime"], sortByColumns: ["Mean", "Allocated"])] 7 | 8 | #if RELEASE 9 | [ShortRunJob(BenchmarkDotNet.Jobs.RuntimeMoniker.NetCoreApp30)] 10 | [ShortRunJob(BenchmarkDotNet.Jobs.RuntimeMoniker.NetCoreApp31)] 11 | [ShortRunJob(BenchmarkDotNet.Jobs.RuntimeMoniker.Net50)] 12 | [ShortRunJob(BenchmarkDotNet.Jobs.RuntimeMoniker.Net60)] 13 | [ShortRunJob(BenchmarkDotNet.Jobs.RuntimeMoniker.Net70)] 14 | [ShortRunJob(BenchmarkDotNet.Jobs.RuntimeMoniker.Net80)] 15 | #endif 16 | [CategoriesColumn] 17 | [HideColumns("Job")] 18 | [MemoryDiagnoser(displayGenColumns: false)] 19 | public class IterationBenchmark 20 | { 21 | [Params(1000)] 22 | public int Length { get; set; } 23 | 24 | private int[] _array = null!; 25 | private List _list = null!; 26 | 27 | [GlobalSetup] 28 | public void Setup() 29 | { 30 | _array = Enumerable.Range(0, Length).ToArray(); 31 | _list = Enumerable.Range(0, Length).ToList(); 32 | } 33 | 34 | [Benchmark(Description = "for"), BenchmarkCategory("Array")] 35 | public void ArrayFor() 36 | { 37 | var length = _array.Length; 38 | for (int i = 0; i < length; i++) 39 | { 40 | _ = _array[i]; 41 | } 42 | } 43 | 44 | [Benchmark(Description = "foreach"), BenchmarkCategory("Array")] 45 | public void ArrayForEach() 46 | { 47 | foreach (var _ in _array) 48 | { 49 | } 50 | } 51 | 52 | [Benchmark(Description = "ForEach()"), BenchmarkCategory("Array")] 53 | public void ArrayForEachMethod() 54 | { 55 | Array.ForEach(_array, _ => { }); 56 | } 57 | 58 | [Benchmark(Description = "for"), BenchmarkCategory("List")] 59 | public void ListFor() 60 | { 61 | var length = _list.Count; 62 | for (int i = 0; i < length; i++) 63 | { 64 | _ = _list[i]; 65 | } 66 | } 67 | 68 | [Benchmark(Description = "foreach"), BenchmarkCategory("List")] 69 | public void ListForEach() 70 | { 71 | foreach (var _ in _list) 72 | { 73 | } 74 | } 75 | 76 | [Benchmark(Description = "ForEach()"), BenchmarkCategory("List")] 77 | public void ListForEachMethod() 78 | { 79 | _list.ForEach(_ => { }); 80 | } 81 | } -------------------------------------------------------------------------------- /samples/IterationBenchmark/IterationBenchmark.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | net9.0;net8.0;net7.0;net6.0;net5.0;netcoreapp3.1;netcoreapp3.0; 6 | enable 7 | enable 8 | latest 9 | false 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /samples/IterationBenchmark/Program.cs: -------------------------------------------------------------------------------- 1 | using BenchmarkDotNetVisualizer; 2 | 3 | if (BenchmarkAutoRunner.IsRunningInDebugMode()) 4 | { 5 | Console.WriteLine("The IteratorsBenchmark needs to be run in RELEASE mode."); 6 | return; 7 | } 8 | 9 | var summary = BenchmarkAutoRunner.Run(); 10 | 11 | var options1 = new JoinReportHtmlOptions 12 | { 13 | Title = "Performance Comparison between for, foreach, and ForEach() method", 14 | MainColumn = "Method", 15 | GroupByColumns = ["Categories", "Length"], // Groups by column 'Categories' and 'Length' 16 | PivotColumn = "Runtime", // Pivot 'Runtime' column per value of 'Mean' 17 | StatisticColumns = ["Mean"], // Colorizes 'Mean' columns as Spectrum 18 | ColumnsOrder = [".NET Core 3.0", ".NET Core 3.1", ".NET 5.0", ".NET 6.0", ".NET 7.0", ".NET 8.0"], //Order of columns 19 | DividerMode = RenderTableDividerMode.SeparateTables, // Separates tables by Grouping by 'GroupByColumns' 20 | HtmlWrapMode = HtmlDocumentWrapMode.RichDataTables, // Uses feature-rich https://datatables.net plugin 21 | Theme = Theme.Dark // Optional (Default is Dark) 22 | }; 23 | 24 | await summary.JoinReportsAndSaveAsHtmlAndImageAsync( 25 | htmlPath: DirectoryHelper.GetPathRelativeToProjectDirectory("Reports\\JoinedBenchmark-PivotBy-Runtime-Dark.html"), 26 | imagePath: DirectoryHelper.GetPathRelativeToProjectDirectory("Reports\\JoinedBenchmark-PivotBy-Runtime-Dark.png"), 27 | options: options1); 28 | 29 | options1.Theme = Theme.Light; 30 | await summary.JoinReportsAndSaveAsImageAsync( 31 | path: DirectoryHelper.GetPathRelativeToProjectDirectory("Reports\\JoinedBenchmark-PivotBy-Runtime-Light.png"), 32 | options: options1); 33 | 34 | var options2 = new JoinReportHtmlOptions 35 | { 36 | Title = "Performance Comparison between for, foreach, and ForEach() method", 37 | MainColumn = "Runtime", 38 | GroupByColumns = ["Categories", "Length"], // Groups by column 'Categories' and 'Length' 39 | PivotColumn = "Method", // Pivot 'Method' column per value of 'Mean' 40 | StatisticColumns = ["Mean"], // Colorizes 'Mean' columns as Spectrum 41 | ColumnsOrder = ["for", "foreach", "ForEach()"], // Order of columns 42 | DividerMode = RenderTableDividerMode.SeparateTables, // Separates tables by Grouping by 'GroupByColumns' 43 | HtmlWrapMode = HtmlDocumentWrapMode.RichDataTables, // Uses feature-rich https://datatables.net plugin 44 | Theme = Theme.Dark // Optional (Default is Dark) 45 | }; 46 | await summary.JoinReportsAndSaveAsHtmlAndImageAsync( 47 | htmlPath: DirectoryHelper.GetPathRelativeToProjectDirectory("Reports\\JoinedBenchmark-PivotBy-Method-Dark.html"), 48 | imagePath: DirectoryHelper.GetPathRelativeToProjectDirectory("Reports\\JoinedBenchmark-PivotBy-Method-Dark.png"), 49 | options: options2); 50 | 51 | options2.Theme = Theme.Light; 52 | await summary.JoinReportsAndSaveAsImageAsync( 53 | path: DirectoryHelper.GetPathRelativeToProjectDirectory("Reports\\JoinedBenchmark-PivotBy-Method-Light.png"), 54 | options: options2); -------------------------------------------------------------------------------- /samples/IterationBenchmark/Reports/JoinedBenchmark-PivotBy-Method-Dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mjebrahimi/BenchmarkDotNetVisualizer/67fea4b47eebb29b9abd0657f31cb2b3671e5900/samples/IterationBenchmark/Reports/JoinedBenchmark-PivotBy-Method-Dark.png -------------------------------------------------------------------------------- /samples/IterationBenchmark/Reports/JoinedBenchmark-PivotBy-Method-Light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mjebrahimi/BenchmarkDotNetVisualizer/67fea4b47eebb29b9abd0657f31cb2b3671e5900/samples/IterationBenchmark/Reports/JoinedBenchmark-PivotBy-Method-Light.png -------------------------------------------------------------------------------- /samples/IterationBenchmark/Reports/JoinedBenchmark-PivotBy-Runtime-Dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mjebrahimi/BenchmarkDotNetVisualizer/67fea4b47eebb29b9abd0657f31cb2b3671e5900/samples/IterationBenchmark/Reports/JoinedBenchmark-PivotBy-Runtime-Dark.png -------------------------------------------------------------------------------- /samples/IterationBenchmark/Reports/JoinedBenchmark-PivotBy-Runtime-Light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mjebrahimi/BenchmarkDotNetVisualizer/67fea4b47eebb29b9abd0657f31cb2b3671e5900/samples/IterationBenchmark/Reports/JoinedBenchmark-PivotBy-Runtime-Light.png -------------------------------------------------------------------------------- /samples/JsonSerializersBenchmark/JsonSerializersBenchmark.cs: -------------------------------------------------------------------------------- 1 | using BenchmarkDotNet.Attributes; 2 | using BenchmarkDotNet.Columns; 3 | using System.Text.Json; 4 | using System.Text.Json.Serialization; 5 | 6 | // Using Exporter Attributes 7 | //[BenchmarkDotNetVisualizer.RichImageExporter(title: "Json Serializers Benchmark", groupByColumns: ["Method"], spectrumColumns: ["Mean", "Allocated"], theme: Theme.Dark)] 8 | //[BenchmarkDotNetVisualizer.RichHtmlExporter(title: "Json Serializers Benchmark", groupByColumns: ["Method"], spectrumColumns: ["Mean", "Allocated"], theme: Theme.Dark)] 9 | //[BenchmarkDotNetVisualizer.RichMarkdownExporter(title: "Json Serializers Benchmark", groupByColumns: ["Method"], sortByColumns: ["Mean", "Allocated"])] 10 | 11 | #if RELEASE 12 | [ShortRunJob] 13 | #endif 14 | [CategoriesColumn] 15 | [MemoryDiagnoser(displayGenColumns: false)] 16 | //[GroupBenchmarksBy(BenchmarkDotNet.Configs.BenchmarkLogicalGroupRule.ByMethod)] 17 | //[Orderer(BenchmarkDotNet.Order.SummaryOrderPolicy.FastestToSlowest)] 18 | public class JsonSerializersBenchmark 19 | { 20 | private static readonly Person Person = GeneratePerson(); 21 | private static readonly string Json = GenerateJson(); 22 | 23 | #pragma warning disable CA1822 // Mark members as static 24 | #region Serialize 25 | [Benchmark(Description = "Serialize"), BenchmarkCategory("NewtonsoftJson")] 26 | public string Serialize1() 27 | { 28 | return Newtonsoft.Json.JsonConvert.SerializeObject(Person); 29 | } 30 | 31 | [Benchmark(Description = "Serialize"), BenchmarkCategory("SystemTextJson")] 32 | public string Serialize2() 33 | { 34 | return JsonSerializer.Serialize(Person); 35 | } 36 | 37 | [Benchmark(Description = "Serialize"), BenchmarkCategory("SystemTextSourceGen")] 38 | public string Serialize3() 39 | { 40 | return JsonSerializer.Serialize(Person, AppJsonSerializerContext.Default.Person); 41 | } 42 | #endregion 43 | 44 | #region Deserialize 45 | [Benchmark(Description = "Deserialize"), BenchmarkCategory("NewtonsoftJson")] 46 | public Person? Deserialize1() 47 | { 48 | return Newtonsoft.Json.JsonConvert.DeserializeObject(Json); 49 | } 50 | 51 | [Benchmark(Description = "Deserialize"), BenchmarkCategory("SystemTextJson")] 52 | public Person? Deserialize2() 53 | { 54 | return JsonSerializer.Deserialize(Json); 55 | } 56 | 57 | [Benchmark(Description = "Deserialize"), BenchmarkCategory("SystemTextSourceGen")] 58 | public Person? Deserialize3() 59 | { 60 | return JsonSerializer.Deserialize(Json, AppJsonSerializerContext.Default.Person); 61 | } 62 | #endregion 63 | #pragma warning restore CA1822 // Mark members as static 64 | 65 | #region private methods 66 | private static Person GeneratePerson() 67 | { 68 | return new 69 | ( 70 | Name: "John", 71 | Age: 30, 72 | Gender: "Male", 73 | #pragma warning disable S6562 // Always set the "DateTimeKind" when creating new "DateTime" instances 74 | DateOfBirth: new DateTime(1994, 1, 1), 75 | #pragma warning restore S6562 // Always set the "DateTimeKind" when creating new "DateTime" instances 76 | PlaceOfBirth: "City, Country", 77 | ContactInfo: new 78 | ( 79 | PhoneNumber: "1234567890", 80 | Email: "example@example.com" 81 | ), 82 | PhysicalDescription: new 83 | ( 84 | Height: 170, 85 | Weight: 70, 86 | HairColor: "Black", 87 | EyeColor: "Brown" 88 | ), 89 | Family: new 90 | ( 91 | Parents: ["Parent 1", "Parent 2"], 92 | Siblings: ["Sibling 1", "Sibling 2"] 93 | ) 94 | ); 95 | } 96 | 97 | private static string GenerateJson() 98 | { 99 | var person = GeneratePerson(); 100 | return Newtonsoft.Json.JsonConvert.SerializeObject(person); 101 | } 102 | #endregion 103 | } 104 | 105 | public record Person(string Name, int Age, string Gender, DateTime DateOfBirth, string PlaceOfBirth, ContactInformation ContactInfo, PhysicalDescription PhysicalDescription, FamilyInfo Family); 106 | public record ContactInformation(string PhoneNumber, string Email); 107 | public record PhysicalDescription(double Height, double Weight, string HairColor, string EyeColor); 108 | public record FamilyInfo(string[] Parents, string[] Siblings); 109 | 110 | [JsonSerializable(typeof(Person))] 111 | public partial class AppJsonSerializerContext : JsonSerializerContext; -------------------------------------------------------------------------------- /samples/JsonSerializersBenchmark/JsonSerializersBenchmark.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | net9.0 6 | enable 7 | enable 8 | latest 9 | false 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /samples/JsonSerializersBenchmark/Program.cs: -------------------------------------------------------------------------------- 1 | using BenchmarkDotNetVisualizer; 2 | 3 | var summary = BenchmarkAutoRunner.Run(); 4 | 5 | var options = new ReportHtmlOptions 6 | { 7 | Title = "Json Serializers Benchmark", 8 | GroupByColumns = ["Method"], // Groups by 'Method' column and highlights groups 9 | SpectrumColumns = ["Mean", "Allocated"], // Colorizes 'Mean' and 'Allocated' columns as Spectrum 10 | DividerMode = RenderTableDividerMode.EmptyDividerRow, // Separates tables by Empty Divider Row 11 | HtmlWrapMode = HtmlDocumentWrapMode.Simple, // Uses simple HTML table 12 | Theme = Theme.Dark // Optional (Default is Dark) 13 | }; 14 | 15 | await summary.SaveAsHtmlAndImageAsync( 16 | htmlPath: DirectoryHelper.GetPathRelativeToProjectDirectory("Reports\\Benchmark-Dark.html"), 17 | imagePath: DirectoryHelper.GetPathRelativeToProjectDirectory("Reports\\Benchmark-Dark.png"), 18 | options: options); 19 | 20 | options.Theme = Theme.Light; 21 | await summary.SaveAsImageAsync( 22 | path: DirectoryHelper.GetPathRelativeToProjectDirectory("Reports\\Benchmark-Light.png"), 23 | options: options); -------------------------------------------------------------------------------- /samples/JsonSerializersBenchmark/Reports/Benchmark-Dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mjebrahimi/BenchmarkDotNetVisualizer/67fea4b47eebb29b9abd0657f31cb2b3671e5900/samples/JsonSerializersBenchmark/Reports/Benchmark-Dark.png -------------------------------------------------------------------------------- /samples/JsonSerializersBenchmark/Reports/Benchmark-Light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mjebrahimi/BenchmarkDotNetVisualizer/67fea4b47eebb29b9abd0657f31cb2b3671e5900/samples/JsonSerializersBenchmark/Reports/Benchmark-Light.png -------------------------------------------------------------------------------- /src/BenchmarkDotNet/Attributes/RichHtmlExporterAttribute.cs: -------------------------------------------------------------------------------- 1 | using BenchmarkDotNet.Attributes; 2 | 3 | namespace BenchmarkDotNetVisualizer; 4 | 5 | /// 6 | /// Initializes a new instance of the class. 7 | /// 8 | /// The title. 9 | /// The group by columns. 10 | /// The sort by columns. 11 | /// The spectrum columns. 12 | /// if set to true highlights groups. 13 | /// The divider mode. (Defaults to ) 14 | /// The HTML wrap mode. (Defaults to ) 15 | /// The theme. (Defaults to ) 16 | /// 17 | public class RichHtmlExporterAttribute( 18 | string title, 19 | string[]? groupByColumns = null, 20 | string[]? sortByColumns = null, 21 | string[]? spectrumColumns = null, 22 | bool highlightGroups = true, 23 | RenderTableDividerMode dividerMode = RenderTableDividerMode.EmptyDividerRow, 24 | HtmlDocumentWrapMode htmlWrapMode = HtmlDocumentWrapMode.Simple, 25 | Theme theme = Theme.Dark) 26 | : ExporterConfigBaseAttribute(new RichHtmlExporter(new() 27 | { 28 | Title = title, 29 | GroupByColumns = groupByColumns, 30 | SortByColumns = sortByColumns, 31 | SpectrumColumns = spectrumColumns, 32 | HighlightGroups = highlightGroups, 33 | DividerMode = dividerMode, 34 | HtmlWrapMode = htmlWrapMode, 35 | Theme = theme, 36 | })) 37 | { 38 | } -------------------------------------------------------------------------------- /src/BenchmarkDotNet/Attributes/RichImageExporterAttribute.cs: -------------------------------------------------------------------------------- 1 | using BenchmarkDotNet.Attributes; 2 | 3 | namespace BenchmarkDotNetVisualizer; 4 | 5 | /// 6 | /// Initializes a new instance of the class. 7 | /// 8 | /// The title. 9 | /// The group by columns. 10 | /// The sort by columns. 11 | /// The spectrum columns. 12 | /// if set to true highlights groups. 13 | /// The divider mode. (Defaults to ) 14 | /// The image format. (Defaults to ) 15 | /// The theme. (Defaults to ) 16 | /// 17 | public class RichImageExporterAttribute( 18 | string title, 19 | string[]? groupByColumns = null, 20 | string[]? sortByColumns = null, 21 | string[]? spectrumColumns = null, 22 | bool highlightGroups = true, 23 | RenderTableDividerMode dividerMode = RenderTableDividerMode.EmptyDividerRow, 24 | ImageFormat format = ImageFormat.Png, 25 | Theme theme = Theme.Dark) 26 | : ExporterConfigBaseAttribute(new RichImageExporter(new() 27 | { 28 | Title = title, 29 | GroupByColumns = groupByColumns, 30 | SortByColumns = sortByColumns, 31 | SpectrumColumns = spectrumColumns, 32 | HighlightGroups = highlightGroups, 33 | DividerMode = dividerMode, 34 | Theme = theme, 35 | }, format)) 36 | { 37 | } -------------------------------------------------------------------------------- /src/BenchmarkDotNet/Attributes/RichMarkdownExporterAttribute.cs: -------------------------------------------------------------------------------- 1 | using BenchmarkDotNet.Attributes; 2 | 3 | namespace BenchmarkDotNetVisualizer; 4 | 5 | /// 6 | /// Initializes a new instance of the class. 7 | /// 8 | /// The title. 9 | /// The group by columns. 10 | /// The sort by columns. 11 | /// The divider mode. (Defaults to ) 12 | /// 13 | public class RichMarkdownExporterAttribute( 14 | string title, 15 | string[]? groupByColumns = null, 16 | string[]? sortByColumns = null, 17 | RenderTableDividerMode dividerMode = RenderTableDividerMode.EmptyDividerRow) 18 | : ExporterConfigBaseAttribute(new RichMarkdownExporter(new() 19 | { 20 | Title = title, 21 | GroupByColumns = groupByColumns, 22 | SortByColumns = sortByColumns, 23 | DividerMode = dividerMode, 24 | })) 25 | { 26 | } -------------------------------------------------------------------------------- /src/BenchmarkDotNet/CustomTagColumn.cs: -------------------------------------------------------------------------------- 1 | using BenchmarkDotNet.Columns; 2 | using BenchmarkDotNet.Reports; 3 | using BenchmarkDotNet.Running; 4 | 5 | namespace BenchmarkDotNetVisualizer; 6 | 7 | /// 8 | /// CustomTagColumn 9 | /// 10 | /// 11 | public class CustomTagColumn : IColumn 12 | { 13 | private readonly Func? getValue1; 14 | private readonly Func? getValue2; 15 | 16 | /// 17 | public string Id { get; } 18 | 19 | /// 20 | public string ColumnName { get; } 21 | 22 | /// 23 | /// Initializes a new instance of the class. 24 | /// 25 | /// Name of the column. 26 | /// The get value function. 27 | public CustomTagColumn(string columnName, Func getValue) 28 | { 29 | getValue1 = getValue; 30 | ColumnName = columnName; 31 | Id = nameof(CustomTagColumn) + "." + ColumnName; 32 | } 33 | 34 | /// 35 | /// Initializes a new instance of the class. 36 | /// 37 | /// Name of the column. 38 | /// The get value function. 39 | public CustomTagColumn(string columnName, Func getValue) 40 | { 41 | getValue2 = getValue; 42 | ColumnName = columnName; 43 | Id = nameof(CustomTagColumn) + "." + ColumnName; 44 | } 45 | 46 | /// 47 | public bool IsDefault(Summary summary, BenchmarkCase benchmarkCase) => false; 48 | 49 | /// 50 | public bool IsAvailable(Summary summary) => true; 51 | 52 | /// 53 | public bool AlwaysShow => true; 54 | 55 | /// 56 | public ColumnCategory Category => ColumnCategory.Custom; 57 | 58 | /// 59 | public int PriorityInCategory => 0; 60 | 61 | /// 62 | public bool IsNumeric => false; 63 | 64 | /// 65 | public UnitType UnitType => UnitType.Dimensionless; 66 | 67 | /// 68 | public string Legend => $"Custom '{ColumnName}' tag column"; 69 | 70 | /// 71 | public string GetValue(Summary summary, BenchmarkCase benchmarkCase) => getValue1!.Invoke(summary, benchmarkCase); 72 | 73 | /// 74 | public string GetValue(Summary summary, BenchmarkCase benchmarkCase, SummaryStyle style) => getValue2?.Invoke(summary, benchmarkCase, style) ?? GetValue(summary, benchmarkCase); 75 | 76 | /// 77 | public override string ToString() => ColumnName; 78 | } -------------------------------------------------------------------------------- /src/BenchmarkDotNet/DebugInProcessConfigDry.cs: -------------------------------------------------------------------------------- 1 | using BenchmarkDotNet.Configs; 2 | using BenchmarkDotNet.Jobs; 3 | using BenchmarkDotNet.Toolchains.InProcess.Emit; 4 | 5 | namespace BenchmarkDotNetVisualizer; 6 | 7 | /// 8 | /// Similar to DebugInProcessConfig but uses Job.Dry instead of Job.Default. 9 | /// 10 | public class DebugInProcessConfigDry : DebugConfig 11 | { 12 | /// 13 | /// Gets the Job.Dry instead of Job.Default. 14 | /// 15 | /// 16 | public override IEnumerable GetJobs() => [ 17 | Job.Dry // Job.Dry instead of Job.Default 18 | .WithToolchain(InProcessEmitToolchain.Instance) //InProcessNoEmitToolchain (A toolchain to run the benchmarks in-process (no emit)) 19 | ]; 20 | } -------------------------------------------------------------------------------- /src/BenchmarkDotNet/Exporters/RichHtmlExporter.cs: -------------------------------------------------------------------------------- 1 | using BenchmarkDotNet.Exporters; 2 | using BenchmarkDotNet.Loggers; 3 | using BenchmarkDotNet.Reports; 4 | 5 | namespace BenchmarkDotNetVisualizer; 6 | 7 | /// 8 | /// Initializes a new instance of the class. 9 | /// 10 | /// The options. 11 | /// 12 | public class RichHtmlExporter(ReportHtmlOptions options) : ExporterBase 13 | { 14 | /// 15 | protected override string FileExtension => "html"; 16 | 17 | /// 18 | protected override string FileNameSuffix => "-rich"; 19 | 20 | /// 21 | public override void ExportToLog(Summary summary, ILogger logger) 22 | { 23 | if (summary.Table.FullContent.Length == 0) 24 | { 25 | logger.WriteLineError("
There are no benchmarks found
"); 26 | return; 27 | } 28 | var html = summary.GetHtml(options); 29 | logger.Write(html); 30 | } 31 | } -------------------------------------------------------------------------------- /src/BenchmarkDotNet/Exporters/RichImageExporter.cs: -------------------------------------------------------------------------------- 1 | using BenchmarkDotNet.Exporters; 2 | using BenchmarkDotNet.Helpers; 3 | using BenchmarkDotNet.Loggers; 4 | using BenchmarkDotNet.Reports; 5 | using BenchmarkDotNet.Running; 6 | 7 | namespace BenchmarkDotNetVisualizer; 8 | 9 | /// 10 | /// Initializes a new instance of the class. 11 | /// 12 | /// The options. 13 | /// The image format. (Defaults to ) 14 | /// 15 | public class RichImageExporter(ReportImageOptions options, ImageFormat format = ImageFormat.Png) : IExporter 16 | { 17 | /// 18 | public string Name => GetType().Name + FileNameSuffix; 19 | 20 | /// 21 | public string FileExtension => format.GetExtension(); 22 | 23 | /// 24 | public string FileCaption => "report"; 25 | 26 | /// 27 | public string FileNameSuffix => "-rich"; 28 | 29 | /// 30 | public IEnumerable ExportToFiles(Summary summary, ILogger consoleLogger) 31 | { 32 | string fileName = GetFileName(summary); 33 | string text = GetArtifactFullName(summary); 34 | if (File.Exists(text)) 35 | { 36 | try 37 | { 38 | File.Delete(text); 39 | } 40 | catch (IOException) 41 | { 42 | string text2 = DateTime.Now.ToString("yyyyMMdd-HHmmss"); 43 | string text3 = Path.Combine(summary.ResultsDirectoryPath, fileName) + "-" + FileCaption + FileNameSuffix + "-" + text2 + "." + FileExtension; 44 | consoleLogger.WriteLineError("Could not overwrite file " + text + ". Exporting to " + text3); 45 | text = text3; 46 | } 47 | } 48 | 49 | ExportToLog(summary, null!); 50 | 51 | return [text]; 52 | } 53 | 54 | /// 55 | public void ExportToLog(Summary summary, ILogger logger) 56 | { 57 | var path = GetArtifactFullName(summary); 58 | summary.SaveAsImageAsync(path, options).GetAwaiter().GetResult(); //.RunSynchronously(); 59 | } 60 | 61 | /// 62 | /// Gets the full name of the artifact. 63 | /// 64 | /// The summary. 65 | /// 66 | internal string GetArtifactFullName(Summary summary) 67 | { 68 | string fileName = GetFileName(summary); 69 | return $"{Path.Combine(summary.ResultsDirectoryPath, fileName)}-{FileCaption}{FileNameSuffix}.{FileExtension}"; 70 | } 71 | 72 | /// 73 | /// Gets the name of the file. 74 | /// 75 | /// The summary. 76 | /// 77 | private static string GetFileName(Summary summary) 78 | { 79 | Type[] array = summary.BenchmarksCases.Select((BenchmarkCase b) => b.Descriptor.Type).Distinct().ToArray(); 80 | if (array.Length == 1) 81 | { 82 | return FolderNameHelper.ToFolderName(array.Single()); 83 | } 84 | 85 | return summary.Title; 86 | } 87 | } -------------------------------------------------------------------------------- /src/BenchmarkDotNet/Exporters/RichMarkdownExporter.cs: -------------------------------------------------------------------------------- 1 | using BenchmarkDotNet.Exporters; 2 | using BenchmarkDotNet.Loggers; 3 | using BenchmarkDotNet.Reports; 4 | 5 | namespace BenchmarkDotNetVisualizer; 6 | 7 | /// 8 | /// Initializes a new instance of the class. 9 | /// 10 | /// The options. 11 | /// 12 | public class RichMarkdownExporter(ReportMarkdownOptions options) : ExporterBase 13 | { 14 | /// 15 | protected override string FileExtension => "md"; 16 | 17 | /// 18 | protected override string FileNameSuffix => "-rich"; 19 | 20 | /// 21 | public override void ExportToLog(Summary summary, ILogger logger) 22 | { 23 | if (summary.Table.FullContent.Length == 0) 24 | { 25 | logger.WriteLineError("There are no benchmarks found "); 26 | logger.WriteLine(); 27 | return; 28 | } 29 | var markdown = summary.GetMarkdown(options); 30 | logger.Write(markdown); 31 | } 32 | } -------------------------------------------------------------------------------- /src/BenchmarkDotNetVisualizer.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.1;net6.0;net7.0;net8.0;net9.0 5 | enable 6 | enable 7 | latest 8 | BenchmarkDotNetVisualizer 9 | BenchmarkDotNetVisualizer 10 | BenchmarkDotNetVisualizer 11 | BenchmarkDotNetVisualizer 12 | 2.1.0 13 | 2.1.0 14 | 2.1.0 15 | Mohammad Javad Ebrahimi 16 | Mohammad Javad Ebrahimi 17 | Copyright © Mohammad Javad Ebrahimi 2025 18 | 19 | Visualizes your BenchmarkDotNet benchmarks to colorful images, feature-rich HTML, and customizable markdown files (and maybe powerful charts in the future!) 20 | 21 | README.md 22 | BenchmarkDotNet Benchmark Benchmarking Exporter Visualizer Visualization 23 | MIT 24 | https://github.com/mjebrahimi/BenchmarkDotNetVisualizer 25 | https://github.com/mjebrahimi/BenchmarkDotNetVisualizer 26 | git 27 | false 28 | true 29 | false 30 | package-icon.png 31 | true 32 | 33 | true 34 | 35 | embedded 36 | 37 | 38 | 39 | true 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /src/Core/BenchmarkInfo.cs: -------------------------------------------------------------------------------- 1 | using BenchmarkDotNet.Reports; 2 | using BenchmarkDotNetVisualizer.Utilities; 3 | using System.Dynamic; 4 | 5 | namespace BenchmarkDotNetVisualizer; 6 | 7 | /// 8 | /// Initializes a new instance of the class with holds information about a benchmark. 9 | /// 10 | /// The summary. 11 | /// Type of the benchmark. 12 | /// The display name. 13 | /// Name of the group. 14 | /// The environment information. 15 | /// The table. 16 | public class BenchmarkInfo(Summary? summary, Type? benchmarkType, string displayName, string groupName, string environmentInfo, IEnumerable table) 17 | { 18 | /// 19 | /// Gets or sets the summary. 20 | /// 21 | /// 22 | /// The summary. 23 | /// 24 | public Summary? Summary { get; set; } = summary; 25 | 26 | /// 27 | /// Gets or sets the benchmark type. 28 | /// 29 | /// 30 | /// The type of the benchmark. 31 | /// 32 | public Type? BenchmarkType { get; set; } = benchmarkType; 33 | 34 | /// 35 | /// Gets or sets the display name. 36 | /// 37 | /// 38 | /// The display name. 39 | /// 40 | public string DisplayName { get; set; } = displayName; 41 | 42 | /// 43 | /// Gets or sets the group name. 44 | /// 45 | /// 46 | /// The name of the group. 47 | /// 48 | public string GroupName { get; set; } = groupName; 49 | 50 | /// 51 | /// Gets or sets the environment information. 52 | /// 53 | /// 54 | /// The environment information. 55 | /// 56 | public string EnvironmentInfo { get; set; } = environmentInfo; 57 | 58 | /// 59 | /// Gets or sets the table. 60 | /// 61 | /// 62 | /// The table. 63 | /// 64 | public IEnumerable Table { get; set; } = table; 65 | 66 | /// 67 | /// Creates benchmark information from directory. 68 | /// 69 | /// The directory. 70 | /// Name of the benchmarks group. 71 | /// The search pattern. 72 | /// 73 | public static IEnumerable CreateFromDirectory(string directory, string? benchmarksGroupName = null, string searchPattern = "*.md") 74 | { 75 | ArgumentException.ThrowIfNullOrWhiteSpace(directory, nameof(directory)); 76 | ArgumentNullException.ThrowIfNull(searchPattern, nameof(searchPattern)); 77 | 78 | var fileNames = Directory.GetFiles(directory, searchPattern); 79 | return CreateFromFile(fileNames, benchmarksGroupName); 80 | } 81 | 82 | /// 83 | /// Creates benchmark information from files. 84 | /// 85 | /// The file names. 86 | /// Name of the benchmarks group. 87 | /// 88 | public static IEnumerable CreateFromFile(IEnumerable fileNames, string? benchmarksGroupName = null) 89 | { 90 | Guard.ThrowIfNullOrEmpty(fileNames, nameof(fileNames)); 91 | 92 | if (benchmarksGroupName is not null) 93 | return fileNames.Select(path => CreateFromFile(path, benchmarksGroupName)); 94 | 95 | return fileNames.FindCommonPartOfEachFileNames().Select(p => CreateFromFile(p.Path, p.CommonPart.ExtractBenchmarkClassName())); 96 | } 97 | 98 | /// 99 | /// Creates benchmark information from file. 100 | /// 101 | /// The file path. 102 | /// Name of the benchmark group. 103 | /// 104 | public static BenchmarkInfo CreateFromFile(string filePath, string? benchmarkGroupName = null) 105 | { 106 | ArgumentException.ThrowIfNullOrWhiteSpace(filePath, nameof(filePath)); 107 | 108 | var benchmarkClassName = filePath.ExtractBenchmarkClassName(); 109 | benchmarkGroupName ??= benchmarkClassName; 110 | 111 | var markdown = File.ReadAllText(filePath); 112 | var environmentInfo = BenchmarkVisualizer.ExtractEnvironmentInfo(markdown); 113 | var markdownTable = BenchmarkVisualizer.ExtractMarkdownTable(markdown); 114 | var table = MarkdownHelper.ParseMarkdownTable(markdownTable); 115 | 116 | return new BenchmarkInfo(null, null, benchmarkClassName, benchmarkGroupName, environmentInfo, table); 117 | } 118 | 119 | /// 120 | /// Creates benchmark information from summary collection. 121 | /// 122 | /// The summaries. 123 | /// 124 | public static IEnumerable CreateFromSummary(IEnumerable summaries) 125 | { 126 | Guard.ThrowIfNullOrEmpty(summaries, nameof(summaries)); 127 | 128 | return summaries.Select(summary => summary.GetBenchmarkInfo()); 129 | } 130 | 131 | /// 132 | /// Creates benchmark information from summary. 133 | /// 134 | /// The summary. 135 | /// 136 | public static BenchmarkInfo CreateFromSummary(Summary summary) 137 | { 138 | ArgumentNullException.ThrowIfNull(summary, nameof(summary)); 139 | 140 | var benchmarkClassType = summary.BenchmarksCases[0].Descriptor.WorkloadMethod.ReflectedType!; 141 | var benchmarkDisplayName = benchmarkClassType.GetDisplayName()!; 142 | var benchmarkGroupName = benchmarkClassType.GetGroupName() ?? benchmarkDisplayName; 143 | 144 | var markdown = summary.GetExportedMarkdown(); 145 | var environmentInfo = BenchmarkVisualizer.ExtractEnvironmentInfo(markdown); 146 | var markdownTable = BenchmarkVisualizer.ExtractMarkdownTable(markdown); 147 | var table = MarkdownHelper.ParseMarkdownTable(markdownTable); 148 | 149 | return new BenchmarkInfo(summary, benchmarkClassType, benchmarkDisplayName, benchmarkGroupName, environmentInfo, table); 150 | } 151 | } -------------------------------------------------------------------------------- /src/Core/BenchmarkVisualizer.ConcatReports.Options.cs: -------------------------------------------------------------------------------- 1 | namespace BenchmarkDotNetVisualizer; 2 | 3 | /// 4 | /// Options for concat reports and render as HTML output. 5 | /// 6 | /// 7 | public class ConcatReportHtmlOptions : ConcatReportImageOptions 8 | { 9 | /// 10 | /// Gets or sets the HTML wrap mode. (Defaults to ) 11 | /// 12 | /// 13 | /// The HTML wrap mode. 14 | /// 15 | public HtmlDocumentWrapMode HtmlWrapMode { get; set; } = HtmlDocumentWrapMode.Simple; 16 | 17 | /// 18 | /// The theme option for html report (Defaults to ) 19 | /// 20 | public Theme Theme { get; set; } = Theme.Dark; 21 | 22 | /// 23 | /// Creates from the specified options. 24 | /// 25 | /// The options. 26 | /// The HTML wrap mode. 27 | /// 28 | public static ConcatReportHtmlOptions From(ConcatReportImageOptions options, HtmlDocumentWrapMode htmlWrapMode = HtmlDocumentWrapMode.Simple) 29 | { 30 | return new() 31 | { 32 | Title = options.Title, 33 | DividerMode = options.DividerMode, 34 | GroupByColumns = options.GroupByColumns, 35 | SpectrumColumns = options.SpectrumColumns, 36 | SortByColumns = options.SortByColumns, 37 | HighlightGroups = options.HighlightGroups, 38 | HtmlWrapMode = htmlWrapMode, 39 | EnvironmentOnce = options.EnvironmentOnce, 40 | }; 41 | } 42 | 43 | /// 44 | public override void Validate() 45 | { 46 | if (HtmlWrapMode == HtmlDocumentWrapMode.RichDataTables && DividerMode == RenderTableDividerMode.EmptyDividerRow) 47 | throw new InvalidOperationException($"{nameof(HtmlWrapMode)}({HtmlWrapMode}) and {nameof(DividerMode)} ({DividerMode}) aren't compatible with each other."); 48 | 49 | base.Validate(); 50 | } 51 | } 52 | 53 | /// 54 | /// Options for concat reports and render as image. 55 | /// 56 | /// 57 | public class ConcatReportImageOptions : ConcatReportMarkdownOptions 58 | { 59 | /// 60 | /// Gets or sets the spectrum columns. 61 | /// 62 | /// 63 | /// The spectrum columns. 64 | /// 65 | public string[]? SpectrumColumns { get; set; } 66 | 67 | /// 68 | /// Gets or sets a value indicating whether highlight groups. (Defaults to ) 69 | /// 70 | /// 71 | /// true if highlight groups; otherwise, false. 72 | /// 73 | public bool HighlightGroups { get; set; } = true; 74 | 75 | /// 76 | public override void Validate() 77 | { 78 | // Commented because of better developer experience 79 | //if (HighlightGroups && GroupByColumns.IsNullOrEmpty()) 80 | //{ 81 | // throw new InvalidDataException( 82 | // $"Argument '{nameof(HighlightGroups)}' is set to true but '{nameof(GroupByColumns)}' are not specified." + 83 | // $" Set '{nameof(HighlightGroups)}' to false or provide '{nameof(GroupByColumns)}'."); 84 | //} 85 | 86 | base.Validate(); 87 | } 88 | } 89 | 90 | /// 91 | /// Options for concat reports and render as markdown output. 92 | /// 93 | public class ConcatReportMarkdownOptions 94 | { 95 | #if NET7_0_OR_GREATER 96 | /// 97 | /// Gets or sets the title. 98 | /// 99 | /// 100 | /// The title. 101 | /// 102 | public required string Title { get; set; } 103 | #else 104 | /// 105 | /// Gets or sets the title. 106 | /// 107 | /// 108 | /// The title. 109 | /// 110 | public string Title { get; set; } = null!; 111 | #endif 112 | 113 | /// 114 | /// Gets or sets the group by columns. 115 | /// 116 | /// 117 | /// The group by columns. 118 | /// 119 | public string[]? GroupByColumns { get; set; } 120 | 121 | /// 122 | /// Gets or sets the sort by columns. 123 | /// 124 | /// 125 | /// The sort by columns. 126 | /// 127 | public string[]? SortByColumns { get; set; } 128 | 129 | /// 130 | /// Gets or sets the divider mode. (Defaults to ) 131 | /// 132 | /// 133 | /// The divider mode. 134 | /// 135 | public virtual RenderTableDividerMode DividerMode { get; set; } = RenderTableDividerMode.EmptyDividerRow; 136 | 137 | /// 138 | /// Gets or sets a value indicating whether print environment once. (Defaults to ) 139 | /// 140 | /// 141 | /// true if print environment once; otherwise, false. 142 | /// 143 | public bool EnvironmentOnce { get; set; } = true; 144 | 145 | /// 146 | /// Validates this instance and throws exception if it's not valid. 147 | /// 148 | public virtual void Validate() 149 | { 150 | ArgumentException.ThrowIfNullOrWhiteSpace(Title, nameof(Title)); 151 | } 152 | } -------------------------------------------------------------------------------- /src/Core/BenchmarkVisualizer.SimpleReport.Options.cs: -------------------------------------------------------------------------------- 1 | namespace BenchmarkDotNetVisualizer; 2 | 3 | /// 4 | /// Options for simple report and render as HTML output. 5 | /// 6 | /// 7 | public class ReportHtmlOptions : ReportImageOptions 8 | { 9 | /// 10 | /// Gets or sets the HTML wrap mode. (Defaults to ) 11 | /// 12 | /// 13 | /// The HTML wrap mode. 14 | /// 15 | public HtmlDocumentWrapMode HtmlWrapMode { get; set; } = HtmlDocumentWrapMode.Simple; 16 | 17 | /// 18 | /// Creates from the specified options. 19 | /// 20 | /// The options. 21 | /// The HTML wrap mode. 22 | /// 23 | public static ReportHtmlOptions From(ReportImageOptions options, HtmlDocumentWrapMode htmlWrapMode = HtmlDocumentWrapMode.Simple) 24 | { 25 | return new() 26 | { 27 | Title = options.Title, 28 | DividerMode = options.DividerMode, 29 | GroupByColumns = options.GroupByColumns, 30 | SpectrumColumns = options.SpectrumColumns, 31 | SortByColumns = options.SortByColumns, 32 | HighlightGroups = options.HighlightGroups, 33 | HtmlWrapMode = htmlWrapMode, 34 | Theme = options.Theme 35 | }; 36 | } 37 | 38 | /// 39 | public override void Validate() 40 | { 41 | if (HtmlWrapMode == HtmlDocumentWrapMode.RichDataTables && DividerMode == RenderTableDividerMode.EmptyDividerRow) 42 | throw new InvalidOperationException($"{nameof(HtmlWrapMode)}({HtmlWrapMode}) and {nameof(DividerMode)} ({DividerMode}) aren't compatible with each other."); 43 | 44 | base.Validate(); 45 | } 46 | } 47 | 48 | /// 49 | /// Options for simple report and render as image. 50 | /// 51 | /// 52 | public class ReportImageOptions : ReportMarkdownOptions 53 | { 54 | /// 55 | /// Gets or sets the spectrum columns. 56 | /// 57 | /// 58 | /// The spectrum columns. 59 | /// 60 | public string[]? SpectrumColumns { get; set; } 61 | 62 | /// 63 | /// Gets or sets a value indicating whether highlight groups. (Defaults to ) 64 | /// 65 | /// 66 | /// true if highlight groups; otherwise, false. 67 | /// 68 | public bool HighlightGroups { get; set; } = true; 69 | 70 | /// 71 | /// Gets or sets the theme of the report. (Defaults to ) 72 | /// 73 | public Theme Theme { get; set; } = Theme.Dark; 74 | 75 | /// 76 | public override void Validate() 77 | { 78 | // Commented because of better developer experience 79 | //if (HighlightGroups && GroupByColumns.IsNullOrEmpty()) 80 | //{ 81 | // throw new InvalidDataException( 82 | // $"Argument '{nameof(HighlightGroups)}' is set to true but '{nameof(GroupByColumns)}' are not specified." + 83 | // $" Set '{nameof(HighlightGroups)}' to false or provide '{nameof(GroupByColumns)}'."); 84 | //} 85 | 86 | base.Validate(); 87 | } 88 | } 89 | 90 | /// 91 | /// Options for simple report and render as markdown output. 92 | /// 93 | public class ReportMarkdownOptions 94 | { 95 | #if NET7_0_OR_GREATER 96 | /// 97 | /// Gets or sets the title. 98 | /// 99 | /// 100 | /// The title. 101 | /// 102 | public required string Title { get; set; } 103 | #else 104 | /// 105 | /// Gets or sets the title. 106 | /// 107 | /// 108 | /// The title. 109 | /// 110 | public string Title { get; set; } = null!; 111 | #endif 112 | 113 | /// 114 | /// Gets or sets the group by columns. 115 | /// 116 | /// 117 | /// The group by columns. 118 | /// 119 | public string[]? GroupByColumns { get; set; } 120 | 121 | /// 122 | /// Gets or sets the sort by columns. 123 | /// 124 | /// 125 | /// The sort by columns. 126 | /// 127 | public string[]? SortByColumns { get; set; } 128 | 129 | /// 130 | /// Gets or sets the divider mode. (Defaults to ) 131 | /// 132 | /// 133 | /// The divider mode. 134 | /// 135 | public virtual RenderTableDividerMode DividerMode { get; set; } = RenderTableDividerMode.EmptyDividerRow; 136 | 137 | /// 138 | /// Validates this instance and throws exception if it's not valid. 139 | /// 140 | public virtual void Validate() 141 | { 142 | ArgumentException.ThrowIfNullOrWhiteSpace(Title, nameof(Title)); 143 | } 144 | } -------------------------------------------------------------------------------- /src/Core/BenchmarkVisualizer.Utils.cs: -------------------------------------------------------------------------------- 1 | using BenchmarkDotNet.Exporters; 2 | using BenchmarkDotNet.Loggers; 3 | using BenchmarkDotNet.Reports; 4 | using BenchmarkDotNetVisualizer.Utilities; 5 | using System.Dynamic; 6 | 7 | namespace BenchmarkDotNetVisualizer; 8 | 9 | /// 10 | /// Benchmark Visualizer 11 | /// 12 | public static partial class BenchmarkVisualizer 13 | { 14 | #region Benchmark Summary Extensions 15 | /// 16 | /// Gets the benchmark information. 17 | /// 18 | /// The summaries. 19 | /// 20 | public static IEnumerable GetBenchmarkInfo(this IEnumerable summaries) 21 | { 22 | return BenchmarkInfo.CreateFromSummary(summaries); 23 | } 24 | 25 | /// 26 | /// Gets the benchmark information. 27 | /// 28 | /// The summary. 29 | /// 30 | public static BenchmarkInfo GetBenchmarkInfo(this Summary summary) 31 | { 32 | return BenchmarkInfo.CreateFromSummary(summary); 33 | } 34 | 35 | /// 36 | /// Gets the report table. 37 | /// 38 | /// The summary. 39 | /// The row divider. 40 | /// 41 | public static IEnumerable GetReportTable(this Summary summary, ParseTableDividerMode rowDivider = ParseTableDividerMode.PlaceNull) 42 | { 43 | ArgumentNullException.ThrowIfNull(summary, nameof(summary)); 44 | 45 | var markdown = summary.GetMarkdownTable(); 46 | return MarkdownHelper.ParseMarkdownTable(markdown, rowDivider); 47 | } 48 | 49 | /// 50 | /// Gets the environment information from benchmark . 51 | /// 52 | /// The summary. 53 | /// 54 | public static string GetEnvironmentInfo(this Summary summary) 55 | { 56 | ArgumentNullException.ThrowIfNull(summary, nameof(summary)); 57 | 58 | var markdown = summary.GetExportedMarkdown(); 59 | return ExtractEnvironmentInfo(markdown); 60 | } 61 | 62 | /// 63 | /// Gets the markdown table from benchmark . 64 | /// 65 | /// The summary. 66 | /// 67 | public static string GetMarkdownTable(this Summary summary) 68 | { 69 | ArgumentNullException.ThrowIfNull(summary, nameof(summary)); 70 | 71 | var markdown = summary.GetExportedMarkdown(); 72 | return ExtractMarkdownTable(markdown); 73 | } 74 | #endregion 75 | 76 | #region ExtractEnvironmentInfo/TableInfo 77 | /// 78 | /// Extracts the environment information from markdown text of a benchmark. 79 | /// 80 | /// The markdown. 81 | /// 82 | public static string ExtractEnvironmentInfo(string markdown) 83 | { 84 | ArgumentNullException.ThrowIfNull(markdown, nameof(markdown)); 85 | ArgumentException.ThrowIfNullOrEmpty(markdown, nameof(markdown)); 86 | 87 | return markdown[..(markdown.LastIndexOf("```") + 4)].Trim([.. Environment.NewLine.ToCharArray(), ' ', '`']); 88 | } 89 | 90 | /// 91 | /// Extracts the markdown table from markdown text of a benchmark. 92 | /// 93 | /// The markdown. 94 | /// 95 | public static string ExtractMarkdownTable(string markdown) 96 | { 97 | ArgumentException.ThrowIfNullOrEmpty(markdown, nameof(markdown)); 98 | 99 | return markdown[(markdown.LastIndexOf("```") + 4)..].Trim([.. Environment.NewLine.ToCharArray(), ' ']); 100 | } 101 | #endregion 102 | 103 | #region private methods 104 | /// 105 | /// Gets the markdown text from benchmark using exporter. 106 | /// 107 | /// The summary. 108 | /// 109 | internal static string GetExportedMarkdown(this Summary summary) 110 | { 111 | ArgumentNullException.ThrowIfNull(summary, nameof(summary)); 112 | 113 | using var writer = new StringWriter(); 114 | using var logger = new TextLogger(writer); 115 | MarkdownExporter.GitHub.ExportToLog(summary, logger); 116 | return writer.ToString(); 117 | } 118 | 119 | /// 120 | /// Finds the common part of each file names. 121 | /// 122 | /// The paths. 123 | /// 124 | internal static (string Path, string CommonPart)[] FindCommonPartOfEachFileNames(this IEnumerable paths) 125 | { 126 | Guard.ThrowIfNullOrEmpty(paths, nameof(paths)); 127 | 128 | var array = paths 129 | .Select(path => 130 | { 131 | var fileName = Path.GetFileName(path); 132 | return new { Path = path, FileName = fileName }; 133 | }).ToArray(); 134 | 135 | return array 136 | .Select(item => 137 | { 138 | var commonParts = array.Select(other => FindCommonPart(item.FileName, other.FileName)).ToArray(); 139 | var bestCommonPart = commonParts.Where(p => p.Length != 0).MinBy(p => p.Length)!.Trim('_', '-'); 140 | return (item.Path, bestCommonPart); 141 | }).ToArray(); 142 | } 143 | 144 | /// 145 | /// Finds the starting common part of many strings. 146 | /// 147 | /// The strings. 148 | /// 149 | internal static string FindCommonPart(params string[] strings) 150 | { 151 | return strings.Aggregate((current, next) => string.Concat(current.TakeWhile((c, i) => c == next[i]))); 152 | } 153 | 154 | /// 155 | /// Extracts the name of the benchmark class from fileName (filePath). 156 | /// 157 | /// The filename. 158 | /// 159 | internal static string ExtractBenchmarkClassName(this string fileName) 160 | { 161 | ArgumentException.ThrowIfNullOrWhiteSpace(fileName, nameof(fileName)); 162 | 163 | fileName = Path.GetFileName(fileName); 164 | 165 | var index = fileName.LastIndexOf("-report"); 166 | if (index > 0) 167 | fileName = fileName[..index]; 168 | 169 | return fileName.Trim('_', '-'); 170 | } 171 | #endregion 172 | } -------------------------------------------------------------------------------- /src/GlobalSuppressions.cs: -------------------------------------------------------------------------------- 1 | // This file is used by Code Analysis to maintain SuppressMessage 2 | // attributes that are applied to this project. 3 | // Project-level suppressions either have no target or are given 4 | // a specific target and scoped to a namespace, type, member, etc. 5 | 6 | using System.Diagnostics.CodeAnalysis; 7 | using System.Runtime.CompilerServices; 8 | 9 | [assembly: SuppressMessage("Minor Code Smell", "S3236:Caller information arguments should not be provided explicitly")] 10 | [assembly: InternalsVisibleTo("BenchmarkDotNetVisualizer.Demo")] -------------------------------------------------------------------------------- /src/Utilities/ColorHelper.cs: -------------------------------------------------------------------------------- 1 | namespace BenchmarkDotNetVisualizer.Utilities; 2 | 3 | /// 4 | /// Color Helper 5 | /// 6 | public class ColorHelper 7 | { 8 | /// 9 | /// Returns hex color value between Red (FF0000) and Green(00FF00 ) from a Scalar value between 0 and 1 10 | /// 11 | /// Scalar value between 0 and 1. 12 | /// Hex color value 13 | public static string GetColorBetweenRedAndGreen(double value) 14 | { 15 | Guard.ThrowIfNotInRange(value, 0.0, 1.0, paramName: nameof(value)); 16 | 17 | value *= 510;// value must be between [0, 510] 18 | 19 | double redValue; 20 | double greenValue; 21 | if (value < 255) 22 | { 23 | redValue = 255; 24 | greenValue = (Math.Sqrt(value) * 16); 25 | } 26 | else 27 | { 28 | greenValue = 255; 29 | value -= 255; 30 | redValue = 255 - (value * value / 255); 31 | } 32 | 33 | return "#" + ConvertToHex(redValue) + ConvertToHex(greenValue) + "00"; 34 | 35 | static string ConvertToHex(double i) => ((int)i).ToString("X2"); 36 | } 37 | 38 | /// 39 | /// Lightens the specified color. 40 | /// 41 | /// The color. 42 | /// The amount. 43 | /// 44 | public static string Lighten(string color, double amount = 0.5) 45 | { 46 | ArgumentException.ThrowIfNullOrWhiteSpace(color, nameof(color)); 47 | Guard.ThrowIfNotInRange(amount, 0.0, 1.0, paramName: nameof(amount)); 48 | 49 | return MixColors(color, "#FFF", amount); 50 | } 51 | 52 | /// 53 | /// Darkens the specified color. 54 | /// 55 | /// The color. 56 | /// The amount. 57 | /// 58 | public static string Darken(string color, double amount = 0.5) 59 | { 60 | ArgumentException.ThrowIfNullOrWhiteSpace(color, nameof(color)); 61 | Guard.ThrowIfNotInRange(amount, 0.0, 1.0, paramName: nameof(amount)); 62 | 63 | return MixColors(color, "#000", amount); 64 | } 65 | 66 | /// 67 | /// Mixes the colors. 68 | /// 69 | /// The color a. 70 | /// The color b. 71 | /// The amount. 72 | /// 73 | public static string MixColors(string colorA, string colorB, double amount = 0.5) 74 | { 75 | ArgumentException.ThrowIfNullOrWhiteSpace(colorA, nameof(colorA)); 76 | ArgumentException.ThrowIfNullOrWhiteSpace(colorB, nameof(colorB)); 77 | Guard.ThrowIfNotInRange(amount, 0.0, 1.0, paramName: nameof(amount)); 78 | 79 | var rgbA = HexToRGB(colorA); 80 | var rgbB = HexToRGB(colorB); 81 | 82 | var mixedColor = rgbA.Select((_, index) => MixChannels(rgbA[index], rgbB[index], amount)).ToArray(); 83 | 84 | return "#" + string.Concat(mixedColor.Select(p => p.ToString("X2"))); 85 | } 86 | 87 | /// 88 | /// Converts hexadecimal color to RGB color. 89 | /// 90 | /// Color of the hexadecimal. 91 | /// 92 | public static int[] HexToRGB(string hexColor) 93 | { 94 | ArgumentException.ThrowIfNullOrWhiteSpace(hexColor, nameof(hexColor)); 95 | 96 | return GetChannels(hexColor).Select(p => Convert.ToInt32(p, 16)).ToArray(); 97 | } 98 | 99 | /// 100 | /// Gets the channels of a hexadecimal color. 101 | /// 102 | /// Color of the hexadecimal. 103 | /// 104 | public static string[] GetChannels(string hexColor) 105 | { 106 | ArgumentException.ThrowIfNullOrWhiteSpace(hexColor, nameof(hexColor)); 107 | 108 | if (hexColor[0] is '#') 109 | hexColor = hexColor[1..]; 110 | 111 | return (hexColor.Length) switch 112 | { 113 | 3 => hexColor.Chunk(1).Select(p => new string(p[0], 2)).ToArray(), 114 | 6 => hexColor.Chunk(2).Select(p => new string(p)).ToArray(), 115 | _ => throw new System.ArgumentException($"Invalid hex color '{hexColor}'", nameof(hexColor)) 116 | }; 117 | } 118 | 119 | /// 120 | /// Mixes two hexadecimal channels. 121 | /// 122 | /// The channel a. 123 | /// The channel b. 124 | /// The amount. 125 | /// 126 | public static int MixChannels(int channelA, int channelB, double amount) 127 | { 128 | Guard.ThrowIfNotInRange(amount, 0.0, 1.0, paramName: nameof(amount)); 129 | 130 | var a = channelA * (1 - amount); 131 | var b = channelB * amount; 132 | return Convert.ToInt32(a + b); 133 | } 134 | } -------------------------------------------------------------------------------- /src/Utilities/ExpandoObject/ExpandoObjectExtensions.ColumnOrdering.cs: -------------------------------------------------------------------------------- 1 | using System.Dynamic; 2 | 3 | namespace BenchmarkDotNetVisualizer.Utilities; 4 | 5 | /// 6 | /// ExpandoObject Extensions 7 | /// 8 | public static partial class ExpandoObjectExtensions 9 | { 10 | /// 11 | /// The column order prefix 12 | /// 13 | public const string ColumnOrderPrefix = "columns-order."; 14 | 15 | /// 16 | /// Gets the columns by order. 17 | /// 18 | /// The expando. 19 | /// 20 | public static string[] GetColumnsByOrder(this ExpandoObject expando) 21 | { 22 | ArgumentNullException.ThrowIfNull(expando, nameof(expando)); 23 | 24 | return expando.GetMetaProperties() 25 | .AsDictionary() 26 | .Where(p => p.Key.StartsWith(ColumnOrderPrefix)) 27 | .OrderBy(p => (int)p.Value!) 28 | .Select(p => p.Key[ColumnOrderPrefix.Length..]) 29 | .Where(expando.HasProperty) 30 | .ToArray(); 31 | } 32 | 33 | /// 34 | /// Sets the columns order as specified. 35 | /// 36 | /// The enumerable. 37 | /// The columns order. 38 | public static void SetColumnsOrder(this IEnumerable enumerable, string[] columnsOrder) 39 | { 40 | Guard.ThrowIfNullOrEmpty(enumerable, nameof(enumerable)); 41 | ArgumentNullException.ThrowIfNull(columnsOrder, nameof(columnsOrder)); 42 | 43 | foreach (var expando in enumerable) 44 | { 45 | if (expando is null) 46 | continue; 47 | SetColumnsOrder(expando, columnsOrder); 48 | } 49 | } 50 | 51 | /// 52 | /// Sets the columns order as specified. 53 | /// 54 | /// The expando. 55 | /// The columns order. 56 | public static void SetColumnsOrder(this ExpandoObject expando, string[] columnsOrder) 57 | { 58 | ArgumentNullException.ThrowIfNull(expando, nameof(expando)); 59 | ArgumentNullException.ThrowIfNull(columnsOrder, nameof(columnsOrder)); 60 | 61 | for (int order = 0; order < columnsOrder.Length; order++) 62 | { 63 | var column = columnsOrder[order]; 64 | expando.SetColumnOrder(column, order); 65 | } 66 | } 67 | 68 | /// 69 | /// Sets the column order identity (the order of the last column + 1). 70 | /// 71 | /// The expando. 72 | /// Name of the property. 73 | public static void SetColumnOrderIdentity(this ExpandoObject expando, string propertyName) 74 | { 75 | ArgumentNullException.ThrowIfNull(expando, nameof(expando)); 76 | ArgumentException.ThrowIfNullOrWhiteSpace(propertyName, nameof(propertyName)); 77 | 78 | var lastProperty = expando.AsDictionary().Last(p => p.Key != propertyName).Key; 79 | var order = expando.GetColumnOrder(lastProperty); 80 | expando.SetColumnOrder(propertyName, order + 1); 81 | } 82 | 83 | /// 84 | /// Transfers the column order from to . 85 | /// 86 | /// The expando. 87 | /// Name of from property. 88 | /// Name of to property. 89 | public static void TransferColumnOrder(this ExpandoObject expando, string fromPropertyName, string toPropertyName) 90 | { 91 | ArgumentNullException.ThrowIfNull(expando, nameof(expando)); 92 | ArgumentException.ThrowIfNullOrWhiteSpace(fromPropertyName, nameof(fromPropertyName)); 93 | ArgumentException.ThrowIfNullOrWhiteSpace(toPropertyName, nameof(toPropertyName)); 94 | 95 | var order = expando.GetColumnOrder(fromPropertyName); 96 | expando.SetColumnOrder(toPropertyName, order); 97 | } 98 | 99 | /// 100 | /// Sets the column order. 101 | /// 102 | /// The expando. 103 | /// Name of the property. 104 | /// The order. 105 | public static void SetColumnOrder(this ExpandoObject expando, string propertyName, int order) 106 | { 107 | ArgumentNullException.ThrowIfNull(expando, nameof(expando)); 108 | ArgumentException.ThrowIfNullOrWhiteSpace(propertyName, nameof(propertyName)); 109 | 110 | expando.SetMetaProperty(ColumnOrderPrefix + propertyName, order); 111 | } 112 | 113 | /// 114 | /// Gets the column order. 115 | /// 116 | /// The expando. 117 | /// Name of the property. 118 | /// 119 | public static int GetColumnOrder(this ExpandoObject expando, string propertyName) 120 | { 121 | ArgumentNullException.ThrowIfNull(expando, nameof(expando)); 122 | ArgumentException.ThrowIfNullOrWhiteSpace(propertyName, nameof(propertyName)); 123 | 124 | return (int)expando.GetMetaProperty(ColumnOrderPrefix + propertyName)!; 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/Utilities/FileLockHunter.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using System.Runtime.InteropServices; 3 | 4 | namespace BenchmarkDotNetVisualizer.Utilities; 5 | 6 | /// 7 | /// FileLock Hunter 8 | /// https://stackoverflow.com/questions/317071/how-do-i-find-out-which-process-is-locking-a-file-using-net 9 | /// http://www.csharphelper.com/howtos/howto_find_file_locker.html 10 | /// 11 | static public class FileLockHunter 12 | { 13 | #pragma warning disable S101 // Types should be named in PascalCase 14 | #pragma warning disable S112 // General or reserved exceptions should never be thrown 15 | [StructLayout(LayoutKind.Sequential)] 16 | struct RM_UNIQUE_PROCESS 17 | { 18 | public int dwProcessId; 19 | public System.Runtime.InteropServices.ComTypes.FILETIME ProcessStartTime; 20 | } 21 | 22 | const int RmRebootReasonNone = 0; 23 | const int CCH_RM_MAX_APP_NAME = 255; 24 | const int CCH_RM_MAX_SVC_NAME = 63; 25 | 26 | enum RM_APP_TYPE 27 | { 28 | RmUnknownApp = 0, 29 | RmMainWindow = 1, 30 | RmOtherWindow = 2, 31 | RmService = 3, 32 | RmExplorer = 4, 33 | RmConsole = 5, 34 | RmCritical = 1000 35 | } 36 | 37 | [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] 38 | struct RM_PROCESS_INFO 39 | { 40 | public RM_UNIQUE_PROCESS Process; 41 | 42 | [MarshalAs(UnmanagedType.ByValTStr, SizeConst = CCH_RM_MAX_APP_NAME + 1)] 43 | public string strAppName; 44 | 45 | [MarshalAs(UnmanagedType.ByValTStr, SizeConst = CCH_RM_MAX_SVC_NAME + 1)] 46 | public string strServiceShortName; 47 | 48 | public RM_APP_TYPE ApplicationType; 49 | public uint AppStatus; 50 | public uint TSSessionId; 51 | [MarshalAs(UnmanagedType.Bool)] 52 | public bool bRestartable; 53 | } 54 | 55 | [DllImport("rstrtmgr.dll", CharSet = CharSet.Unicode)] 56 | static extern int RmRegisterResources(uint pSessionHandle, 57 | uint nFiles, 58 | string[] rgsFilenames, 59 | uint nApplications, 60 | [In] RM_UNIQUE_PROCESS[] rgApplications, 61 | uint nServices, 62 | string[] rgsServiceNames); 63 | 64 | [DllImport("rstrtmgr.dll", CharSet = CharSet.Auto)] 65 | static extern int RmStartSession(out uint pSessionHandle, int dwSessionFlags, string strSessionKey); 66 | 67 | [DllImport("rstrtmgr.dll")] 68 | static extern int RmEndSession(uint pSessionHandle); 69 | 70 | [DllImport("rstrtmgr.dll")] 71 | static extern int RmGetList(uint dwSessionHandle, 72 | out uint pnProcInfoNeeded, 73 | ref uint pnProcInfo, 74 | [In, Out] RM_PROCESS_INFO[] rgAffectedApps, 75 | ref uint lpdwRebootReasons); 76 | 77 | /// 78 | /// Find out what process(es) have a lock on the specified file. 79 | /// 80 | /// Path of the file. 81 | /// Processes locking the file 82 | /// See also: 83 | /// http://msdn.microsoft.com/en-us/library/windows/desktop/aa373661(v=vs.85).aspx 84 | /// http://wyupdate.googlecode.com/svn-history/r401/trunk/frmFilesInUse.cs (no copyright in code at time of viewing) 85 | /// 86 | /// 87 | public static List WhoIsLocking(string path) 88 | { 89 | var key = Guid.NewGuid().ToString(); 90 | var processes = new List(); 91 | 92 | int res = RmStartSession(out var handle, 0, key); 93 | if (res != 0) throw new Exception("Could not begin restart session. Unable to determine file locker."); 94 | 95 | try 96 | { 97 | const int ERROR_MORE_DATA = 234; 98 | uint pnProcInfo = 0, 99 | lpdwRebootReasons = RmRebootReasonNone; 100 | 101 | string[] resources = [path]; // Just checking on one resource. 102 | 103 | res = RmRegisterResources(handle, (uint)resources.Length, resources, 0, null!, 0, null!); 104 | 105 | if (res != 0) throw new Exception("Could not register resource."); 106 | 107 | //Note: there's a race condition here -- the first call to RmGetList() returns 108 | // the total number of process. However, when we call RmGetList() again to get 109 | // the actual processes this number may have increased. 110 | res = RmGetList(handle, out var pnProcInfoNeeded, ref pnProcInfo, null!, ref lpdwRebootReasons); 111 | 112 | if (res == ERROR_MORE_DATA) 113 | { 114 | // Create an array to store the process results 115 | var processInfo = new RM_PROCESS_INFO[pnProcInfoNeeded]; 116 | pnProcInfo = pnProcInfoNeeded; 117 | 118 | // Get the list 119 | res = RmGetList(handle, out pnProcInfoNeeded, ref pnProcInfo, processInfo, ref lpdwRebootReasons); 120 | if (res == 0) 121 | { 122 | processes = new List((int)pnProcInfo); 123 | 124 | // Enumerate all of the results and add them to the 125 | // list to be returned 126 | for (int i = 0; i < pnProcInfo; i++) 127 | { 128 | try 129 | { 130 | processes.Add(Process.GetProcessById(processInfo[i].Process.dwProcessId)); 131 | } 132 | // catch the error -- in case the process is no longer running 133 | catch (System.ArgumentException) { } 134 | } 135 | } 136 | else 137 | { 138 | throw new Exception("Could not list processes locking resource."); 139 | } 140 | } 141 | else if (res != 0) 142 | { 143 | throw new Exception("Could not list processes locking resource. Failed to get size of result."); 144 | } 145 | } 146 | finally 147 | { 148 | _ = RmEndSession(handle); 149 | } 150 | 151 | return processes; 152 | } 153 | #pragma warning restore S112 // General or reserved exceptions should never be thrown 154 | #pragma warning restore S101 // Types should be named in PascalCase 155 | } 156 | -------------------------------------------------------------------------------- /src/Utilities/Guard.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel; 2 | 3 | namespace BenchmarkDotNetVisualizer.Utilities; 4 | 5 | /// 6 | /// Guard and Validation 7 | /// 8 | public static class Guard 9 | { 10 | /// Throws an exception if is null or empty. 11 | /// The argument to validate as non-null and non-empty. 12 | /// The name of the parameter with which corresponds. 13 | /// 14 | /// 15 | public static void ThrowIfNullOrEmpty([System.Diagnostics.CodeAnalysis.NotNull] IEnumerable argument, 16 | #if NETCOREAPP3_0_OR_GREATER || NET5_0_OR_GREATER 17 | [System.Runtime.CompilerServices.CallerArgumentExpression(nameof(argument))] 18 | #endif 19 | string? paramName = null) 20 | { 21 | ArgumentNullException.ThrowIfNull(argument, paramName); 22 | 23 | ThrowIfEmpty(argument, paramName); 24 | } 25 | 26 | /// Throws an exception if is empty. 27 | /// The argument to validate as non-empty. 28 | /// The name of the parameter with which corresponds. 29 | /// 30 | public static void ThrowIfEmpty(IEnumerable argument, 31 | #if NETCOREAPP3_0_OR_GREATER || NET5_0_OR_GREATER 32 | [System.Runtime.CompilerServices.CallerArgumentExpression(nameof(argument))] 33 | #endif 34 | string? paramName = null) 35 | { 36 | if (argument is null) return; 37 | 38 | //Performance Tip: using pattern matching on array is faster than TryGetNonEnumeratedCount 39 | if (argument is Array and { Length: 0 } 40 | || (argument.TryGetNonEnumeratedCount(out var count) && count == 0) 41 | || argument.Any() is false) 42 | { 43 | throw new System.ArgumentException("Argument is empty.", paramName); 44 | } 45 | } 46 | 47 | #if NET8_0_OR_GREATER 48 | /// Throws an if is not in the range. 49 | /// The argument to validate. 50 | /// The starting value to compare with . 51 | /// The ending value to compare with . 52 | /// if set to true inclusive. 53 | /// The name of the parameter with which corresponds. 54 | /// 55 | public static void ThrowIfNotInRange(T value, T from, T to, bool inclusive = true, [System.Runtime.CompilerServices.CallerArgumentExpression(nameof(value))] string? paramName = null) where T : IComparable 56 | { 57 | if (inclusive) 58 | { 59 | ArgumentOutOfRangeException.ThrowIfLessThan(value, from, paramName); 60 | ArgumentOutOfRangeException.ThrowIfGreaterThan(value, to, paramName); 61 | } 62 | else 63 | { 64 | ArgumentOutOfRangeException.ThrowIfLessThanOrEqual(value, from, paramName); 65 | ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(value, to, paramName); 66 | } 67 | } 68 | #else 69 | /// Throws an if is not in the range. 70 | /// The argument to validate. 71 | /// The starting value to compare with . 72 | /// The ending value to compare with . 73 | /// if set to true inclusive. 74 | /// The name of the parameter with which corresponds. 75 | /// 76 | public static void ThrowIfNotInRange(double value, double from, double to, bool inclusive = true, string? paramName = null) 77 | { 78 | if (inclusive) 79 | { 80 | ArgumentOutOfRangeException.ThrowIfLessThan(value, from, paramName); 81 | ArgumentOutOfRangeException.ThrowIfGreaterThan(value, to, paramName); 82 | } 83 | else 84 | { 85 | ArgumentOutOfRangeException.ThrowIfLessThanOrEqual(value, from, paramName); 86 | ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(value, to, paramName); 87 | } 88 | } 89 | #endif 90 | 91 | /// Throws an if is not an enum defined value. 92 | /// The argument to validate. 93 | /// The name of the parameter with which corresponds. 94 | /// 95 | public static void ThrowIfEnumNotDefined(TEnum value, 96 | #if NETCOREAPP3_0_OR_GREATER || NET5_0_OR_GREATER 97 | [System.Runtime.CompilerServices.CallerArgumentExpression(nameof(value))] 98 | #endif 99 | string? paramName = null) where TEnum : struct, Enum 100 | { 101 | if (Enum.IsDefined(typeof(TEnum), value) is false) 102 | throw new InvalidEnumArgumentException(paramName, Convert.ToInt32(value), typeof(TEnum)); 103 | } 104 | } -------------------------------------------------------------------------------- /src/Utilities/Html/Browser.cs: -------------------------------------------------------------------------------- 1 | using PuppeteerSharp; 2 | 3 | namespace BenchmarkDotNetVisualizer.Utilities; 4 | 5 | /// 6 | /// Represents a browser configuration. 7 | /// 8 | public class Browser 9 | { 10 | /// 11 | /// Initializes a new instance of the class. 12 | /// 13 | /// Type of the browser. 14 | /// The executable path. 15 | public Browser(SupportedBrowser browserType, string executablePath) 16 | { 17 | ArgumentException.ThrowIfNullOrWhiteSpace(executablePath, nameof(executablePath)); 18 | 19 | BrowserType = browserType; 20 | ExecutablePath = executablePath; 21 | } 22 | 23 | /// 24 | /// Gets or sets the type of the browser. 25 | /// 26 | /// 27 | /// The type of the browser. 28 | /// 29 | public SupportedBrowser BrowserType { get; set; } 30 | 31 | /// 32 | /// Gets or sets the executable path. 33 | /// 34 | /// 35 | /// The executable path. 36 | /// 37 | public string ExecutablePath { get; set; } 38 | } 39 | -------------------------------------------------------------------------------- /src/Utilities/Html/HtmlDocumentWrapMode.cs: -------------------------------------------------------------------------------- 1 | namespace BenchmarkDotNetVisualizer; 2 | 3 | /// 4 | /// Html Document Wrap Mode 5 | /// 6 | public enum HtmlDocumentWrapMode 7 | { 8 | /// 9 | /// the Simple mode 10 | /// 11 | Simple, 12 | /// 13 | /// Rich data tables mode (using https://DataTables.net/) 14 | /// 15 | RichDataTables 16 | } -------------------------------------------------------------------------------- /src/Utilities/Html/RenderTableDividerMode.cs: -------------------------------------------------------------------------------- 1 | namespace BenchmarkDotNetVisualizer; 2 | 3 | /// 4 | /// Render Table Divider Mode 5 | /// 6 | public enum RenderTableDividerMode 7 | { 8 | /// 9 | /// Renders empty rows as null divider rows 10 | /// 11 | EmptyDividerRow, 12 | /// 13 | /// Ignores null divider rows when rendering 14 | /// 15 | Ignore, 16 | /// 17 | /// Separates tables with null divider rows when rendering 18 | /// 19 | SeparateTables 20 | } 21 | -------------------------------------------------------------------------------- /src/Utilities/Html/Theme.cs: -------------------------------------------------------------------------------- 1 | namespace BenchmarkDotNetVisualizer; 2 | 3 | /// 4 | /// Determines the theme of the reports 5 | /// 6 | public enum Theme 7 | { 8 | /// 9 | /// Dark theme (default) 10 | /// 11 | Dark, 12 | 13 | /// 14 | /// Light theme 15 | /// 16 | Light, 17 | } -------------------------------------------------------------------------------- /src/Utilities/HtmlHelper.T.cs: -------------------------------------------------------------------------------- 1 | using System.Dynamic; 2 | using System.Reflection; 3 | using System.Text; 4 | 5 | namespace BenchmarkDotNetVisualizer.Utilities; 6 | 7 | public static partial class HtmlHelper 8 | { 9 | public static Task RenderToImageAsync(IEnumerable source, string path, string elementQuery = "body", 10 | RenderTableDividerMode dividerMode = RenderTableDividerMode.EmptyDividerRow) 11 | { 12 | Guard.ThrowIfNullOrEmpty(source, nameof(source)); 13 | ArgumentException.ThrowIfNullOrWhiteSpace(path, nameof(path)); 14 | ArgumentException.ThrowIfNullOrWhiteSpace(elementQuery, nameof(elementQuery)); 15 | 16 | var table = ToHtmlTable(source, dividerMode); 17 | var html = WrapInHtmlDocument(table, string.Empty, HtmlDocumentWrapMode.WithCSS); 18 | return RenderHtmlToImageAsync(html, path, elementQuery); 19 | } 20 | 21 | public static Task RenderToImageDataAsync(IEnumerable source, string elementQuery = "body", 22 | RenderTableDividerMode dividerMode = RenderTableDividerMode.EmptyDividerRow) 23 | { 24 | Guard.ThrowIfNullOrEmpty(source, nameof(source)); 25 | ArgumentException.ThrowIfNullOrWhiteSpace(elementQuery, nameof(elementQuery)); 26 | 27 | var table = ToHtmlTable(source, dividerMode); 28 | var html = WrapInHtmlDocument(table, string.Empty, HtmlDocumentWrapMode.WithCSS); 29 | return RenderHtmlToImageDataAsync(html, elementQuery); 30 | } 31 | 32 | public static Task SaveAsHtmlTableDocumentAsync(IEnumerable source, string path, string title, 33 | RenderTableDividerMode dividerMode = RenderTableDividerMode.SeparateTables, HtmlDocumentWrapMode htmlWrapMode = HtmlDocumentWrapMode.WithRichDataTables, CancellationToken cancellationToken = default) 34 | { 35 | Guard.ThrowIfNullOrEmpty(source, nameof(source)); 36 | ArgumentException.ThrowIfNullOrWhiteSpace(path, nameof(path)); 37 | 38 | var text = ToHtmlTableDocument(source, title, dividerMode, htmlWrapMode); 39 | return File.WriteAllTextAsync(path, text, cancellationToken); 40 | } 41 | 42 | public static void SaveAsHtmlTableDocument(IEnumerable source, string path, string title, 43 | RenderTableDividerMode dividerMode = RenderTableDividerMode.SeparateTables, HtmlDocumentWrapMode htmlWrapMode = HtmlDocumentWrapMode.WithRichDataTables) 44 | { 45 | Guard.ThrowIfNullOrEmpty(source, nameof(source)); 46 | ArgumentException.ThrowIfNullOrWhiteSpace(path, nameof(path)); 47 | 48 | var text = ToHtmlTableDocument(source, title, dividerMode, htmlWrapMode); 49 | File.WriteAllText(path, text); 50 | } 51 | 52 | public static string ToHtmlTableDocument(IEnumerable source, string title, 53 | RenderTableDividerMode dividerMode = RenderTableDividerMode.SeparateTables, HtmlDocumentWrapMode htmlWrapMode = HtmlDocumentWrapMode.WithRichDataTables) 54 | { 55 | Guard.ThrowIfNullOrEmpty(source, nameof(source)); 56 | 57 | var table = ToHtmlTable(source, dividerMode); 58 | return WrapInHtmlDocument(table, title, htmlWrapMode); 59 | } 60 | 61 | public static string ToHtmlTable(IEnumerable source, RenderTableDividerMode dividerMode = RenderTableDividerMode.SeparateTables) 62 | { 63 | Guard.ThrowIfNullOrEmpty(source, nameof(source)); 64 | 65 | return source switch 66 | { 67 | IEnumerable> collectionExpando => ToHtmlTableCore(collectionExpando.ConcatBy(null), dividerMode), 68 | IEnumerable> collectionT => ToHtmlTableCore(collectionT.ConcatBy(null), dividerMode), 69 | _ => ToHtmlTableCore(source, dividerMode) 70 | }; 71 | } 72 | 73 | private static string ToHtmlTableCore(IEnumerable source, RenderTableDividerMode dividerMode = RenderTableDividerMode.SeparateTables) 74 | { 75 | Guard.ThrowIfNullOrEmpty(source, nameof(source)); 76 | 77 | var stringBuilder = new StringBuilder(); 78 | switch (dividerMode) 79 | { 80 | case RenderTableDividerMode.Ignore: 81 | case RenderTableDividerMode.EmptyDividerRow: 82 | case var _ when source.Any(p => p is null) is false: 83 | Render(source); 84 | break; 85 | case RenderTableDividerMode.SeparateTables: 86 | foreach (var table in source.SplitByOccurrence(p => p is null)) 87 | Render(table); 88 | break; 89 | default: 90 | throw new NotImplementedException(); 91 | } 92 | return stringBuilder.ToString().TrimEnd(Environment.NewLine.ToCharArray()); 93 | 94 | void Render(IEnumerable table) 95 | { 96 | var props = typeof(T).GetProperties(BindingFlags.Instance | BindingFlags.Public); 97 | 98 | #pragma warning disable RCS1197 // Optimize StringBuilder.Append/AppendLine call 99 | stringBuilder.AppendLine(""); 100 | 101 | //Header 102 | stringBuilder.AppendLine(""); 103 | stringBuilder.AppendLine(""); 104 | foreach (var prop in props) 105 | { 106 | stringBuilder.AppendLine($""); 107 | } 108 | stringBuilder.AppendLine(""); 109 | stringBuilder.AppendLine(""); 110 | 111 | //Body 112 | stringBuilder.AppendLine(""); 113 | foreach (var item in table) 114 | { 115 | if (item is null) 116 | { 117 | switch (dividerMode) 118 | { 119 | case RenderTableDividerMode.EmptyDividerRow: 120 | stringBuilder.AppendLine(@""); 121 | stringBuilder.AppendLine($@""); 122 | stringBuilder.AppendLine(""); 123 | 124 | stringBuilder.AppendLine(@""); 125 | stringBuilder.AppendLine($@""); 126 | stringBuilder.AppendLine(""); 127 | continue; 128 | case RenderTableDividerMode.Ignore: 129 | continue; 130 | default: 131 | throw new NotImplementedException(); 132 | } 133 | } 134 | 135 | stringBuilder.AppendLine(""); 136 | foreach (var prop in props) 137 | { 138 | var value = ReplaceMarkdownBoldWithHtmlBold(prop.GetValue(item)?.ToString()); 139 | var style = item.TryGetMetaProperty($"{prop.Name}.background-color", out var bgColor) ? $@" style=""background-color: {bgColor};""" : null; 140 | 141 | stringBuilder.AppendLine($"{value}"); 142 | } 143 | stringBuilder.AppendLine(""); 144 | } 145 | 146 | stringBuilder.AppendLine(""); 147 | stringBuilder.AppendLine("
{prop.Name}
 
"); 148 | stringBuilder.AppendLine(); 149 | #pragma warning restore RCS1197 // Optimize StringBuilder.Append/AppendLine call 150 | } 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /src/Utilities/Image/ImageCompressionLevel.cs: -------------------------------------------------------------------------------- 1 | namespace BenchmarkDotNetVisualizer.Utilities; 2 | 3 | #pragma warning disable CA1069, RCS1234 // Enums values should not be duplicated 4 | /// 5 | /// Image Compression Level 6 | /// 7 | public enum ImageCompressionLevel 8 | { 9 | /// 10 | /// No compression. Equivalent to 11 | /// 12 | NoCompression = 0, 13 | /// 14 | /// Best speed compression level. (Equivalent to ) 15 | /// 16 | BestSpeed = 10, 17 | /// 18 | /// The default compression level. (Equivalent to ) 19 | /// 20 | DefaultCompressionForJpeg = 40, 21 | /// 22 | /// The default compression level. (Equivalent to ) 23 | /// 24 | DefaultCompressionForPng = 100, 25 | #if NET6_0_OR_GREATER 26 | /// 27 | /// The default compression level. (Equivalent to ) 28 | /// 29 | DefaultCompressionForWebp = 100, 30 | #else 31 | /// 32 | /// The default compression level. (Equivalent to ) 33 | /// 34 | DefaultCompressionForWebp = 40, 35 | #endif 36 | /// 37 | /// Best compression level. (Equivalent to ) 38 | /// 39 | BestCompression = 100, 40 | 41 | /// 42 | /// Level 0. 43 | /// 44 | Level0 = 0, 45 | /// 46 | /// Level 10. 47 | /// 48 | Level10 = 10, 49 | /// 50 | /// Level 20. 51 | /// 52 | Level20 = 20, 53 | /// 54 | /// Level 30. 55 | /// 56 | Level30 = 30, 57 | /// 58 | /// Level 40. 59 | /// 60 | Level40 = 40, 61 | /// 62 | /// Level 50. 63 | /// 64 | Level50 = 50, 65 | /// 66 | /// Level 60. 67 | /// 68 | Level60 = 60, 69 | /// 70 | /// Level 70. 71 | /// 72 | Level70 = 70, 73 | /// 74 | /// Level 80. 75 | /// 76 | Level80 = 80, 77 | /// 78 | /// Level 90. 79 | /// 80 | Level90 = 90, 81 | /// 82 | /// Level 100. 83 | /// 84 | Level100 = 100 85 | } 86 | #pragma warning restore CA1069, RCS1234 // Enums values should not be duplicated -------------------------------------------------------------------------------- /src/Utilities/Image/ImageFormat.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel; 2 | 3 | namespace BenchmarkDotNetVisualizer; 4 | 5 | /// 6 | /// Image Format 7 | /// 8 | public enum ImageFormat 9 | { 10 | /// 11 | /// The PNG format 12 | /// 13 | Png, 14 | /// 15 | /// The JPEG format 16 | /// 17 | Jpeg, 18 | /// 19 | /// The Webp format 20 | /// 21 | Webp 22 | } 23 | 24 | /// 25 | /// ImageFormat Extensions 26 | /// 27 | public static class ImageFormatExtensions 28 | { 29 | /// 30 | /// Gets extension based on image format. 31 | /// 32 | /// The image format. 33 | /// 34 | public static string GetExtension(this ImageFormat format) 35 | { 36 | return format switch 37 | { 38 | ImageFormat.Png => "png", 39 | ImageFormat.Jpeg => "jpg", 40 | ImageFormat.Webp => "webp", 41 | _ => throw new InvalidEnumArgumentException(nameof(format), Convert.ToInt32(format), format.GetType()) 42 | }; 43 | } 44 | } -------------------------------------------------------------------------------- /src/Utilities/Image/ImageSharpShims.cs: -------------------------------------------------------------------------------- 1 | // Ignore Spelling: Jpeg Png Webp metadata 2 | 3 | using SixLabors.ImageSharp; 4 | using SixLabors.ImageSharp.Formats; 5 | using SixLabors.ImageSharp.Metadata; 6 | 7 | namespace BenchmarkDotNetVisualizer.Utilities; 8 | 9 | /// 10 | /// Shims for backward compatibility to older versions of ImageSharp 11 | /// 12 | public static class ImageSharpShims 13 | { 14 | #if NET6_0_OR_GREATER 15 | /// 16 | /// Gets the decoded image format. 17 | /// 18 | /// The metadata. 19 | /// 20 | public static IImageFormat? GetDecodedImageFormat(this ImageMetadata metadata) 21 | { 22 | return metadata.DecodedImageFormat; 23 | } 24 | #else 25 | #pragma warning disable S3011 // Reflection should not be used to increase accessibility of classes, methods, or fields 26 | private static readonly System.Reflection.FieldInfo? formatMetadataField = typeof(ImageMetadata).GetField("formatMetadata", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); 27 | #pragma warning restore S3011 // Reflection should not be used to increase accessibility of classes, methods, or fields 28 | 29 | /// 30 | /// Gets the decoded image format. 31 | /// 32 | /// The metadata. 33 | /// 34 | public static IImageFormat? GetDecodedImageFormat(this ImageMetadata metadata) 35 | { 36 | var formatMetadataValue = formatMetadataField?.GetValue(metadata) as Dictionary; 37 | return formatMetadataValue?.First().Key; 38 | } 39 | 40 | /// 41 | /// For the specified file extensions type find the e . 42 | /// 43 | /// The format manager. 44 | /// The extension to return the format for. 45 | /// 46 | /// When this method returns, contains the format that matches the given extension; 47 | /// otherwise, the default value for the type of the parameter. 48 | /// This parameter is passed uninitialized. 49 | /// 50 | /// if a match is found; otherwise, 51 | public static bool TryFindFormatByFileExtension(this ImageFormatManager formatManager, string extension, [System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out IImageFormat? format) 52 | { 53 | if (!string.IsNullOrWhiteSpace(extension) && extension[0] == '.') 54 | { 55 | extension = extension[1..]; 56 | } 57 | 58 | format = formatManager.ImageFormats.FirstOrDefault(x => 59 | x.FileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase)); 60 | 61 | return format is not null; 62 | } 63 | #endif 64 | } -------------------------------------------------------------------------------- /src/Utilities/Markdown/MarkdownHelper.Parse.cs: -------------------------------------------------------------------------------- 1 | using System.Dynamic; 2 | using System.Net; 3 | using System.Text.RegularExpressions; 4 | 5 | namespace BenchmarkDotNetVisualizer.Utilities; 6 | 7 | /// 8 | /// Markdown Helper for Parsing 9 | /// 10 | public static partial class MarkdownHelper 11 | { 12 | /// 13 | /// The line separator for all operation systems 14 | /// 15 | internal static readonly string[] separator = ["\r\n", "\r", "\n"]; 16 | 17 | /// 18 | /// Parses the markdown table. 19 | /// 20 | /// The markdown table. 21 | /// The row divider. 22 | /// 23 | /// Invalid Markdown table format. 24 | public static IEnumerable ParseMarkdownTable(string markdownTable, ParseTableDividerMode rowDivider = ParseTableDividerMode.PlaceNull) 25 | { 26 | ArgumentException.ThrowIfNullOrWhiteSpace(markdownTable, nameof(markdownTable)); 27 | 28 | // Extract the table rows from the Markdown table 29 | var rows = markdownTable.Split(separator, StringSplitOptions.RemoveEmptyEntries); 30 | 31 | if (rows.Length < 3) 32 | { 33 | // The table should have at least three rows (header, separator, and content) 34 | throw new System.ArgumentException("Invalid Markdown table format."); 35 | } 36 | 37 | return Iterate(); 38 | 39 | IEnumerable Iterate() 40 | { 41 | // Extract the header row and determine the column names 42 | var headers = rows[0].Trim().Split('|', StringSplitOptions.RemoveEmptyEntries); 43 | 44 | // Extract the separator row to determine column widths 45 | var separators = rows[1].Trim().Split('|', StringSplitOptions.RemoveEmptyEntries); 46 | var columnWidths = new int[headers.Length]; 47 | for (int i = 0; i < separators.Length; i++) 48 | { 49 | columnWidths[i] = separators[i].Trim().Length; 50 | } 51 | 52 | // Process the content rows 53 | for (int i = 2; i < rows.Length; i++) 54 | { 55 | var cells = rows[i].Trim().Split('|', StringSplitOptions.RemoveEmptyEntries); 56 | 57 | if (cells.Length != headers.Length) 58 | { 59 | // The number of cells in the row should match the number of columns 60 | throw new System.ArgumentException("Invalid Markdown table format."); 61 | } 62 | 63 | var expando = new ExpandoObject(); 64 | 65 | for (int j = 0; j < headers.Length; j++) 66 | { 67 | var propertyName = headers[j].Trim(); 68 | var cellValue = cells[j].Trim(); 69 | 70 | var isBold = IsSurroundedByMarkdownBold(cellValue); 71 | if (isBold) 72 | cellValue = cellValue.Trim('*'); 73 | 74 | cellValue = WebUtility.HtmlDecode(cellValue).Trim('\''); //because of the additional html encoded quotes (')(') in "Method" column 75 | 76 | if (isBold) 77 | cellValue = $"**{cellValue}**"; 78 | 79 | expando.SetProperty(propertyName, cellValue); 80 | 81 | expando.SetColumnOrder(propertyName, j); 82 | } 83 | 84 | if (expando.IsNullDivider()) 85 | { 86 | switch (rowDivider) 87 | { 88 | case ParseTableDividerMode.PlaceNull: 89 | yield return null; 90 | break; 91 | case ParseTableDividerMode.Ignore: 92 | continue; 93 | } 94 | } 95 | else 96 | { 97 | yield return expando; 98 | } 99 | } 100 | } 101 | } 102 | 103 | /// 104 | /// Determines whether the specified expando is null divider. 105 | /// 106 | /// The expando. 107 | /// 108 | /// true if the specified expando is null divider; otherwise, false. 109 | /// 110 | public static bool IsNullDivider(this ExpandoObject expando) 111 | { 112 | if (expando is null) 113 | return true; 114 | 115 | var expandoDict = expando.AsDictionary()!; 116 | return expandoDict.Values.All(p => string.IsNullOrEmpty(p?.ToString())); 117 | } 118 | 119 | /// 120 | /// Removes the markdown bold from text. 121 | /// 122 | /// The text. 123 | /// 124 | public static string RemoveMarkdownBold(this string text) 125 | { 126 | ArgumentNullException.ThrowIfNull(text, nameof(text)); 127 | 128 | return GetMarkdownBoldRegex().Replace(text, "$1"); 129 | } 130 | 131 | /// 132 | /// Determines whether the specified value is surrounded by markdown bold. 133 | /// 134 | /// The value. 135 | /// 136 | /// true if the specified value is surrounded by markdown bold; otherwise, false. 137 | /// 138 | private static bool IsSurroundedByMarkdownBold(string? value) 139 | { 140 | if (value is null) 141 | return false; 142 | 143 | return value.StartsWith("**") && value.EndsWith("**"); 144 | } 145 | 146 | #if NET7_0_OR_GREATER 147 | [GeneratedRegex(@"\*\*(.*?)\*\*")] 148 | internal static partial Regex GetMarkdownBoldRegex(); 149 | #else 150 | internal static Regex GetMarkdownBoldRegex() => MarkdownBoldRegex; 151 | internal static readonly Regex MarkdownBoldRegex = new(@"\*\*(.*?)\*\*"); 152 | #endif 153 | } -------------------------------------------------------------------------------- /src/Utilities/Markdown/MarkdownHelper.Render.cs: -------------------------------------------------------------------------------- 1 | using System.Data; 2 | using System.Dynamic; 3 | using System.Text; 4 | 5 | namespace BenchmarkDotNetVisualizer.Utilities; 6 | 7 | /// 8 | /// Markdown Helper for Rendering 9 | /// 10 | public static partial class MarkdownHelper 11 | { 12 | /// 13 | /// Converts the enumerable to markdown table and saves as specified asynchronously. 14 | /// 15 | /// The source. 16 | /// The path. 17 | /// The divider mode. 18 | /// The cancellation token. 19 | /// 20 | public static Task SaveAsMarkdownTableAsync(this IEnumerable source, string path, RenderTableDividerMode dividerMode = RenderTableDividerMode.EmptyDividerRow, 21 | CancellationToken cancellationToken = default) 22 | { 23 | Guard.ThrowIfNullOrEmpty(source, nameof(source)); 24 | ArgumentException.ThrowIfNullOrWhiteSpace(path, nameof(path)); 25 | 26 | var text = source.ToMarkdownTable(dividerMode); 27 | DirectoryHelper.EnsureDirectoryExists(path); 28 | return File.WriteAllTextAsync(path, text, cancellationToken); 29 | } 30 | 31 | /// 32 | /// Converts the enumerable to markdown table and saves as specified . 33 | /// 34 | /// The source. 35 | /// The path. 36 | /// The divider mode. 37 | public static void SaveAsMarkdownTable(this IEnumerable source, string path, RenderTableDividerMode dividerMode = RenderTableDividerMode.EmptyDividerRow) 38 | { 39 | Guard.ThrowIfNullOrEmpty(source, nameof(source)); 40 | ArgumentException.ThrowIfNullOrWhiteSpace(path, nameof(path)); 41 | 42 | var text = source.ToMarkdownTable(dividerMode); 43 | DirectoryHelper.EnsureDirectoryExists(path); 44 | File.WriteAllText(path, text); 45 | } 46 | 47 | /// 48 | /// Converts the enumerable to markdown table. 49 | /// 50 | /// The source. 51 | /// The divider mode. 52 | /// 53 | public static string ToMarkdownTable(this IEnumerable source, RenderTableDividerMode dividerMode = RenderTableDividerMode.EmptyDividerRow) 54 | { 55 | Guard.ThrowIfNullOrEmpty(source, nameof(source)); 56 | 57 | return ToMarkdownTableCore(source, dividerMode); 58 | } 59 | 60 | private static string ToMarkdownTableCore(this IEnumerable source, RenderTableDividerMode dividerMode = RenderTableDividerMode.EmptyDividerRow) 61 | { 62 | Guard.ThrowIfNullOrEmpty(source, nameof(source)); 63 | 64 | var stringBuilder = new StringBuilder(); 65 | switch (dividerMode) 66 | { 67 | case RenderTableDividerMode.Ignore: 68 | case RenderTableDividerMode.EmptyDividerRow: 69 | case var _ when source.Any(p => p is null) is false: 70 | Render(source); 71 | break; 72 | case RenderTableDividerMode.SeparateTables: 73 | foreach (var table in source.SplitByNull()) 74 | Render(table); 75 | break; 76 | default: 77 | throw new NotImplementedException(); 78 | } 79 | return stringBuilder.ToString().TrimEnd(Environment.NewLine.ToCharArray()); 80 | 81 | void Render(IEnumerable table) 82 | { 83 | var columnNames = table.First()!.GetColumnsByOrder(); 84 | 85 | var maxColumnValues = table.Where(expando => expando is not null) 86 | .Select(expando => columnNames.Select(column => expando?.GetProperty(column)?.ToString()?.Length ?? 0)) 87 | .Union([columnNames.Select(column => column.Length)]) // Include header in column sizes 88 | .Aggregate( 89 | new int[columnNames.Length].AsEnumerable(), 90 | (accumulate, x) => accumulate.Zip(x, Math.Max)) 91 | .ToArray(); 92 | 93 | var headerValues = columnNames.Select((column, index) => column.PadRight(maxColumnValues[index])); 94 | var headerRow = string.Join(" | ", headerValues); 95 | var headerLine = $"| {headerRow} |"; 96 | 97 | var isNumericValues = columnNames.Select(column => table.Any(item => item?.GetProperty(column)?.IsNumericType() is true)).ToArray(); 98 | 99 | var headerDividerValues = columnNames.Select((_, index) => new string('-', maxColumnValues[index]) + (isNumericValues[index] ? ':' : ' ')); 100 | var headerDividerRow = string.Join("| ", headerDividerValues); 101 | var headerDividerLine = $"| {headerDividerRow}|"; 102 | 103 | var tableLines = table.Select(expando => 104 | { 105 | if (expando is null && dividerMode is RenderTableDividerMode.Ignore) 106 | return null; 107 | 108 | var columnValues = columnNames.Select((column, index) => 109 | { 110 | var columnValue = expando is null ? "" : expando.GetProperty(column)?.ToString() ?? ""; 111 | return columnValue.PadRight(maxColumnValues[index]); 112 | }); 113 | var row = string.Join(" | ", columnValues); 114 | return $"| {row} |"; 115 | }).Where(p => p is not null); 116 | 117 | stringBuilder.AppendLine(headerLine); 118 | stringBuilder.AppendLine(headerDividerLine); 119 | stringBuilder.AppendJoin(Environment.NewLine, tableLines); 120 | stringBuilder.AppendLine(); 121 | stringBuilder.AppendLine(); 122 | } 123 | } 124 | 125 | /// 126 | /// Wraps the code in a markdown code block. 127 | /// 128 | /// The code. 129 | /// The language. 130 | /// 131 | public static string WrapInCodeBlock(string code, string language = "text") 132 | { 133 | return $""" 134 | ```{language} 135 | {code} 136 | ``` 137 | """; 138 | } 139 | 140 | /// 141 | /// Determines whether the specified object is numeric type. 142 | /// 143 | /// The object. 144 | /// 145 | /// true if the specified object is numeric type; otherwise, false. 146 | /// 147 | internal static bool IsNumericType(this object? obj) 148 | { 149 | #pragma warning disable S2589 // Boolean expressions should not be gratuitous - ?.Trim('*') 150 | return obj?.GetType()?.IsNumericType() is true || obj?.ToString()?.Trim('*')?.StartsWithNumber() is true; 151 | #pragma warning restore S2589 // Boolean expressions should not be gratuitous 152 | } 153 | } -------------------------------------------------------------------------------- /src/Utilities/Markdown/ParseTableDividerMode.cs: -------------------------------------------------------------------------------- 1 | namespace BenchmarkDotNetVisualizer; 2 | 3 | /// 4 | /// Parse Table Divider Mode 5 | /// 6 | public enum ParseTableDividerMode 7 | { 8 | /// 9 | /// Places null as divider when parsing 10 | /// 11 | PlaceNull, 12 | /// 13 | /// Ignores dividers when parsing 14 | /// 15 | Ignore 16 | } 17 | -------------------------------------------------------------------------------- /src/Utilities/NETShims/NETShims.Exception.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | 3 | #pragma warning disable S2166 // Classes named like "Exception" should extend "Exception" or a subclass 4 | 5 | #if !NET8_0_OR_GREATER 6 | /// 7 | /// ArgumentException 8 | /// 9 | internal static class ArgumentException 10 | { 11 | public static void ThrowIfNullOrWhiteSpace([NotNull] string? str, string? paramName) 12 | { 13 | if (string.IsNullOrWhiteSpace(str)) 14 | throw new System.ArgumentException($"Parameter '{paramName}' is null or white-space.", paramName); 15 | } 16 | 17 | public static void ThrowIfNullOrEmpty([NotNull] string? str, string? paramName) 18 | { 19 | if (string.IsNullOrEmpty(str)) 20 | throw new System.ArgumentException($"Parameter '{paramName}' is null or empty.", paramName); 21 | } 22 | } 23 | #endif 24 | 25 | #if !NET6_0_OR_GREATER 26 | /// 27 | /// ArgumentNullException 28 | /// 29 | internal static class ArgumentNullException 30 | { 31 | public static void ThrowIfNull([NotNull] object? obj, string? paramName) 32 | { 33 | #pragma warning disable RCS1256 // Invalid argument null check 34 | if (obj is null) 35 | throw new System.ArgumentNullException(paramName, $"Parameter '{paramName}' is null."); 36 | #pragma warning restore RCS1256 // Invalid argument null check 37 | } 38 | } 39 | #endif 40 | 41 | #if !NET8_0_OR_GREATER 42 | /// 43 | /// ArgumentOutOfRangeException 44 | /// 45 | internal static class ArgumentOutOfRangeException 46 | { 47 | /// Throws an if is less than . 48 | /// The argument to validate as greater than or equal than . 49 | /// The value to compare with . 50 | /// The name of the parameter with which corresponds. 51 | public static void ThrowIfLessThan(T value, T other, string? paramName = null) 52 | where T : IComparable 53 | { 54 | if (value.CompareTo(other) < 0) 55 | ThrowLess(value, other, paramName); 56 | } 57 | 58 | /// Throws an if is greater than . 59 | /// The argument to validate as less or equal than . 60 | /// The value to compare with . 61 | /// The name of the parameter with which corresponds. 62 | public static void ThrowIfGreaterThan(T value, T other, string? paramName = null) 63 | where T : IComparable 64 | { 65 | if (value.CompareTo(other) > 0) 66 | ThrowGreater(value, other, paramName); 67 | } 68 | 69 | /// Throws an if is less than or equal . 70 | /// The argument to validate as greater than . 71 | /// The value to compare with . 72 | /// The name of the parameter with which corresponds. 73 | public static void ThrowIfLessThanOrEqual(T value, T other, string? paramName = null) 74 | where T : IComparable 75 | { 76 | if (value.CompareTo(other) <= 0) 77 | ThrowLessEqual(value, other, paramName); 78 | } 79 | 80 | /// Throws an if is greater than or equal . 81 | /// The argument to validate as less than . 82 | /// The value to compare with . 83 | /// The name of the parameter with which corresponds. 84 | public static void ThrowIfGreaterThanOrEqual(T value, T other, string? paramName = null) 85 | where T : IComparable 86 | { 87 | if (value.CompareTo(other) >= 0) 88 | ThrowGreaterEqual(value, other, paramName); 89 | } 90 | 91 | private static void ThrowLess(T value, T other, string? paramName) => 92 | throw new System.ArgumentOutOfRangeException(paramName, value, $"Parameter '{paramName}' (value: {value}) must be greater than or equal to {other}"); 93 | 94 | private static void ThrowGreater(T value, T other, string? paramName) => 95 | throw new System.ArgumentOutOfRangeException(paramName, value, $"Parameter '{paramName}' (value: {value}) must be less than or equal to {other}"); 96 | 97 | private static void ThrowLessEqual(T value, T other, string? paramName) => 98 | throw new System.ArgumentOutOfRangeException(paramName, value, $"Parameter '{paramName}' (value: {value}) must be greater than {other}"); 99 | 100 | private static void ThrowGreaterEqual(T value, T other, string? paramName) => 101 | throw new System.ArgumentOutOfRangeException(paramName, value, $"Parameter '{paramName}' (value: {value}) must be less than {other}"); 102 | } 103 | #endif 104 | #pragma warning restore S2166 // Classes named like "Exception" should extend "Exception" or a subclass -------------------------------------------------------------------------------- /src/Utilities/NumberExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Text.RegularExpressions; 2 | 3 | namespace BenchmarkDotNetVisualizer.Utilities; 4 | 5 | /// 6 | /// Number Extensions 7 | /// 8 | public static partial class NumberExtensions 9 | { 10 | /// 11 | /// Extracts the number from the . 12 | /// 13 | /// The input. 14 | /// 15 | public static decimal ExtractNumber(this string input) 16 | { 17 | ArgumentException.ThrowIfNullOrWhiteSpace(input, nameof(input)); 18 | 19 | var str = GetExtractNumberRegex().Match(input).Value;// .Value.Replace(",", ""); 20 | return decimal.Parse(str); 21 | } 22 | 23 | /// 24 | /// Extracts the number from the or default. 25 | /// 26 | /// The input. 27 | /// 28 | public static decimal ExtractNumberOrDefault(this string input) 29 | { 30 | TryExtractNumber(input, out var value); 31 | return value; 32 | } 33 | 34 | /// 35 | /// Tries to extract the number from the . 36 | /// 37 | /// The input. 38 | /// The value. 39 | /// 40 | public static bool TryExtractNumber(this string input, out decimal value) 41 | { 42 | if (string.IsNullOrWhiteSpace(input)) 43 | { 44 | value = default; 45 | return false; 46 | } 47 | 48 | var str = GetExtractNumberRegex().Match(input).Value;// .Value.Replace(",", ""); 49 | return decimal.TryParse(str, out value); 50 | } 51 | 52 | /// 53 | /// Determines whether the input starts with a number. 54 | /// 55 | /// The input. 56 | /// 57 | /// true if the input starts with a number; otherwise, false. 58 | /// 59 | public static bool StartsWithNumber(this string input) 60 | { 61 | ArgumentNullException.ThrowIfNull(input, nameof(input)); 62 | 63 | return GetStartsWithNumberRegex().IsMatch(input); 64 | } 65 | 66 | #if NET7_0_OR_GREATER 67 | [GeneratedRegex(@"[\d,]+(?:\.\d+)?")] 68 | internal static partial Regex GetExtractNumberRegex(); 69 | 70 | [GeneratedRegex(@"^\d")] 71 | internal static partial Regex GetStartsWithNumberRegex(); 72 | #else 73 | internal static Regex GetExtractNumberRegex() => ExtractNumberRegex; 74 | internal static readonly Regex ExtractNumberRegex = new(@"[\d,]+(?:\.\d+)?"); 75 | internal static Regex GetStartsWithNumberRegex() => StartsWithNumberRegex; 76 | internal static readonly Regex StartsWithNumberRegex = new(@"^\d"); 77 | #endif 78 | } 79 | -------------------------------------------------------------------------------- /src/Utilities/ReflectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel; 2 | using System.ComponentModel.DataAnnotations; 3 | using System.Reflection; 4 | 5 | namespace BenchmarkDotNetVisualizer.Utilities; 6 | 7 | /// 8 | /// Reflection Extensions 9 | /// 10 | public static class ReflectionExtensions 11 | { 12 | /// 13 | /// Gets the attribute property. 14 | /// 15 | /// The type of the attribute. 16 | /// The type of the property. 17 | /// The type. 18 | /// The property selector. 19 | /// Set true to inspect the ancestors of element; otherwise, false. 20 | /// 21 | public static TProp GetAttributeProperty(this Type type, Func propertySelector, bool inherit = false) where TAttr : Attribute 22 | { 23 | var attr = type.GetCustomAttribute(inherit); 24 | return propertySelector(attr); 25 | } 26 | 27 | /// 28 | /// Gets the value of the type. 29 | /// 30 | /// The type. 31 | /// 32 | public static string? GetGroupName(this Type type) 33 | { 34 | ArgumentNullException.ThrowIfNull(type, nameof(type)); 35 | 36 | return type.GetCustomAttribute()?.GroupName; 37 | } 38 | 39 | /// 40 | /// Gets the or value of the type or fallback to the Type.Name. 41 | /// 42 | /// The type. 43 | /// if set to true fallback to Type.Name. 44 | /// if set to true [format]. 45 | /// 46 | public static string? GetDisplayName(this Type type, bool fallbackToTypeName = true, bool format = true) 47 | { 48 | ArgumentNullException.ThrowIfNull(type, nameof(type)); 49 | 50 | var benchmarkDisplayName = 51 | type.GetCustomAttribute()?.Name 52 | ?? type.GetCustomAttribute()?.DisplayName; 53 | 54 | if (fallbackToTypeName) 55 | benchmarkDisplayName ??= type.Name; 56 | 57 | if (benchmarkDisplayName is not null && format) 58 | { 59 | var genericTypeNames = type.GenericTypeArguments.Select(PrettyTypeName).ToArray(); 60 | benchmarkDisplayName = string.Format(benchmarkDisplayName, genericTypeNames); 61 | } 62 | 63 | return benchmarkDisplayName; 64 | } 65 | 66 | /// 67 | /// Determines whether the specified type is numeric type. 68 | /// 69 | /// The type. 70 | /// 71 | /// true if the specified type is numeric type; otherwise, false. 72 | /// 73 | public static bool IsNumericType(this Type type) 74 | { 75 | return type == typeof(byte) || 76 | type == typeof(sbyte) || 77 | type == typeof(ushort) || 78 | type == typeof(uint) || 79 | type == typeof(ulong) || 80 | type == typeof(short) || 81 | type == typeof(int) || 82 | type == typeof(long) || 83 | type == typeof(decimal) || 84 | type == typeof(double) || 85 | type == typeof(float); 86 | } 87 | 88 | private static string PrettyTypeName(Type t) 89 | { 90 | if (t.IsArray) 91 | return PrettyTypeName(t.GetElementType()!) + "[]"; 92 | 93 | if (t.IsGenericType) 94 | return string.Format("{0}<{1}>", 95 | t.Name.Substring(0, t.Name.LastIndexOf("`", StringComparison.InvariantCulture)), 96 | string.Join(", ", t.GetGenericArguments().Select(PrettyTypeName))); 97 | 98 | return t.Name; 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/Utilities/StreamExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace BenchmarkDotNetVisualizer.Utilities; 2 | 3 | /// 4 | /// Stream Extensions 5 | /// 6 | public static class StreamExtensions 7 | { 8 | /// 9 | /// Gets the trimmed buffer of the specified . 10 | /// 11 | /// The stream. 12 | /// 13 | /// Use it only if you are the owner/creator of the instance, NOT when it came from somewhere else. 14 | /// Otherwise it can cause problems 15 | /// 16 | /// 17 | public static byte[] GetTrimmedBuffer(this MemoryStream stream) 18 | { 19 | var bytes = stream.GetBuffer(); 20 | Array.Resize(ref bytes, (int)stream.Length); 21 | return bytes; 22 | } 23 | } -------------------------------------------------------------------------------- /src/package-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mjebrahimi/BenchmarkDotNetVisualizer/67fea4b47eebb29b9abd0657f31cb2b3671e5900/src/package-icon.png -------------------------------------------------------------------------------- /test/BenchmarkDotNetVisualizer.Tests/BenchmarkDotNet.Artifacts/Benchmarks.ContainsBenchmark_ClassInts_-report-github.md: -------------------------------------------------------------------------------- 1 | ``` 2 | 3 | BenchmarkDotNet v0.13.12, Windows 11 (10.0.22631.3007/23H2/2023Update/SunValley3) 4 | AMD Ryzen 7 5800H with Radeon Graphics, 1 CPU, 16 logical and 8 physical cores 5 | .NET SDK 8.0.101 6 | [Host] : .NET 8.0.1 (8.0.123.58001), X64 RyuJIT AVX2 7 | ShortRun : .NET 8.0.1 (8.0.123.58001), X64 RyuJIT AVX2 8 | 9 | Job=ShortRun IterationCount=3 LaunchCount=1 10 | WarmupCount=3 11 | 12 | ``` 13 | | Method | Categories | DataType | Length | Existed | Mean | Error | StdDev | Allocated | 14 | |----------------------------------------- |----------- |---------- |------- |-------- |-------------:|------------:|-----------:|----------:| 15 | | **FrozenSet** | **O(1)** | **ClassInts** | **1000** | **False** | **3.816 ns** | **0.3439 ns** | **0.0188 ns** | **-** | 16 | | FrozenDictionary | O(1) | ClassInts | 1000 | False | 3.970 ns | 0.8071 ns | 0.0442 ns | - | 17 | | Dictionary | O(1) | ClassInts | 1000 | False | 5.605 ns | 0.5061 ns | 0.0277 ns | - | 18 | | HashSet | O(1) | ClassInts | 1000 | False | 5.896 ns | 0.8437 ns | 0.0462 ns | - | 19 | | ReadOnlyDictionary | O(1) | ClassInts | 1000 | False | 7.707 ns | 0.5068 ns | 0.0278 ns | - | 20 | | ImmutableDictionary | O(1) | ClassInts | 1000 | False | 10.967 ns | 0.5921 ns | 0.0325 ns | - | 21 | | ImmutableHashSet | O(1) | ClassInts | 1000 | False | 24.802 ns | 2.6348 ns | 0.1444 ns | - | 22 | | | | | | | | | | | 23 | | **FrozenSet** | **O(1)** | **ClassInts** | **1000** | **True** | **7.293 ns** | **0.9093 ns** | **0.0498 ns** | **-** | 24 | | FrozenDictionary | O(1) | ClassInts | 1000 | True | 7.667 ns | 2.8821 ns | 0.1580 ns | - | 25 | | HashSet | O(1) | ClassInts | 1000 | True | 9.332 ns | 0.7328 ns | 0.0402 ns | - | 26 | | Dictionary | O(1) | ClassInts | 1000 | True | 10.268 ns | 1.6772 ns | 0.0919 ns | - | 27 | | ReadOnlyDictionary | O(1) | ClassInts | 1000 | True | 11.855 ns | 2.1780 ns | 0.1194 ns | - | 28 | | ImmutableDictionary | O(1) | ClassInts | 1000 | True | 17.843 ns | 4.8202 ns | 0.2642 ns | - | 29 | | ImmutableHashSet | O(1) | ClassInts | 1000 | True | 28.555 ns | 3.5986 ns | 0.1973 ns | - | 30 | | | | | | | | | | | 31 | | **'Array (Sorted + BinarySearch)'** | **O(log(N))** | **ClassInts** | **1000** | **False** | **36.756 ns** | **7.9028 ns** | **0.4332 ns** | **-** | 32 | | 'List (Sorted + BinarySearch)' | O(log(N)) | ClassInts | 1000 | False | 37.883 ns | 12.6570 ns | 0.6938 ns | - | 33 | | 'ImmutableArray (Sorted + BinarySearch)' | O(log(N)) | ClassInts | 1000 | False | 39.942 ns | 1.4205 ns | 0.0779 ns | - | 34 | | SortedSet | O(log(N)) | ClassInts | 1000 | False | 40.534 ns | 11.2143 ns | 0.6147 ns | - | 35 | | SortedList | O(log(N)) | ClassInts | 1000 | False | 46.525 ns | 12.5093 ns | 0.6857 ns | - | 36 | | ImmutableSortedDictionary | O(log(N)) | ClassInts | 1000 | False | 50.695 ns | 1.1834 ns | 0.0649 ns | - | 37 | | ImmutableSortedSet | O(log(N)) | ClassInts | 1000 | False | 52.178 ns | 2.5300 ns | 0.1387 ns | - | 38 | | SortedDictionary | O(log(N)) | ClassInts | 1000 | False | 63.056 ns | 10.0247 ns | 0.5495 ns | - | 39 | | 'ImmutableList (Sorted + BinarySearch)' | O(log(N)) | ClassInts | 1000 | False | 77.122 ns | 19.9195 ns | 1.0919 ns | - | 40 | | | | | | | | | | | 41 | | **'Array (Sorted + BinarySearch)'** | **O(log(N))** | **ClassInts** | **1000** | **True** | **36.618 ns** | **2.7424 ns** | **0.1503 ns** | **-** | 42 | | 'List (Sorted + BinarySearch)' | O(log(N)) | ClassInts | 1000 | True | 37.077 ns | 11.5146 ns | 0.6312 ns | - | 43 | | SortedSet | O(log(N)) | ClassInts | 1000 | True | 37.702 ns | 8.5544 ns | 0.4689 ns | - | 44 | | 'ImmutableArray (Sorted + BinarySearch)' | O(log(N)) | ClassInts | 1000 | True | 39.211 ns | 1.6640 ns | 0.0912 ns | - | 45 | | ImmutableSortedSet | O(log(N)) | ClassInts | 1000 | True | 41.586 ns | 6.6197 ns | 0.3628 ns | - | 46 | | ImmutableSortedDictionary | O(log(N)) | ClassInts | 1000 | True | 42.858 ns | 3.6642 ns | 0.2008 ns | - | 47 | | SortedList | O(log(N)) | ClassInts | 1000 | True | 43.436 ns | 6.7912 ns | 0.3722 ns | - | 48 | | SortedDictionary | O(log(N)) | ClassInts | 1000 | True | 58.380 ns | 2.8560 ns | 0.1565 ns | - | 49 | | 'ImmutableList (Sorted + BinarySearch)' | O(log(N)) | ClassInts | 1000 | True | 64.249 ns | 3.4480 ns | 0.1890 ns | - | 50 | | | | | | | | | | | 51 | | **ReadOnlyCollection** | **O(N)** | **ClassInts** | **1000** | **False** | **773.154 ns** | **161.5290 ns** | **8.8539 ns** | **-** | 52 | | Array | O(N) | ClassInts | 1000 | False | 1,495.015 ns | 156.0190 ns | 8.5519 ns | - | 53 | | List | O(N) | ClassInts | 1000 | False | 1,734.703 ns | 135.2084 ns | 7.4112 ns | - | 54 | | ImmutableArray | O(N) | ClassInts | 1000 | False | 1,762.379 ns | 452.0526 ns | 24.7785 ns | - | 55 | | | | | | | | | | | 56 | | **ReadOnlyCollection** | **O(N)** | **ClassInts** | **1000** | **True** | **378.222 ns** | **92.7520 ns** | **5.0840 ns** | **-** | 57 | | Array | O(N) | ClassInts | 1000 | True | 722.915 ns | 62.4702 ns | 3.4242 ns | - | 58 | | List | O(N) | ClassInts | 1000 | True | 841.805 ns | 201.8899 ns | 11.0663 ns | - | 59 | | ImmutableArray | O(N) | ClassInts | 1000 | True | 850.812 ns | 122.4955 ns | 6.7144 ns | - | 60 | -------------------------------------------------------------------------------- /test/BenchmarkDotNetVisualizer.Tests/BenchmarkDotNet.Artifacts/Benchmarks.ContainsBenchmark_Int32_-report-github.md: -------------------------------------------------------------------------------- 1 | ``` 2 | 3 | BenchmarkDotNet v0.13.12, Windows 11 (10.0.22631.3007/23H2/2023Update/SunValley3) 4 | AMD Ryzen 7 5800H with Radeon Graphics, 1 CPU, 16 logical and 8 physical cores 5 | .NET SDK 8.0.101 6 | [Host] : .NET 8.0.1 (8.0.123.58001), X64 RyuJIT AVX2 7 | ShortRun : .NET 8.0.1 (8.0.123.58001), X64 RyuJIT AVX2 8 | 9 | Job=ShortRun IterationCount=3 LaunchCount=1 10 | WarmupCount=3 11 | 12 | ``` 13 | | Method | Categories | DataType | Length | Existed | Mean | Error | StdDev | Allocated | 14 | |----------------------------------------- |----------- |--------- |------- |-------- |----------:|-----------:|----------:|----------:| 15 | | **FrozenSet** | **O(1)** | **Int32** | **1000** | **False** | **1.945 ns** | **0.0588 ns** | **0.0032 ns** | **-** | 16 | | FrozenDictionary | O(1) | Int32 | 1000 | False | 2.029 ns | 0.1983 ns | 0.0109 ns | - | 17 | | Dictionary | O(1) | Int32 | 1000 | False | 3.151 ns | 0.2901 ns | 0.0159 ns | - | 18 | | HashSet | O(1) | Int32 | 1000 | False | 3.557 ns | 0.2335 ns | 0.0128 ns | - | 19 | | ReadOnlyDictionary | O(1) | Int32 | 1000 | False | 3.620 ns | 0.2061 ns | 0.0113 ns | - | 20 | | ImmutableDictionary | O(1) | Int32 | 1000 | False | 6.936 ns | 0.9263 ns | 0.0508 ns | - | 21 | | ImmutableHashSet | O(1) | Int32 | 1000 | False | 7.002 ns | 1.1803 ns | 0.0647 ns | - | 22 | | | | | | | | | | | 23 | | **FrozenDictionary** | **O(1)** | **Int32** | **1000** | **True** | **2.105 ns** | **0.2321 ns** | **0.0127 ns** | **-** | 24 | | FrozenSet | O(1) | Int32 | 1000 | True | 2.348 ns | 0.6690 ns | 0.0367 ns | - | 25 | | HashSet | O(1) | Int32 | 1000 | True | 3.854 ns | 1.0097 ns | 0.0553 ns | - | 26 | | Dictionary | O(1) | Int32 | 1000 | True | 3.885 ns | 0.2723 ns | 0.0149 ns | - | 27 | | ReadOnlyDictionary | O(1) | Int32 | 1000 | True | 4.289 ns | 0.4145 ns | 0.0227 ns | - | 28 | | ImmutableDictionary | O(1) | Int32 | 1000 | True | 6.075 ns | 2.0104 ns | 0.1102 ns | - | 29 | | ImmutableHashSet | O(1) | Int32 | 1000 | True | 6.100 ns | 2.0314 ns | 0.1113 ns | - | 30 | | | | | | | | | | | 31 | | **SortedSet** | **O(log(N))** | **Int32** | **1000** | **False** | **12.098 ns** | **1.9697 ns** | **0.1080 ns** | **-** | 32 | | SortedList | O(log(N)) | Int32 | 1000 | False | 14.066 ns | 4.9435 ns | 0.2710 ns | - | 33 | | 'Array (Sorted + BinarySearch)' | O(log(N)) | Int32 | 1000 | False | 15.108 ns | 1.4230 ns | 0.0780 ns | - | 34 | | 'ImmutableArray (Sorted + BinarySearch)' | O(log(N)) | Int32 | 1000 | False | 15.658 ns | 3.4903 ns | 0.1913 ns | - | 35 | | 'List (Sorted + BinarySearch)' | O(log(N)) | Int32 | 1000 | False | 19.406 ns | 7.5607 ns | 0.4144 ns | - | 36 | | SortedDictionary | O(log(N)) | Int32 | 1000 | False | 23.119 ns | 3.2485 ns | 0.1781 ns | - | 37 | | ImmutableSortedDictionary | O(log(N)) | Int32 | 1000 | False | 23.298 ns | 1.0291 ns | 0.0564 ns | - | 38 | | ImmutableSortedSet | O(log(N)) | Int32 | 1000 | False | 23.324 ns | 2.6545 ns | 0.1455 ns | - | 39 | | 'ImmutableList (Sorted + BinarySearch)' | O(log(N)) | Int32 | 1000 | False | 28.372 ns | 6.6657 ns | 0.3654 ns | - | 40 | | | | | | | | | | | 41 | | **SortedSet** | **O(log(N))** | **Int32** | **1000** | **True** | **11.001 ns** | **2.1815 ns** | **0.1196 ns** | **-** | 42 | | 'ImmutableArray (Sorted + BinarySearch)' | O(log(N)) | Int32 | 1000 | True | 13.082 ns | 2.2910 ns | 0.1256 ns | - | 43 | | 'Array (Sorted + BinarySearch)' | O(log(N)) | Int32 | 1000 | True | 13.183 ns | 1.5786 ns | 0.0865 ns | - | 44 | | SortedList | O(log(N)) | Int32 | 1000 | True | 14.344 ns | 0.4976 ns | 0.0273 ns | - | 45 | | 'List (Sorted + BinarySearch)' | O(log(N)) | Int32 | 1000 | True | 14.669 ns | 1.7234 ns | 0.0945 ns | - | 46 | | SortedDictionary | O(log(N)) | Int32 | 1000 | True | 16.904 ns | 1.2891 ns | 0.0707 ns | - | 47 | | ImmutableSortedDictionary | O(log(N)) | Int32 | 1000 | True | 18.442 ns | 1.7855 ns | 0.0979 ns | - | 48 | | ImmutableSortedSet | O(log(N)) | Int32 | 1000 | True | 18.480 ns | 4.0187 ns | 0.2203 ns | - | 49 | | 'ImmutableList (Sorted + BinarySearch)' | O(log(N)) | Int32 | 1000 | True | 24.008 ns | 7.1657 ns | 0.3928 ns | - | 50 | | | | | | | | | | | 51 | | **List** | **O(N)** | **Int32** | **1000** | **False** | **43.877 ns** | **8.4752 ns** | **0.4646 ns** | **-** | 52 | | ReadOnlyCollection | O(N) | Int32 | 1000 | False | 44.524 ns | 4.7096 ns | 0.2581 ns | - | 53 | | ImmutableArray | O(N) | Int32 | 1000 | False | 68.765 ns | 10.3856 ns | 0.5693 ns | - | 54 | | Array | O(N) | Int32 | 1000 | False | 70.900 ns | 79.2658 ns | 4.3448 ns | - | 55 | | | | | | | | | | | 56 | | **List** | **O(N)** | **Int32** | **1000** | **True** | **29.495 ns** | **11.5223 ns** | **0.6316 ns** | **-** | 57 | | Array | O(N) | Int32 | 1000 | True | 30.105 ns | 3.9241 ns | 0.2151 ns | - | 58 | | ReadOnlyCollection | O(N) | Int32 | 1000 | True | 30.159 ns | 3.3028 ns | 0.1810 ns | - | 59 | | ImmutableArray | O(N) | Int32 | 1000 | True | 30.334 ns | 1.9629 ns | 0.1076 ns | - | 60 | -------------------------------------------------------------------------------- /test/BenchmarkDotNetVisualizer.Tests/BenchmarkDotNet.Artifacts/Benchmarks.ContainsBenchmark_String_-report-github.md: -------------------------------------------------------------------------------- 1 | ``` 2 | 3 | BenchmarkDotNet v0.13.12, Windows 11 (10.0.22631.3007/23H2/2023Update/SunValley3) 4 | AMD Ryzen 7 5800H with Radeon Graphics, 1 CPU, 16 logical and 8 physical cores 5 | .NET SDK 8.0.101 6 | [Host] : .NET 8.0.1 (8.0.123.58001), X64 RyuJIT AVX2 7 | ShortRun : .NET 8.0.1 (8.0.123.58001), X64 RyuJIT AVX2 8 | 9 | Job=ShortRun IterationCount=3 LaunchCount=1 10 | WarmupCount=3 11 | 12 | ``` 13 | | Method | Categories | DataType | Length | Existed | Mean | Error | StdDev | Allocated | 14 | |----------------------------------------- |----------- |--------- |------- |-------- |-------------:|------------:|-----------:|----------:| 15 | | **FrozenSet** | **O(1)** | **String** | **1000** | **False** | **4.308 ns** | **0.0803 ns** | **0.0044 ns** | **-** | 16 | | FrozenDictionary | O(1) | String | 1000 | False | 4.405 ns | 0.4932 ns | 0.0270 ns | - | 17 | | HashSet | O(1) | String | 1000 | False | 6.841 ns | 1.0581 ns | 0.0580 ns | - | 18 | | Dictionary | O(1) | String | 1000 | False | 7.015 ns | 1.8951 ns | 0.1039 ns | - | 19 | | ReadOnlyDictionary | O(1) | String | 1000 | False | 8.961 ns | 2.6484 ns | 0.1452 ns | - | 20 | | ImmutableDictionary | O(1) | String | 1000 | False | 16.405 ns | 2.8413 ns | 0.1557 ns | - | 21 | | ImmutableHashSet | O(1) | String | 1000 | False | 26.570 ns | 3.4740 ns | 0.1904 ns | - | 22 | | | | | | | | | | | 23 | | **FrozenSet** | **O(1)** | **String** | **1000** | **True** | **4.452 ns** | **0.1191 ns** | **0.0065 ns** | **-** | 24 | | FrozenDictionary | O(1) | String | 1000 | True | 4.987 ns | 0.4987 ns | 0.0273 ns | - | 25 | | HashSet | O(1) | String | 1000 | True | 8.720 ns | 0.2512 ns | 0.0138 ns | - | 26 | | Dictionary | O(1) | String | 1000 | True | 8.825 ns | 1.4643 ns | 0.0803 ns | - | 27 | | ReadOnlyDictionary | O(1) | String | 1000 | True | 11.300 ns | 2.2122 ns | 0.1213 ns | - | 28 | | ImmutableDictionary | O(1) | String | 1000 | True | 21.104 ns | 2.9163 ns | 0.1599 ns | - | 29 | | ImmutableHashSet | O(1) | String | 1000 | True | 28.374 ns | 1.4515 ns | 0.0796 ns | - | 30 | | | | | | | | | | | 31 | | **SortedSet** | **O(log(N))** | **String** | **1000** | **False** | **307.629 ns** | **40.8524 ns** | **2.2393 ns** | **-** | 32 | | 'List (Sorted + BinarySearch)' | O(log(N)) | String | 1000 | False | 312.884 ns | 14.2113 ns | 0.7790 ns | - | 33 | | 'ImmutableArray (Sorted + BinarySearch)' | O(log(N)) | String | 1000 | False | 313.269 ns | 57.7894 ns | 3.1676 ns | - | 34 | | 'Array (Sorted + BinarySearch)' | O(log(N)) | String | 1000 | False | 314.854 ns | 105.9571 ns | 5.8079 ns | - | 35 | | SortedList | O(log(N)) | String | 1000 | False | 322.710 ns | 107.3378 ns | 5.8835 ns | - | 36 | | ImmutableSortedSet | O(log(N)) | String | 1000 | False | 323.908 ns | 61.6276 ns | 3.3780 ns | - | 37 | | ImmutableSortedDictionary | O(log(N)) | String | 1000 | False | 330.652 ns | 116.2749 ns | 6.3734 ns | - | 38 | | 'ImmutableList (Sorted + BinarySearch)' | O(log(N)) | String | 1000 | False | 356.776 ns | 58.3859 ns | 3.2003 ns | - | 39 | | SortedDictionary | O(log(N)) | String | 1000 | False | 361.296 ns | 118.4797 ns | 6.4943 ns | - | 40 | | | | | | | | | | | 41 | | **ImmutableSortedDictionary** | **O(log(N))** | **String** | **1000** | **True** | **241.350 ns** | **79.5247 ns** | **4.3590 ns** | **-** | 42 | | ImmutableSortedSet | O(log(N)) | String | 1000 | True | 248.392 ns | 79.2214 ns | 4.3424 ns | - | 43 | | 'ImmutableList (Sorted + BinarySearch)' | O(log(N)) | String | 1000 | True | 264.561 ns | 56.0615 ns | 3.0729 ns | - | 44 | | 'List (Sorted + BinarySearch)' | O(log(N)) | String | 1000 | True | 266.752 ns | 27.9816 ns | 1.5338 ns | - | 45 | | 'ImmutableArray (Sorted + BinarySearch)' | O(log(N)) | String | 1000 | True | 267.570 ns | 31.6018 ns | 1.7322 ns | - | 46 | | 'Array (Sorted + BinarySearch)' | O(log(N)) | String | 1000 | True | 268.204 ns | 102.9788 ns | 5.6446 ns | - | 47 | | SortedList | O(log(N)) | String | 1000 | True | 271.979 ns | 22.9028 ns | 1.2554 ns | - | 48 | | SortedSet | O(log(N)) | String | 1000 | True | 272.019 ns | 76.0691 ns | 4.1696 ns | - | 49 | | SortedDictionary | O(log(N)) | String | 1000 | True | 280.641 ns | 10.9002 ns | 0.5975 ns | - | 50 | | | | | | | | | | | 51 | | **ReadOnlyCollection** | **O(N)** | **String** | **1000** | **False** | **2,851.432 ns** | **683.4577 ns** | **37.4626 ns** | **-** | 52 | | List | O(N) | String | 1000 | False | 3,747.693 ns | 485.0937 ns | 26.5896 ns | - | 53 | | ImmutableArray | O(N) | String | 1000 | False | 4,042.309 ns | 569.2436 ns | 31.2022 ns | - | 54 | | Array | O(N) | String | 1000 | False | 4,171.897 ns | 475.1176 ns | 26.0428 ns | - | 55 | | | | | | | | | | | 56 | | **ReadOnlyCollection** | **O(N)** | **String** | **1000** | **True** | **1,392.430 ns** | **60.8564 ns** | **3.3357 ns** | **-** | 57 | | List | O(N) | String | 1000 | True | 1,836.948 ns | 69.8249 ns | 3.8273 ns | - | 58 | | Array | O(N) | String | 1000 | True | 1,838.527 ns | 646.2116 ns | 35.4210 ns | - | 59 | | ImmutableArray | O(N) | String | 1000 | True | 1,869.762 ns | 776.7829 ns | 42.5781 ns | - | 60 | -------------------------------------------------------------------------------- /test/BenchmarkDotNetVisualizer.Tests/BenchmarkDotNet.Artifacts/Benchmarks.TryGetValueBenchmark_ClassInts_-report-github.md: -------------------------------------------------------------------------------- 1 | ``` 2 | 3 | BenchmarkDotNet v0.13.12, Windows 11 (10.0.22631.3007/23H2/2023Update/SunValley3) 4 | AMD Ryzen 7 5800H with Radeon Graphics, 1 CPU, 16 logical and 8 physical cores 5 | .NET SDK 8.0.101 6 | [Host] : .NET 8.0.1 (8.0.123.58001), X64 RyuJIT AVX2 7 | ShortRun : .NET 8.0.1 (8.0.123.58001), X64 RyuJIT AVX2 8 | 9 | Job=ShortRun IterationCount=3 LaunchCount=1 10 | WarmupCount=3 11 | 12 | ``` 13 | | Method | Categories | DataType | Length | Existed | Mean | Error | StdDev | Allocated | 14 | |----------------------------------------- |----------- |---------- |------- |-------- |-------------:|------------:|-----------:|----------:| 15 | | **FrozenDictionary** | **O(1)** | **ClassInts** | **1000** | **False** | **3.893 ns** | **0.1170 ns** | **0.0064 ns** | **-** | 16 | | Dictionary | O(1) | ClassInts | 1000 | False | 5.800 ns | 0.6555 ns | 0.0359 ns | - | 17 | | HashSet | O(1) | ClassInts | 1000 | False | 6.058 ns | 0.5716 ns | 0.0313 ns | - | 18 | | FrozenSet | O(1) | ClassInts | 1000 | False | 7.170 ns | 0.7202 ns | 0.0395 ns | - | 19 | | ReadOnlyDictionary | O(1) | ClassInts | 1000 | False | 8.227 ns | 0.4546 ns | 0.0249 ns | - | 20 | | ImmutableHashSet | O(1) | ClassInts | 1000 | False | 10.564 ns | 2.0148 ns | 0.1104 ns | - | 21 | | ImmutableDictionary | O(1) | ClassInts | 1000 | False | 11.503 ns | 2.1492 ns | 0.1178 ns | - | 22 | | | | | | | | | | | 23 | | **FrozenDictionary** | **O(1)** | **ClassInts** | **1000** | **True** | **7.734 ns** | **1.7179 ns** | **0.0942 ns** | **-** | 24 | | Dictionary | O(1) | ClassInts | 1000 | True | 9.370 ns | 1.2367 ns | 0.0678 ns | - | 25 | | HashSet | O(1) | ClassInts | 1000 | True | 9.552 ns | 1.4827 ns | 0.0813 ns | - | 26 | | ReadOnlyDictionary | O(1) | ClassInts | 1000 | True | 12.288 ns | 2.8657 ns | 0.1571 ns | - | 27 | | FrozenSet | O(1) | ClassInts | 1000 | True | 13.617 ns | 1.1161 ns | 0.0612 ns | - | 28 | | ImmutableHashSet | O(1) | ClassInts | 1000 | True | 15.310 ns | 5.3803 ns | 0.2949 ns | - | 29 | | ImmutableDictionary | O(1) | ClassInts | 1000 | True | 17.573 ns | 0.7587 ns | 0.0416 ns | - | 30 | | | | | | | | | | | 31 | | **'List (Sorted + BinarySearch)'** | **O(log(N))** | **ClassInts** | **1000** | **False** | **36.647 ns** | **9.3205 ns** | **0.5109 ns** | **-** | 32 | | 'Array (Sorted + BinarySearch)' | O(log(N)) | ClassInts | 1000 | False | 37.925 ns | 5.4951 ns | 0.3012 ns | - | 33 | | 'ImmutableArray (Sorted + BinarySearch)' | O(log(N)) | ClassInts | 1000 | False | 39.116 ns | 16.9538 ns | 0.9293 ns | - | 34 | | SortedSet | O(log(N)) | ClassInts | 1000 | False | 42.620 ns | 8.3177 ns | 0.4559 ns | - | 35 | | SortedList | O(log(N)) | ClassInts | 1000 | False | 48.149 ns | 42.0762 ns | 2.3063 ns | - | 36 | | ImmutableSortedSet | O(log(N)) | ClassInts | 1000 | False | 48.278 ns | 11.8497 ns | 0.6495 ns | - | 37 | | ImmutableSortedDictionary | O(log(N)) | ClassInts | 1000 | False | 52.262 ns | 4.9041 ns | 0.2688 ns | - | 38 | | SortedDictionary | O(log(N)) | ClassInts | 1000 | False | 60.603 ns | 4.7155 ns | 0.2585 ns | - | 39 | | 'ImmutableList (Sorted + BinarySearch)' | O(log(N)) | ClassInts | 1000 | False | 79.511 ns | 20.5448 ns | 1.1261 ns | - | 40 | | | | | | | | | | | 41 | | **'List (Sorted + BinarySearch)'** | **O(log(N))** | **ClassInts** | **1000** | **True** | **36.386 ns** | **5.4246 ns** | **0.2973 ns** | **-** | 42 | | 'ImmutableArray (Sorted + BinarySearch)' | O(log(N)) | ClassInts | 1000 | True | 38.523 ns | 1.1629 ns | 0.0637 ns | - | 43 | | 'Array (Sorted + BinarySearch)' | O(log(N)) | ClassInts | 1000 | True | 39.157 ns | 7.1774 ns | 0.3934 ns | - | 44 | | ImmutableSortedSet | O(log(N)) | ClassInts | 1000 | True | 39.679 ns | 2.4508 ns | 0.1343 ns | - | 45 | | SortedSet | O(log(N)) | ClassInts | 1000 | True | 39.951 ns | 15.2387 ns | 0.8353 ns | - | 46 | | ImmutableSortedDictionary | O(log(N)) | ClassInts | 1000 | True | 42.528 ns | 4.8931 ns | 0.2682 ns | - | 47 | | SortedList | O(log(N)) | ClassInts | 1000 | True | 43.552 ns | 5.2954 ns | 0.2903 ns | - | 48 | | SortedDictionary | O(log(N)) | ClassInts | 1000 | True | 59.154 ns | 9.6093 ns | 0.5267 ns | - | 49 | | 'ImmutableList (Sorted + BinarySearch)' | O(log(N)) | ClassInts | 1000 | True | 68.583 ns | 4.8163 ns | 0.2640 ns | - | 50 | | | | | | | | | | | 51 | | **ReadOnlyCollection** | **O(N)** | **ClassInts** | **1000** | **False** | **773.975 ns** | **105.2108 ns** | **5.7670 ns** | **-** | 52 | | ImmutableArray | O(N) | ClassInts | 1000 | False | 1,504.838 ns | 258.8998 ns | 14.1912 ns | - | 53 | | List | O(N) | ClassInts | 1000 | False | 1,508.505 ns | 109.9052 ns | 6.0243 ns | - | 54 | | Array | O(N) | ClassInts | 1000 | False | 1,736.114 ns | 224.0401 ns | 12.2804 ns | - | 55 | | | | | | | | | | | 56 | | **ReadOnlyCollection** | **O(N)** | **ClassInts** | **1000** | **True** | **265.686 ns** | **38.7472 ns** | **2.1239 ns** | **-** | 57 | | List | O(N) | ClassInts | 1000 | True | 726.899 ns | 89.2420 ns | 4.8917 ns | - | 58 | | Array | O(N) | ClassInts | 1000 | True | 841.566 ns | 78.2787 ns | 4.2907 ns | - | 59 | | ImmutableArray | O(N) | ClassInts | 1000 | True | 852.207 ns | 251.4988 ns | 13.7855 ns | - | 60 | -------------------------------------------------------------------------------- /test/BenchmarkDotNetVisualizer.Tests/BenchmarkDotNet.Artifacts/Benchmarks.TryGetValueBenchmark_Int32_-report-github.md: -------------------------------------------------------------------------------- 1 | ``` 2 | 3 | BenchmarkDotNet v0.13.12, Windows 11 (10.0.22631.3007/23H2/2023Update/SunValley3) 4 | AMD Ryzen 7 5800H with Radeon Graphics, 1 CPU, 16 logical and 8 physical cores 5 | .NET SDK 8.0.101 6 | [Host] : .NET 8.0.1 (8.0.123.58001), X64 RyuJIT AVX2 7 | ShortRun : .NET 8.0.1 (8.0.123.58001), X64 RyuJIT AVX2 8 | 9 | Job=ShortRun IterationCount=3 LaunchCount=1 10 | WarmupCount=3 11 | 12 | ``` 13 | | Method | Categories | DataType | Length | Existed | Mean | Error | StdDev | Allocated | 14 | |----------------------------------------- |----------- |--------- |------- |-------- |----------:|----------:|----------:|----------:| 15 | | **FrozenSet** | **O(1)** | **Int32** | **1000** | **False** | **2.017 ns** | **0.1896 ns** | **0.0104 ns** | **-** | 16 | | FrozenDictionary | O(1) | Int32 | 1000 | False | 2.030 ns | 0.1221 ns | 0.0067 ns | - | 17 | | Dictionary | O(1) | Int32 | 1000 | False | 3.391 ns | 0.5301 ns | 0.0291 ns | - | 18 | | ReadOnlyDictionary | O(1) | Int32 | 1000 | False | 3.455 ns | 0.6222 ns | 0.0341 ns | - | 19 | | HashSet | O(1) | Int32 | 1000 | False | 3.481 ns | 0.5842 ns | 0.0320 ns | - | 20 | | ImmutableDictionary | O(1) | Int32 | 1000 | False | 7.248 ns | 3.0504 ns | 0.1672 ns | - | 21 | | ImmutableHashSet | O(1) | Int32 | 1000 | False | 7.340 ns | 2.7137 ns | 0.1487 ns | - | 22 | | | | | | | | | | | 23 | | **FrozenDictionary** | **O(1)** | **Int32** | **1000** | **True** | **2.095 ns** | **0.2315 ns** | **0.0127 ns** | **-** | 24 | | FrozenSet | O(1) | Int32 | 1000 | True | 2.394 ns | 0.5573 ns | 0.0305 ns | - | 25 | | HashSet | O(1) | Int32 | 1000 | True | 2.853 ns | 0.5433 ns | 0.0298 ns | - | 26 | | Dictionary | O(1) | Int32 | 1000 | True | 4.085 ns | 0.1796 ns | 0.0098 ns | - | 27 | | ReadOnlyDictionary | O(1) | Int32 | 1000 | True | 4.432 ns | 0.6207 ns | 0.0340 ns | - | 28 | | ImmutableDictionary | O(1) | Int32 | 1000 | True | 6.307 ns | 2.3095 ns | 0.1266 ns | - | 29 | | ImmutableHashSet | O(1) | Int32 | 1000 | True | 6.315 ns | 0.9480 ns | 0.0520 ns | - | 30 | | | | | | | | | | | 31 | | **SortedSet** | **O(log(N))** | **Int32** | **1000** | **False** | **11.367 ns** | **2.2254 ns** | **0.1220 ns** | **-** | 32 | | 'List (Sorted + BinarySearch)' | O(log(N)) | Int32 | 1000 | False | 14.998 ns | 2.6544 ns | 0.1455 ns | - | 33 | | 'Array (Sorted + BinarySearch)' | O(log(N)) | Int32 | 1000 | False | 15.383 ns | 2.5442 ns | 0.1395 ns | - | 34 | | 'ImmutableArray (Sorted + BinarySearch)' | O(log(N)) | Int32 | 1000 | False | 15.764 ns | 0.6891 ns | 0.0378 ns | - | 35 | | SortedList | O(log(N)) | Int32 | 1000 | False | 16.714 ns | 1.9644 ns | 0.1077 ns | - | 36 | | SortedDictionary | O(log(N)) | Int32 | 1000 | False | 19.917 ns | 1.2177 ns | 0.0667 ns | - | 37 | | ImmutableSortedDictionary | O(log(N)) | Int32 | 1000 | False | 23.368 ns | 5.7702 ns | 0.3163 ns | - | 38 | | ImmutableSortedSet | O(log(N)) | Int32 | 1000 | False | 24.754 ns | 3.4614 ns | 0.1897 ns | - | 39 | | 'ImmutableList (Sorted + BinarySearch)' | O(log(N)) | Int32 | 1000 | False | 28.555 ns | 0.5529 ns | 0.0303 ns | - | 40 | | | | | | | | | | | 41 | | **SortedSet** | **O(log(N))** | **Int32** | **1000** | **True** | **12.824 ns** | **1.0387 ns** | **0.0569 ns** | **-** | 42 | | 'ImmutableArray (Sorted + BinarySearch)' | O(log(N)) | Int32 | 1000 | True | 13.101 ns | 1.0020 ns | 0.0549 ns | - | 43 | | 'Array (Sorted + BinarySearch)' | O(log(N)) | Int32 | 1000 | True | 13.181 ns | 1.1741 ns | 0.0644 ns | - | 44 | | SortedList | O(log(N)) | Int32 | 1000 | True | 13.182 ns | 1.1688 ns | 0.0641 ns | - | 45 | | 'List (Sorted + BinarySearch)' | O(log(N)) | Int32 | 1000 | True | 13.594 ns | 3.7703 ns | 0.2067 ns | - | 46 | | ImmutableSortedDictionary | O(log(N)) | Int32 | 1000 | True | 18.594 ns | 0.5907 ns | 0.0324 ns | - | 47 | | ImmutableSortedSet | O(log(N)) | Int32 | 1000 | True | 18.637 ns | 0.7645 ns | 0.0419 ns | - | 48 | | SortedDictionary | O(log(N)) | Int32 | 1000 | True | 19.448 ns | 3.0688 ns | 0.1682 ns | - | 49 | | 'ImmutableList (Sorted + BinarySearch)' | O(log(N)) | Int32 | 1000 | True | 27.142 ns | 3.5470 ns | 0.1944 ns | - | 50 | | | | | | | | | | | 51 | | **Array** | **O(N)** | **Int32** | **1000** | **False** | **41.027 ns** | **7.8113 ns** | **0.4282 ns** | **-** | 52 | | ReadOnlyCollection | O(N) | Int32 | 1000 | False | 42.841 ns | 2.3381 ns | 0.1282 ns | - | 53 | | List | O(N) | Int32 | 1000 | False | 42.976 ns | 8.8244 ns | 0.4837 ns | - | 54 | | ImmutableArray | O(N) | Int32 | 1000 | False | 68.369 ns | 6.3947 ns | 0.3505 ns | - | 55 | | | | | | | | | | | 56 | | **Array** | **O(N)** | **Int32** | **1000** | **True** | **28.672 ns** | **3.1231 ns** | **0.1712 ns** | **-** | 57 | | List | O(N) | Int32 | 1000 | True | 29.101 ns | 8.8553 ns | 0.4854 ns | - | 58 | | ImmutableArray | O(N) | Int32 | 1000 | True | 30.060 ns | 1.7139 ns | 0.0939 ns | - | 59 | | ReadOnlyCollection | O(N) | Int32 | 1000 | True | 32.001 ns | 3.3766 ns | 0.1851 ns | - | 60 | -------------------------------------------------------------------------------- /test/BenchmarkDotNetVisualizer.Tests/BenchmarkDotNet.Artifacts/Benchmarks.TryGetValueBenchmark_String_-report-github.md: -------------------------------------------------------------------------------- 1 | ``` 2 | 3 | BenchmarkDotNet v0.13.12, Windows 11 (10.0.22631.3007/23H2/2023Update/SunValley3) 4 | AMD Ryzen 7 5800H with Radeon Graphics, 1 CPU, 16 logical and 8 physical cores 5 | .NET SDK 8.0.101 6 | [Host] : .NET 8.0.1 (8.0.123.58001), X64 RyuJIT AVX2 7 | ShortRun : .NET 8.0.1 (8.0.123.58001), X64 RyuJIT AVX2 8 | 9 | Job=ShortRun IterationCount=3 LaunchCount=1 10 | WarmupCount=3 11 | 12 | ``` 13 | | Method | Categories | DataType | Length | Existed | Mean | Error | StdDev | Allocated | 14 | |----------------------------------------- |----------- |--------- |------- |-------- |-------------:|------------:|-----------:|----------:| 15 | | **FrozenDictionary** | **O(1)** | **String** | **1000** | **False** | **4.421 ns** | **0.5035 ns** | **0.0276 ns** | **-** | 16 | | FrozenSet | O(1) | String | 1000 | False | 6.279 ns | 0.4430 ns | 0.0243 ns | - | 17 | | Dictionary | O(1) | String | 1000 | False | 7.095 ns | 1.3972 ns | 0.0766 ns | - | 18 | | HashSet | O(1) | String | 1000 | False | 7.151 ns | 1.3768 ns | 0.0755 ns | - | 19 | | ReadOnlyDictionary | O(1) | String | 1000 | False | 9.358 ns | 1.8240 ns | 0.1000 ns | - | 20 | | ImmutableHashSet | O(1) | String | 1000 | False | 15.515 ns | 3.4441 ns | 0.1888 ns | - | 21 | | ImmutableDictionary | O(1) | String | 1000 | False | 16.466 ns | 8.5353 ns | 0.4678 ns | - | 22 | | | | | | | | | | | 23 | | **FrozenDictionary** | **O(1)** | **String** | **1000** | **True** | **4.857 ns** | **0.6566 ns** | **0.0360 ns** | **-** | 24 | | Dictionary | O(1) | String | 1000 | True | 8.973 ns | 1.2395 ns | 0.0679 ns | - | 25 | | HashSet | O(1) | String | 1000 | True | 9.021 ns | 0.4231 ns | 0.0232 ns | - | 26 | | FrozenSet | O(1) | String | 1000 | True | 10.957 ns | 1.4986 ns | 0.0821 ns | - | 27 | | ReadOnlyDictionary | O(1) | String | 1000 | True | 11.229 ns | 1.0306 ns | 0.0565 ns | - | 28 | | ImmutableHashSet | O(1) | String | 1000 | True | 19.697 ns | 2.6280 ns | 0.1441 ns | - | 29 | | ImmutableDictionary | O(1) | String | 1000 | True | 20.911 ns | 5.6117 ns | 0.3076 ns | - | 30 | | | | | | | | | | | 31 | | **SortedSet** | **O(log(N))** | **String** | **1000** | **False** | **303.085 ns** | **43.5154 ns** | **2.3852 ns** | **-** | 32 | | 'Array (Sorted + BinarySearch)' | O(log(N)) | String | 1000 | False | 311.216 ns | 25.6718 ns | 1.4072 ns | - | 33 | | SortedList | O(log(N)) | String | 1000 | False | 319.730 ns | 13.0537 ns | 0.7155 ns | - | 34 | | 'ImmutableArray (Sorted + BinarySearch)' | O(log(N)) | String | 1000 | False | 321.109 ns | 146.2676 ns | 8.0174 ns | - | 35 | | ImmutableSortedDictionary | O(log(N)) | String | 1000 | False | 323.934 ns | 91.7163 ns | 5.0273 ns | - | 36 | | ImmutableSortedSet | O(log(N)) | String | 1000 | False | 336.183 ns | 32.8334 ns | 1.7997 ns | - | 37 | | 'List (Sorted + BinarySearch)' | O(log(N)) | String | 1000 | False | 353.234 ns | 88.7693 ns | 4.8657 ns | - | 38 | | 'ImmutableList (Sorted + BinarySearch)' | O(log(N)) | String | 1000 | False | 357.601 ns | 75.3227 ns | 4.1287 ns | - | 39 | | SortedDictionary | O(log(N)) | String | 1000 | False | 358.193 ns | 15.9219 ns | 0.8727 ns | - | 40 | | | | | | | | | | | 41 | | **ImmutableSortedDictionary** | **O(log(N))** | **String** | **1000** | **True** | **237.537 ns** | **45.9291 ns** | **2.5175 ns** | **-** | 42 | | ImmutableSortedSet | O(log(N)) | String | 1000 | True | 245.379 ns | 55.6813 ns | 3.0521 ns | - | 43 | | 'List (Sorted + BinarySearch)' | O(log(N)) | String | 1000 | True | 264.359 ns | 52.4840 ns | 2.8768 ns | - | 44 | | 'Array (Sorted + BinarySearch)' | O(log(N)) | String | 1000 | True | 264.543 ns | 23.6696 ns | 1.2974 ns | - | 45 | | 'ImmutableArray (Sorted + BinarySearch)' | O(log(N)) | String | 1000 | True | 265.289 ns | 36.0121 ns | 1.9739 ns | - | 46 | | 'ImmutableList (Sorted + BinarySearch)' | O(log(N)) | String | 1000 | True | 268.966 ns | 10.2658 ns | 0.5627 ns | - | 47 | | SortedList | O(log(N)) | String | 1000 | True | 270.004 ns | 52.1485 ns | 2.8584 ns | - | 48 | | SortedDictionary | O(log(N)) | String | 1000 | True | 281.324 ns | 66.7073 ns | 3.6565 ns | - | 49 | | SortedSet | O(log(N)) | String | 1000 | True | 289.570 ns | 59.6427 ns | 3.2692 ns | - | 50 | | | | | | | | | | | 51 | | **ReadOnlyCollection** | **O(N)** | **String** | **1000** | **False** | **3,005.547 ns** | **436.2414 ns** | **23.9119 ns** | **-** | 52 | | ImmutableArray | O(N) | String | 1000 | False | 3,938.770 ns | 526.2674 ns | 28.8465 ns | - | 53 | | List | O(N) | String | 1000 | False | 4,002.468 ns | 646.8517 ns | 35.4561 ns | - | 54 | | Array | O(N) | String | 1000 | False | 4,006.808 ns | 218.9723 ns | 12.0026 ns | - | 55 | | | | | | | | | | | 56 | | **ReadOnlyCollection** | **O(N)** | **String** | **1000** | **True** | **1,435.280 ns** | **715.1145 ns** | **39.1978 ns** | **-** | 57 | | ImmutableArray | O(N) | String | 1000 | True | 1,789.514 ns | 753.6574 ns | 41.3105 ns | - | 58 | | List | O(N) | String | 1000 | True | 1,826.351 ns | 227.0355 ns | 12.4446 ns | - | 59 | | Array | O(N) | String | 1000 | True | 1,831.162 ns | 403.4804 ns | 22.1161 ns | - | 60 | -------------------------------------------------------------------------------- /test/BenchmarkDotNetVisualizer.Tests/BenchmarkDotNet.Artifacts/IterationBenchmark-report-github.md: -------------------------------------------------------------------------------- 1 | ``` 2 | 3 | BenchmarkDotNet v0.14.0, Windows 11 (10.0.22631.3958/23H2/2023Update/SunValley3) 4 | AMD Ryzen 7 5800H with Radeon Graphics, 1 CPU, 16 logical and 8 physical cores 5 | .NET SDK 8.0.304 6 | [Host] : .NET 8.0.8 (8.0.824.36612), X64 RyuJIT AVX2 7 | ShortRun-.NET 5.0 : .NET 5.0.17 (5.0.1722.21314), X64 RyuJIT AVX2 8 | ShortRun-.NET 6.0 : .NET 6.0.32 (6.0.3224.31407), X64 RyuJIT AVX2 9 | ShortRun-.NET 7.0 : .NET 7.0.20 (7.0.2024.26716), X64 RyuJIT AVX2 10 | ShortRun-.NET 8.0 : .NET 8.0.8 (8.0.824.36612), X64 RyuJIT AVX2 11 | ShortRun-.NET Core 3.0 : .NET Core 3.0.3 (CoreCLR 4.700.20.6603, CoreFX 4.700.20.6701), X64 RyuJIT AVX2 12 | ShortRun-.NET Core 3.1 : .NET Core 3.1.32 (CoreCLR 4.700.22.55902, CoreFX 4.700.22.56512), X64 RyuJIT AVX2 13 | 14 | IterationCount=3 LaunchCount=1 WarmupCount=3 15 | 16 | ``` 17 | | Method | Runtime | Categories | Length | Mean | Error | StdDev | Allocated | 18 | |---------- |-------------- |----------- |------- |-----------:|------------:|----------:|----------:| 19 | | for | .NET 5.0 | Array | 1000 | 246.7 ns | 30.46 ns | 1.67 ns | - | 20 | | foreach | .NET 5.0 | Array | 1000 | 240.5 ns | 52.98 ns | 2.90 ns | - | 21 | | ForEach() | .NET 5.0 | Array | 1000 | 1,444.4 ns | 324.50 ns | 17.79 ns | - | 22 | | for | .NET 6.0 | Array | 1000 | 245.4 ns | 84.32 ns | 4.62 ns | - | 23 | | foreach | .NET 6.0 | Array | 1000 | 243.3 ns | 26.15 ns | 1.43 ns | - | 24 | | ForEach() | .NET 6.0 | Array | 1000 | 1,434.5 ns | 130.43 ns | 7.15 ns | - | 25 | | for | .NET 7.0 | Array | 1000 | 243.9 ns | 47.43 ns | 2.60 ns | - | 26 | | foreach | .NET 7.0 | Array | 1000 | 239.2 ns | 34.23 ns | 1.88 ns | - | 27 | | ForEach() | .NET 7.0 | Array | 1000 | 1,432.3 ns | 109.75 ns | 6.02 ns | - | 28 | | for | .NET 8.0 | Array | 1000 | 242.3 ns | 5.06 ns | 0.28 ns | - | 29 | | foreach | .NET 8.0 | Array | 1000 | 237.9 ns | 16.09 ns | 0.88 ns | - | 30 | | ForEach() | .NET 8.0 | Array | 1000 | 243.6 ns | 1.51 ns | 0.08 ns | - | 31 | | for | .NET Core 3.0 | Array | 1000 | 293.0 ns | 18.81 ns | 1.03 ns | - | 32 | | foreach | .NET Core 3.0 | Array | 1000 | 476.0 ns | 11.86 ns | 0.65 ns | - | 33 | | ForEach() | .NET Core 3.0 | Array | 1000 | 1,433.8 ns | 28.19 ns | 1.55 ns | - | 34 | | for | .NET Core 3.1 | Array | 1000 | 294.6 ns | 88.04 ns | 4.83 ns | - | 35 | | foreach | .NET Core 3.1 | Array | 1000 | 242.9 ns | 65.42 ns | 3.59 ns | - | 36 | | ForEach() | .NET Core 3.1 | Array | 1000 | 1,674.4 ns | 176.58 ns | 9.68 ns | - | 37 | | for | .NET 5.0 | List | 1000 | 478.0 ns | 8.57 ns | 0.47 ns | - | 38 | | foreach | .NET 5.0 | List | 1000 | 1,882.4 ns | 579.49 ns | 31.76 ns | - | 39 | | ForEach() | .NET 5.0 | List | 1000 | 1,684.6 ns | 74.63 ns | 4.09 ns | - | 40 | | for | .NET 6.0 | List | 1000 | 392.4 ns | 887.03 ns | 48.62 ns | - | 41 | | foreach | .NET 6.0 | List | 1000 | 718.4 ns | 48.25 ns | 2.64 ns | - | 42 | | ForEach() | .NET 6.0 | List | 1000 | 1,675.2 ns | 26.74 ns | 1.47 ns | - | 43 | | for | .NET 7.0 | List | 1000 | 477.6 ns | 10.90 ns | 0.60 ns | - | 44 | | foreach | .NET 7.0 | List | 1000 | 489.2 ns | 50.86 ns | 2.79 ns | - | 45 | | ForEach() | .NET 7.0 | List | 1000 | 1,572.6 ns | 697.66 ns | 38.24 ns | - | 46 | | for | .NET 8.0 | List | 1000 | 394.7 ns | 1,003.36 ns | 55.00 ns | - | 47 | | foreach | .NET 8.0 | List | 1000 | 480.1 ns | 30.16 ns | 1.65 ns | - | 48 | | ForEach() | .NET 8.0 | List | 1000 | 619.3 ns | 77.13 ns | 4.23 ns | - | 49 | | for | .NET Core 3.0 | List | 1000 | 480.3 ns | 12.57 ns | 0.69 ns | - | 50 | | foreach | .NET Core 3.0 | List | 1000 | 1,759.1 ns | 1,965.96 ns | 107.76 ns | - | 51 | | ForEach() | .NET Core 3.0 | List | 1000 | 1,562.5 ns | 1,827.30 ns | 100.16 ns | - | 52 | | for | .NET Core 3.1 | List | 1000 | 479.9 ns | 19.59 ns | 1.07 ns | - | 53 | | foreach | .NET Core 3.1 | List | 1000 | 1,693.5 ns | 12.95 ns | 0.71 ns | - | 54 | | ForEach() | .NET Core 3.1 | List | 1000 | 1,737.7 ns | 1,531.54 ns | 83.95 ns | - | 55 | -------------------------------------------------------------------------------- /test/BenchmarkDotNetVisualizer.Tests/BenchmarkDotNet.Artifacts/JsonSerializersBenchmark-report-github.md: -------------------------------------------------------------------------------- 1 | ``` 2 | 3 | BenchmarkDotNet v0.14.0, Windows 11 (10.0.22631.3958/23H2/2023Update/SunValley3) 4 | AMD Ryzen 7 5800H with Radeon Graphics, 1 CPU, 16 logical and 8 physical cores 5 | .NET SDK 8.0.304 6 | [Host] : .NET 8.0.8 (8.0.824.36612), X64 RyuJIT AVX2 7 | ShortRun : .NET 8.0.8 (8.0.824.36612), X64 RyuJIT AVX2 8 | 9 | Job=ShortRun IterationCount=3 LaunchCount=1 10 | WarmupCount=3 11 | 12 | ``` 13 | | Method | Categories | Mean | Error | StdDev | Allocated | 14 | |------------ |-------------------- |-----------:|------------:|---------:|----------:| 15 | | Serialize | NewtonsoftJson | 1,956.6 ns | 588.60 ns | 32.26 ns | 3048 B | 16 | | Deserialize | NewtonsoftJson | 3,557.3 ns | 1,517.05 ns | 83.15 ns | 5504 B | 17 | | Serialize | SystemTextJson | 1,185.5 ns | 101.66 ns | 5.57 ns | 1040 B | 18 | | Deserialize | SystemTextJson | 2,002.2 ns | 193.02 ns | 10.58 ns | 1784 B | 19 | | Serialize | SystemTextSourceGen | 721.9 ns | 96.62 ns | 5.30 ns | 728 B | 20 | | Deserialize | SystemTextSourceGen | 2,093.9 ns | 230.57 ns | 12.64 ns | 1688 B | 21 | -------------------------------------------------------------------------------- /test/BenchmarkDotNetVisualizer.Tests/BenchmarkDotNetVisualizer.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net9.0 5 | enable 6 | enable 7 | false 8 | true 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /test/BenchmarkDotNetVisualizer.Tests/ImageComparer.cs: -------------------------------------------------------------------------------- 1 | using SixLabors.ImageSharp; 2 | using SixLabors.ImageSharp.PixelFormats; 3 | 4 | namespace BenchmarkDotNetVisualizer.Tests; 5 | 6 | internal static class ImageComparer 7 | { 8 | public static async Task IsEqualAsync(string imagePath1, string imagePath2) 9 | { 10 | using var image1 = await Image.LoadAsync(imagePath1); 11 | using var image2 = await Image.LoadAsync(imagePath2); 12 | 13 | if (image1.Width != image2.Width || image1.Height != image2.Height) 14 | { 15 | Console.WriteLine("Images must be of the same dimensions."); 16 | return false; 17 | } 18 | 19 | for (int x = 0; x < image1.Width; x++) 20 | { 21 | for (int y = 0; y < image1.Height; y++) 22 | { 23 | var pixel1 = image1[x, y]; 24 | var pixel2 = image2[x, y]; 25 | 26 | if (pixel1 != pixel2) 27 | { 28 | return false; 29 | } 30 | } 31 | } 32 | 33 | return true; 34 | } 35 | 36 | public static async Task IsSimilarAsync(string imagePath1, string imagePath2, double ignoreDifferentColorPercentLessThan = 5.0, double thresholdDifferentPixlesPercent = 1.0) 37 | { 38 | var diff = await CalculateDiffAsync(imagePath1, imagePath2, ignoreDifferentColorPercentLessThan); 39 | return diff < thresholdDifferentPixlesPercent; 40 | } 41 | 42 | public static async Task CalculateDiffAsync(string imagePath1, string imagePath2, double ignoreDifferentColorPercentLessThan = 10.0) 43 | { 44 | //TODO: Test with Rgba32 and PackedValue property 45 | 46 | using var image1 = await Image.LoadAsync(imagePath1); 47 | using var image2 = await Image.LoadAsync(imagePath2); 48 | 49 | if (image1.Width != image2.Width || image1.Height != image2.Height) 50 | { 51 | Console.WriteLine("Images must be of the same dimensions."); 52 | return 100; 53 | } 54 | 55 | int width = image1.Width; 56 | int height = image1.Height; 57 | int totalPixels = width * height; 58 | double diffSum = 0; 59 | //var differentPixels = new List(); 60 | 61 | for (int x = 0; x < width; x++) 62 | { 63 | for (int y = 0; y < height; y++) 64 | { 65 | var pixel1 = image1[x, y]; 66 | var pixel2 = image2[x, y]; 67 | 68 | var diff = GetDiff(pixel1, pixel2); 69 | if (diff > ignoreDifferentColorPercentLessThan) 70 | diffSum += diff; 71 | //if (diff > 0) 72 | // differentPixels.Add(diff); 73 | } 74 | } 75 | 76 | //Classifying diffs 77 | //var diff5 = differentPixels.FindAll(p => p is <= 5.0); 78 | //var diff10 = differentPixels.FindAll(p => p is <= 10.0); 79 | 80 | //Threshold[] thresholds = 81 | //[ 82 | // new(0 ,5 ,2.0 ), //0 ~ 5 2.0 % 83 | // new(5 ,10 ,1.5 ), //5 ~ 10 1.5 % 84 | // new(10 ,20 ,1.0 ), //10 ~ 20 1.0 % 85 | // new(20 ,30 ,0.7 ), //20 ~ 30 0.7 % 86 | // new(30 ,40 ,0.5 ), //30 ~ 40 0.5 % 87 | // new(40 ,60 ,0.3 ), //40 ~ 60 0.3 % 88 | // new(60 ,80 ,0.2 ), //60 ~ 80 0.2 % 89 | // new(80 ,100 ,0.1 ), //80 ~ 100 0.1 % 90 | //]; 91 | 92 | //foreach (var threshold in thresholds) 93 | //{ 94 | // var diffs = differentPixels.FindAll(p => p > threshold.ColorDiffPercentFrom && p <= threshold.ColorDiffPercentTo); 95 | // threshold.RealPixlePercent = diffs.DefaultIfEmpty().Sum() / totalPixels; 96 | //} 97 | 98 | var differentPixlesPercent = diffSum / totalPixels; 99 | 100 | return differentPixlesPercent; 101 | 102 | static double GetDiff(RgbaVector pixle1, RgbaVector pixle2) 103 | { 104 | var diffR = Math.Abs(pixle1.R - pixle2.R); 105 | var diffG = Math.Abs(pixle1.G - pixle2.G); 106 | var diffB = Math.Abs(pixle1.B - pixle2.B); 107 | var diffA = Math.Abs(pixle1.A - pixle2.A); 108 | 109 | var diffRGB = ((double)diffR + diffG + diffB) / 3; 110 | 111 | return diffRGB * (1 - diffA) * 100; 112 | } 113 | } 114 | 115 | //private class Threshold(double colorDiffPercentFrom, double colorDiffPercentTo, double thresholdPixlePercent) 116 | //{ 117 | // public double ColorDiffPercentFrom => colorDiffPercentFrom; 118 | // public double ColorDiffPercentTo => colorDiffPercentTo; 119 | // public double ThresholdPixlePercent => thresholdPixlePercent; 120 | // public double RealPixlePercent { get; internal set; } 121 | //} 122 | } 123 | -------------------------------------------------------------------------------- /test/BenchmarkDotNetVisualizer.Tests/IterationBenchmarkTests.cs: -------------------------------------------------------------------------------- 1 | namespace BenchmarkDotNetVisualizer.Tests; 2 | 3 | public class IterationBenchmarkTests : TestBase 4 | { 5 | [Fact] 6 | public async Task Joined_PivotBy_Runtime_Dark() 7 | { 8 | var benchmarkInfo = BenchmarkInfo.CreateFromDirectory(_benchmarkArtifactsPath, searchPattern: "IterationBenchmark-report-github.md").First(); 9 | 10 | var options = new JoinReportHtmlOptions 11 | { 12 | Title = "Performance Comparison between for, foreach, and ForEeach() method", 13 | MainColumn = "Method", 14 | GroupByColumns = ["Categories", "Length"], 15 | PivotColumn = "Runtime", 16 | StatisticColumns = ["Mean"], 17 | ColumnsOrder = [".NET Core 3.0", ".NET Core 3.1", ".NET 5.0", ".NET 6.0", ".NET 7.0", ".NET 8.0"], 18 | DividerMode = RenderTableDividerMode.SeparateTables, 19 | HtmlWrapMode = HtmlDocumentWrapMode.RichDataTables, 20 | Theme = Theme.Dark 21 | }; 22 | 23 | var (htmlPath, _) = await JoinReportsAndSaveAsHtmlAndImageAsync(benchmarkInfo, options); 24 | 25 | await VerifyHtml(htmlPath); 26 | } 27 | 28 | [Fact] 29 | public async Task Joined_PivotBy_Runtime_Light() 30 | { 31 | var benchmarkInfo = BenchmarkInfo.CreateFromDirectory(_benchmarkArtifactsPath, searchPattern: "IterationBenchmark-report-github.md").First(); 32 | 33 | var options = new JoinReportHtmlOptions 34 | { 35 | Title = "Performance Comparison between for, foreach, and ForEeach() method", 36 | MainColumn = "Method", 37 | GroupByColumns = ["Categories", "Length"], 38 | PivotColumn = "Runtime", 39 | StatisticColumns = ["Mean"], 40 | ColumnsOrder = [".NET Core 3.0", ".NET Core 3.1", ".NET 5.0", ".NET 6.0", ".NET 7.0", ".NET 8.0"], 41 | DividerMode = RenderTableDividerMode.SeparateTables, 42 | HtmlWrapMode = HtmlDocumentWrapMode.RichDataTables, 43 | Theme = Theme.Light 44 | }; 45 | 46 | var (htmlPath, _) = await JoinReportsAndSaveAsHtmlAndImageAsync(benchmarkInfo, options); 47 | 48 | await VerifyHtml(htmlPath); 49 | } 50 | 51 | [Fact] 52 | public async Task Joined_PivotBy_Method_Dark() 53 | { 54 | var benchmarkInfo = BenchmarkInfo.CreateFromDirectory(_benchmarkArtifactsPath, searchPattern: "IterationBenchmark-report-github.md").First(); 55 | 56 | var options = new JoinReportHtmlOptions 57 | { 58 | Title = "Performance Comparison between for, foreach, and ForEeach() method", 59 | MainColumn = "Runtime", 60 | GroupByColumns = ["Categories", "Length"], 61 | PivotColumn = "Method", 62 | StatisticColumns = ["Mean"], 63 | ColumnsOrder = ["for", "foreach", "ForEach()"], 64 | DividerMode = RenderTableDividerMode.SeparateTables, 65 | HtmlWrapMode = HtmlDocumentWrapMode.RichDataTables, 66 | Theme = Theme.Dark 67 | }; 68 | 69 | var (htmlPath, _) = await JoinReportsAndSaveAsHtmlAndImageAsync(benchmarkInfo, options); 70 | 71 | await VerifyHtml(htmlPath); 72 | } 73 | 74 | [Fact] 75 | public async Task Joined_PivotBy_Method_Light() 76 | { 77 | var benchmarkInfo = BenchmarkInfo.CreateFromDirectory(_benchmarkArtifactsPath, searchPattern: "IterationBenchmark-report-github.md").First(); 78 | 79 | var options = new JoinReportHtmlOptions 80 | { 81 | Title = "Performance Comparison between for, foreach, and ForEeach() method", 82 | MainColumn = "Runtime", 83 | GroupByColumns = ["Categories", "Length"], 84 | PivotColumn = "Method", 85 | StatisticColumns = ["Mean"], 86 | ColumnsOrder = ["for", "foreach", "ForEach()"], 87 | DividerMode = RenderTableDividerMode.SeparateTables, 88 | HtmlWrapMode = HtmlDocumentWrapMode.RichDataTables, 89 | Theme = Theme.Light 90 | }; 91 | 92 | var (htmlPath, _) = await JoinReportsAndSaveAsHtmlAndImageAsync(benchmarkInfo, options); 93 | 94 | await VerifyHtml(htmlPath); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /test/BenchmarkDotNetVisualizer.Tests/JsonSerializersBenchmarkTests.cs: -------------------------------------------------------------------------------- 1 | namespace BenchmarkDotNetVisualizer.Tests; 2 | 3 | public class JsonSerializersBenchmarkTests : TestBase 4 | { 5 | [Fact] 6 | public async Task Simple_Dark() 7 | { 8 | var benchmarkInfo = BenchmarkInfo.CreateFromDirectory(_benchmarkArtifactsPath, searchPattern: "JsonSerializersBenchmark-report-github.md").First(); 9 | 10 | var options = new ReportHtmlOptions 11 | { 12 | Title = "Json Serializers Benchmark", 13 | GroupByColumns = ["Method"], 14 | SpectrumColumns = ["Mean", "Allocated"], 15 | DividerMode = RenderTableDividerMode.EmptyDividerRow, 16 | HtmlWrapMode = HtmlDocumentWrapMode.Simple, 17 | Theme = Theme.Dark 18 | }; 19 | 20 | var (htmlPath, _) = await SaveAsHtmlAndImageAsync(benchmarkInfo, options); 21 | 22 | await VerifyHtml(htmlPath); 23 | } 24 | 25 | [Fact] 26 | public async Task Simple_Light() 27 | { 28 | var benchmarkInfo = BenchmarkInfo.CreateFromDirectory(_benchmarkArtifactsPath, searchPattern: "JsonSerializersBenchmark-report-github.md").First(); 29 | 30 | var options = new ReportHtmlOptions 31 | { 32 | Title = "Json Serializers Benchmark", 33 | GroupByColumns = ["Method"], 34 | SpectrumColumns = ["Mean", "Allocated"], 35 | DividerMode = RenderTableDividerMode.EmptyDividerRow, 36 | HtmlWrapMode = HtmlDocumentWrapMode.Simple, 37 | Theme = Theme.Light 38 | }; 39 | 40 | var (htmlPath, _) = await SaveAsHtmlAndImageAsync(benchmarkInfo, options); 41 | 42 | await VerifyHtml(htmlPath); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /test/BenchmarkDotNetVisualizer.Tests/TestBase.cs: -------------------------------------------------------------------------------- 1 | using BenchmarkDotNetVisualizer.Utilities; 2 | using System.Runtime.CompilerServices; 3 | using VerifyTests.DiffPlex; 4 | 5 | namespace BenchmarkDotNetVisualizer.Tests; 6 | 7 | public abstract class TestBase 8 | { 9 | protected static readonly string _benchmarkArtifactsPath = DirectoryHelper.GetPathRelativeToProjectDirectory("BenchmarkDotNet.Artifacts"); 10 | private static readonly string _snapshotsPath = DirectoryHelper.GetPathRelativeToProjectDirectory("_snapshots"); 11 | private static readonly string _reportsPath = DirectoryHelper.GetPathRelativeToProjectDirectory("_reports"); 12 | 13 | static TestBase() 14 | { 15 | VerifyDiffPlex.Initialize(OutputType.Compact); 16 | DerivePathInfo((_, _, type, method) => new PathInfo(_snapshotsPath, type.Name, method.Name)); 17 | HtmlHelper.EnsureBrowserDownloadedAsync(true).GetAwaiter().GetResult(); 18 | } 19 | 20 | public async Task<(string htmlPath, string imgPath)> JoinReportsAndSaveAsHtmlAndImageAsync(BenchmarkInfo[] benchmarkInfo, JoinReportHtmlOptions options, 21 | [CallerFilePath] string callerFilePath = null!, [CallerMemberName] string callerMethod = null!) 22 | { 23 | var (htmlPath, imgPath) = GetHtmlAndImagePath(callerFilePath, callerMethod); 24 | 25 | await benchmarkInfo.JoinReportsAndSaveAsHtmlAndImageAsync(htmlPath, imgPath, options); 26 | 27 | return (htmlPath, imgPath); 28 | } 29 | 30 | public async Task<(string htmlPath, string imgPath)> JoinReportsAndSaveAsHtmlAndImageAsync(BenchmarkInfo benchmarkInfo, JoinReportHtmlOptions options, 31 | [CallerFilePath] string callerFilePath = null!, [CallerMemberName] string callerMethod = null!) 32 | { 33 | var (htmlPath, imgPath) = GetHtmlAndImagePath(callerFilePath, callerMethod); 34 | 35 | await benchmarkInfo.JoinReportsAndSaveAsHtmlAndImageAsync(htmlPath, imgPath, options); 36 | 37 | return (htmlPath, imgPath); 38 | } 39 | 40 | public async Task<(string htmlPath, string imgPath)> SaveAsHtmlAndImageAsync(BenchmarkInfo benchmarkInfo, ReportHtmlOptions options, 41 | [CallerFilePath] string callerFilePath = null!, [CallerMemberName] string callerMethod = null!) 42 | { 43 | var (htmlPath, imgPath) = GetHtmlAndImagePath(callerFilePath, callerMethod); 44 | 45 | await benchmarkInfo.SaveAsHtmlAndImageAsync(htmlPath, imgPath, options); 46 | 47 | return (htmlPath, imgPath); 48 | } 49 | 50 | #region Verify 51 | public async Task VerifyHtmlAndImage(string htmlPath, string imgPath, 52 | [CallerFilePath] string callerFilePath = null!, [CallerMemberName] string callerMethod = null!) 53 | { 54 | await VerifyHtml(htmlPath); 55 | 56 | await VerifyImage(imgPath, callerFilePath, callerMethod); 57 | } 58 | 59 | public async Task VerifyHtml(string htmlPath) 60 | { 61 | var html = await File.ReadAllTextAsync(htmlPath); 62 | await Verify(html, "html"); 63 | File.Delete(htmlPath); 64 | } 65 | 66 | public async Task VerifyImage(string imgPath, 67 | [CallerFilePath] string callerFilePath = null!, [CallerMemberName] string callerMethod = null!) 68 | { 69 | var methodName = GetMethodName(callerFilePath, callerMethod); 70 | var destPath = Path.Combine(_snapshotsPath, $"{methodName}.png"); 71 | var isEqual = await ImageComparer.IsEqualAsync(imgPath, destPath); 72 | Assert.True(isEqual, "Images are not equal."); 73 | File.Delete(imgPath); 74 | } 75 | #endregion 76 | 77 | #region Utils 78 | private static (string htmlPath, string imgPath) GetHtmlAndImagePath([CallerFilePath] string callerFilePath = null!, [CallerMemberName] string callerMethod = null!) 79 | { 80 | var methodName = GetMethodName(callerFilePath, callerMethod); 81 | 82 | var htmlPath = Path.Combine(_reportsPath, $"{methodName}.html"); 83 | var imgPath = Path.Combine(_reportsPath, $"{methodName}.png"); 84 | 85 | return (htmlPath, imgPath); 86 | } 87 | 88 | private static string GetMethodName(string callerFilePath, string callerMethod) 89 | { 90 | var callerTypeName = Path.GetFileNameWithoutExtension(callerFilePath); 91 | return $"{callerTypeName}.{callerMethod}"; 92 | } 93 | #endregion 94 | } -------------------------------------------------------------------------------- /test/BenchmarkDotNetVisualizer.Tests/_snapshots/DotNetCollectionsBenchmarkTests.Benchmark_Initialize_Allocated.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mjebrahimi/BenchmarkDotNetVisualizer/67fea4b47eebb29b9abd0657f31cb2b3671e5900/test/BenchmarkDotNetVisualizer.Tests/_snapshots/DotNetCollectionsBenchmarkTests.Benchmark_Initialize_Allocated.png -------------------------------------------------------------------------------- /test/BenchmarkDotNetVisualizer.Tests/_snapshots/DotNetCollectionsBenchmarkTests.Benchmark_Initialize_Mean.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mjebrahimi/BenchmarkDotNetVisualizer/67fea4b47eebb29b9abd0657f31cb2b3671e5900/test/BenchmarkDotNetVisualizer.Tests/_snapshots/DotNetCollectionsBenchmarkTests.Benchmark_Initialize_Mean.png -------------------------------------------------------------------------------- /test/BenchmarkDotNetVisualizer.Tests/_snapshots/DotNetCollectionsBenchmarkTests.Benchmark_SearchContains_Allocated.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mjebrahimi/BenchmarkDotNetVisualizer/67fea4b47eebb29b9abd0657f31cb2b3671e5900/test/BenchmarkDotNetVisualizer.Tests/_snapshots/DotNetCollectionsBenchmarkTests.Benchmark_SearchContains_Allocated.png -------------------------------------------------------------------------------- /test/BenchmarkDotNetVisualizer.Tests/_snapshots/DotNetCollectionsBenchmarkTests.Benchmark_SearchContains_Mean.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mjebrahimi/BenchmarkDotNetVisualizer/67fea4b47eebb29b9abd0657f31cb2b3671e5900/test/BenchmarkDotNetVisualizer.Tests/_snapshots/DotNetCollectionsBenchmarkTests.Benchmark_SearchContains_Mean.png -------------------------------------------------------------------------------- /test/BenchmarkDotNetVisualizer.Tests/_snapshots/DotNetCollectionsBenchmarkTests.Benchmark_SearchTryGetValue_Allocated.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mjebrahimi/BenchmarkDotNetVisualizer/67fea4b47eebb29b9abd0657f31cb2b3671e5900/test/BenchmarkDotNetVisualizer.Tests/_snapshots/DotNetCollectionsBenchmarkTests.Benchmark_SearchTryGetValue_Allocated.png -------------------------------------------------------------------------------- /test/BenchmarkDotNetVisualizer.Tests/_snapshots/DotNetCollectionsBenchmarkTests.Benchmark_SearchTryGetValue_Mean.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mjebrahimi/BenchmarkDotNetVisualizer/67fea4b47eebb29b9abd0657f31cb2b3671e5900/test/BenchmarkDotNetVisualizer.Tests/_snapshots/DotNetCollectionsBenchmarkTests.Benchmark_SearchTryGetValue_Mean.png -------------------------------------------------------------------------------- /test/BenchmarkDotNetVisualizer.Tests/_snapshots/IterationBenchmarkTests.Joined_PivotBy_Method_Dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mjebrahimi/BenchmarkDotNetVisualizer/67fea4b47eebb29b9abd0657f31cb2b3671e5900/test/BenchmarkDotNetVisualizer.Tests/_snapshots/IterationBenchmarkTests.Joined_PivotBy_Method_Dark.png -------------------------------------------------------------------------------- /test/BenchmarkDotNetVisualizer.Tests/_snapshots/IterationBenchmarkTests.Joined_PivotBy_Method_Light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mjebrahimi/BenchmarkDotNetVisualizer/67fea4b47eebb29b9abd0657f31cb2b3671e5900/test/BenchmarkDotNetVisualizer.Tests/_snapshots/IterationBenchmarkTests.Joined_PivotBy_Method_Light.png -------------------------------------------------------------------------------- /test/BenchmarkDotNetVisualizer.Tests/_snapshots/IterationBenchmarkTests.Joined_PivotBy_Runtime_Dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mjebrahimi/BenchmarkDotNetVisualizer/67fea4b47eebb29b9abd0657f31cb2b3671e5900/test/BenchmarkDotNetVisualizer.Tests/_snapshots/IterationBenchmarkTests.Joined_PivotBy_Runtime_Dark.png -------------------------------------------------------------------------------- /test/BenchmarkDotNetVisualizer.Tests/_snapshots/IterationBenchmarkTests.Joined_PivotBy_Runtime_Light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mjebrahimi/BenchmarkDotNetVisualizer/67fea4b47eebb29b9abd0657f31cb2b3671e5900/test/BenchmarkDotNetVisualizer.Tests/_snapshots/IterationBenchmarkTests.Joined_PivotBy_Runtime_Light.png -------------------------------------------------------------------------------- /test/BenchmarkDotNetVisualizer.Tests/_snapshots/JsonSerializersBenchmarkTests.Simple_Dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mjebrahimi/BenchmarkDotNetVisualizer/67fea4b47eebb29b9abd0657f31cb2b3671e5900/test/BenchmarkDotNetVisualizer.Tests/_snapshots/JsonSerializersBenchmarkTests.Simple_Dark.png -------------------------------------------------------------------------------- /test/BenchmarkDotNetVisualizer.Tests/_snapshots/JsonSerializersBenchmarkTests.Simple_Light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mjebrahimi/BenchmarkDotNetVisualizer/67fea4b47eebb29b9abd0657f31cb2b3671e5900/test/BenchmarkDotNetVisualizer.Tests/_snapshots/JsonSerializersBenchmarkTests.Simple_Light.png --------------------------------------------------------------------------------