Skip to content

Commit e933bb0

Browse files
Initial ScottPlotExporter with just Bar Plot and Unit Tests (#2560)
* Initial ScottPlotExporter with just Bar Plot and Unit Tests * Simplifying project settings, added missing common.props, adde some documentation for config settings. * Removed redundant warning suppressions * Fix missing public documentation * Removed redundant condition * Update tests/BenchmarkDotNet.Exporters.Plotting.Tests/BenchmarkDotNet.Exporters.Plotting.Tests.csproj --------- Co-authored-by: Tim Cassell <[email protected]>
1 parent 20e2ee7 commit e933bb0

File tree

5 files changed

+555
-0
lines changed

5 files changed

+555
-0
lines changed

BenchmarkDotNet.sln

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BenchmarkDotNet.TestAdapter
5555
EndProject
5656
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BenchmarkDotNet.Diagnostics.dotMemory", "src\BenchmarkDotNet.Diagnostics.dotMemory\BenchmarkDotNet.Diagnostics.dotMemory.csproj", "{2E2283A3-6DA6-4482-8518-99D6D9F689AB}"
5757
EndProject
58+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BenchmarkDotNet.Exporters.Plotting", "src\BenchmarkDotNet.Exporters.Plotting\BenchmarkDotNet.Exporters.Plotting.csproj", "{B92ECCEF-7C27-4012-9E19-679F3C40A6A6}"
59+
EndProject
60+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BenchmarkDotNet.Exporters.Plotting.Tests", "tests\BenchmarkDotNet.Exporters.Plotting.Tests\BenchmarkDotNet.Exporters.Plotting.Tests.csproj", "{199AC83E-30BD-40CD-87CE-0C838AC0320D}"
61+
EndProject
5862
Global
5963
GlobalSection(SolutionConfigurationPlatforms) = preSolution
6064
Debug|Any CPU = Debug|Any CPU
@@ -149,6 +153,14 @@ Global
149153
{2E2283A3-6DA6-4482-8518-99D6D9F689AB}.Debug|Any CPU.Build.0 = Debug|Any CPU
150154
{2E2283A3-6DA6-4482-8518-99D6D9F689AB}.Release|Any CPU.ActiveCfg = Release|Any CPU
151155
{2E2283A3-6DA6-4482-8518-99D6D9F689AB}.Release|Any CPU.Build.0 = Release|Any CPU
156+
{B92ECCEF-7C27-4012-9E19-679F3C40A6A6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
157+
{B92ECCEF-7C27-4012-9E19-679F3C40A6A6}.Debug|Any CPU.Build.0 = Debug|Any CPU
158+
{B92ECCEF-7C27-4012-9E19-679F3C40A6A6}.Release|Any CPU.ActiveCfg = Release|Any CPU
159+
{B92ECCEF-7C27-4012-9E19-679F3C40A6A6}.Release|Any CPU.Build.0 = Release|Any CPU
160+
{199AC83E-30BD-40CD-87CE-0C838AC0320D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
161+
{199AC83E-30BD-40CD-87CE-0C838AC0320D}.Debug|Any CPU.Build.0 = Debug|Any CPU
162+
{199AC83E-30BD-40CD-87CE-0C838AC0320D}.Release|Any CPU.ActiveCfg = Release|Any CPU
163+
{199AC83E-30BD-40CD-87CE-0C838AC0320D}.Release|Any CPU.Build.0 = Release|Any CPU
152164
EndGlobalSection
153165
GlobalSection(SolutionProperties) = preSolution
154166
HideSolutionNode = FALSE
@@ -176,6 +188,8 @@ Global
176188
{AACA2C63-A85B-47AB-99FC-72C3FF408B14} = {14195214-591A-45B7-851A-19D3BA2413F9}
177189
{4C9C89B8-7C4E-4ECF-B3C9-324C8772EDAC} = {D6597E3A-6892-4A68-8E14-042FC941FDA2}
178190
{2E2283A3-6DA6-4482-8518-99D6D9F689AB} = {D6597E3A-6892-4A68-8E14-042FC941FDA2}
191+
{B92ECCEF-7C27-4012-9E19-679F3C40A6A6} = {D6597E3A-6892-4A68-8E14-042FC941FDA2}
192+
{199AC83E-30BD-40CD-87CE-0C838AC0320D} = {14195214-591A-45B7-851A-19D3BA2413F9}
179193
EndGlobalSection
180194
GlobalSection(ExtensibilityGlobals) = postSolution
181195
SolutionGuid = {4D9AF12B-1F7F-45A7-9E8C-E4E46ADCBD1F}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
<Import Project="..\..\build\common.props" />
3+
<PropertyGroup>
4+
<AssemblyTitle>BenchmarkDotNet plotting export support.</AssemblyTitle>
5+
<TargetFrameworks>netstandard2.0</TargetFrameworks>
6+
<AssemblyName>BenchmarkDotNet.Exporters.Plotting</AssemblyName>
7+
<PackageId>BenchmarkDotNet.Exporters.Plotting</PackageId>
8+
<RootNamespace>BenchmarkDotNet.Exporters.Plotting</RootNamespace>
9+
<!-- needed for docfx xref resolver -->
10+
<ProduceReferenceAssembly>True</ProduceReferenceAssembly>
11+
<Nullable>enable</Nullable>
12+
</PropertyGroup>
13+
<ItemGroup>
14+
<ProjectReference Include="..\BenchmarkDotNet\BenchmarkDotNet.csproj" />
15+
</ItemGroup>
16+
<ItemGroup>
17+
<PackageReference Include="ScottPlot" Version="5.0.25" />
18+
</ItemGroup>
19+
</Project>
Lines changed: 256 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,256 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.IO;
4+
using System.Linq;
5+
using BenchmarkDotNet.Loggers;
6+
using BenchmarkDotNet.Properties;
7+
using BenchmarkDotNet.Reports;
8+
using ScottPlot;
9+
using ScottPlot.Plottables;
10+
11+
namespace BenchmarkDotNet.Exporters.Plotting
12+
{
13+
/// <summary>
14+
/// Provides plot exports as .png files.
15+
/// </summary>
16+
public class ScottPlotExporter : IExporter
17+
{
18+
/// <summary>
19+
/// Default instance of the exporter with default configuration.
20+
/// </summary>
21+
public static readonly IExporter Default = new ScottPlotExporter();
22+
23+
/// <summary>
24+
/// Gets the name of the Exporter type.
25+
/// </summary>
26+
public string Name => nameof(ScottPlotExporter);
27+
28+
/// <summary>
29+
/// Initializes a new instance of ScottPlotExporter.
30+
/// </summary>
31+
/// <param name="width">The width of all plots in pixels (optional). Defaults to 1920.</param>
32+
/// <param name="height">The height of all plots in pixels (optional). Defaults to 1080.</param>
33+
public ScottPlotExporter(int width = 1920, int height = 1080)
34+
{
35+
this.Width = width;
36+
this.Height = height;
37+
this.IncludeBarPlot = true;
38+
this.RotateLabels = true;
39+
}
40+
41+
/// <summary>
42+
/// Gets or sets the width of all plots in pixels.
43+
/// </summary>
44+
public int Width { get; set; }
45+
46+
/// <summary>
47+
/// Gets or sets the height of all plots in pixels.
48+
/// </summary>
49+
public int Height { get; set; }
50+
51+
/// <summary>
52+
/// Gets or sets a value indicating whether labels for Plot X-axis should be rotated.
53+
/// This allows for longer labels at the expense of chart height.
54+
/// </summary>
55+
public bool RotateLabels { get; set; }
56+
57+
/// <summary>
58+
/// Gets or sets a value indicating whether a bar plot for time-per-op
59+
/// measurement values should be exported.
60+
/// </summary>
61+
public bool IncludeBarPlot { get; set; }
62+
63+
/// <summary>
64+
/// Not supported.
65+
/// </summary>
66+
/// <param name="summary">This parameter is not used.</param>
67+
/// <param name="logger">This parameter is not used.</param>
68+
/// <exception cref="NotSupportedException"></exception>
69+
public void ExportToLog(Summary summary, ILogger logger)
70+
{
71+
throw new NotSupportedException();
72+
}
73+
74+
/// <summary>
75+
/// Exports plots to .png file.
76+
/// </summary>
77+
/// <param name="summary">The summary to be exported.</param>
78+
/// <param name="consoleLogger">Logger to output to.</param>
79+
/// <returns>The file paths of every plot exported.</returns>
80+
public IEnumerable<string> ExportToFiles(Summary summary, ILogger consoleLogger)
81+
{
82+
var title = summary.Title;
83+
var version = BenchmarkDotNetInfo.Instance.BrandTitle;
84+
var annotations = GetAnnotations(version);
85+
86+
var (timeUnit, timeScale) = GetTimeUnit(summary.Reports.SelectMany(m => m.AllMeasurements));
87+
88+
foreach (var benchmark in summary.Reports.GroupBy(r => r.BenchmarkCase.Descriptor.Type.Name))
89+
{
90+
var benchmarkName = benchmark.Key;
91+
92+
// Get the measurement nanoseconds per op, divided by time scale, grouped by target and Job [param].
93+
var timeStats = from report in benchmark
94+
let jobId = report.BenchmarkCase.DisplayInfo.Replace(report.BenchmarkCase.Descriptor.DisplayInfo + ": ", string.Empty)
95+
from measurement in report.AllMeasurements
96+
let measurementValue = measurement.Nanoseconds / measurement.Operations
97+
group measurementValue / timeScale by (Target: report.BenchmarkCase.Descriptor.WorkloadMethodDisplayInfo, JobId: jobId) into g
98+
select (g.Key.Target, g.Key.JobId, Mean: g.Average(), StdError: StandardError(g.ToList()));
99+
100+
if (this.IncludeBarPlot)
101+
{
102+
// <BenchmarkName>-barplot.png
103+
yield return CreateBarPlot(
104+
$"{title} - {benchmarkName}",
105+
Path.Combine(summary.ResultsDirectoryPath, $"{title}-{benchmarkName}-barplot.png"),
106+
$"Time ({timeUnit})",
107+
"Target",
108+
timeStats,
109+
annotations);
110+
}
111+
112+
/* TODO: Rest of the RPlotExporter plots.
113+
<BenchmarkName>-boxplot.png
114+
<BenchmarkName>-<MethodName>-density.png
115+
<BenchmarkName>-<MethodName>-facetTimeline.png
116+
<BenchmarkName>-<MethodName>-facetTimelineSmooth.png
117+
<BenchmarkName>-<MethodName>-<JobName>-timelineSmooth.png
118+
<BenchmarkName>-<MethodName>-<JobName>-timelineSmooth.png*/
119+
}
120+
}
121+
122+
/// <summary>
123+
/// Calculate Standard Deviation.
124+
/// </summary>
125+
/// <param name="values">Values to calculate from.</param>
126+
/// <returns>Standard deviation of values.</returns>
127+
private static double StandardError(IReadOnlyList<double> values)
128+
{
129+
double average = values.Average();
130+
double sumOfSquaresOfDifferences = values.Select(val => (val - average) * (val - average)).Sum();
131+
double standardDeviation = Math.Sqrt(sumOfSquaresOfDifferences / values.Count);
132+
return standardDeviation / Math.Sqrt(values.Count);
133+
}
134+
135+
/// <summary>
136+
/// Gets the lowest appropriate time scale across all measurements.
137+
/// </summary>
138+
/// <param name="values">All measurements</param>
139+
/// <returns>A unit and scaling factor to convert from nanoseconds.</returns>
140+
private (string Unit, double ScaleFactor) GetTimeUnit(IEnumerable<Measurement> values)
141+
{
142+
var minValue = values.Select(m => m.Nanoseconds / m.Operations).DefaultIfEmpty(0d).Min();
143+
if (minValue > 1000000000d)
144+
{
145+
return ("sec", 1000000000d);
146+
}
147+
148+
if (minValue > 1000000d)
149+
{
150+
return ("ms", 1000000d);
151+
}
152+
153+
if (minValue > 1000d)
154+
{
155+
return ("us", 1000d);
156+
}
157+
158+
return ("ns", 1d);
159+
}
160+
161+
private string CreateBarPlot(string title, string fileName, string yLabel, string xLabel, IEnumerable<(string Target, string JobId, double Mean, double StdError)> data, IReadOnlyList<Annotation> annotations)
162+
{
163+
Plot plt = new Plot();
164+
plt.Title(title, 28);
165+
plt.YLabel(yLabel);
166+
plt.XLabel(xLabel);
167+
168+
var palette = new ScottPlot.Palettes.Category10();
169+
170+
var legendPalette = data.Select(d => d.JobId)
171+
.Distinct()
172+
.Select((jobId, index) => (jobId, index))
173+
.ToDictionary(t => t.jobId, t => palette.GetColor(t.index));
174+
175+
plt.Legend.IsVisible = true;
176+
plt.Legend.Location = Alignment.UpperRight;
177+
var legend = data.Select(d => d.JobId)
178+
.Distinct()
179+
.Select((label, index) => new LegendItem()
180+
{
181+
Label = label,
182+
FillColor = legendPalette[label]
183+
})
184+
.ToList();
185+
186+
plt.Legend.ManualItems.AddRange(legend);
187+
188+
var jobCount = plt.Legend.ManualItems.Count;
189+
var ticks = data
190+
.Select((d, index) => new Tick(index, d.Target))
191+
.ToArray();
192+
plt.Axes.Bottom.TickGenerator = new ScottPlot.TickGenerators.NumericManual(ticks);
193+
plt.Axes.Bottom.MajorTickStyle.Length = 0;
194+
195+
if (this.RotateLabels)
196+
{
197+
plt.Axes.Bottom.TickLabelStyle.Rotation = 45;
198+
plt.Axes.Bottom.TickLabelStyle.Alignment = Alignment.MiddleLeft;
199+
200+
// determine the width of the largest tick label
201+
float largestLabelWidth = 0;
202+
foreach (Tick tick in ticks)
203+
{
204+
PixelSize size = plt.Axes.Bottom.TickLabelStyle.Measure(tick.Label);
205+
largestLabelWidth = Math.Max(largestLabelWidth, size.Width);
206+
}
207+
208+
// ensure axis panels do not get smaller than the largest label
209+
plt.Axes.Bottom.MinimumSize = largestLabelWidth;
210+
plt.Axes.Right.MinimumSize = largestLabelWidth;
211+
}
212+
213+
var bars = data
214+
.Select((d, index) => new Bar()
215+
{
216+
Position = ticks[index].Position,
217+
Value = d.Mean,
218+
Error = d.StdError,
219+
FillColor = legendPalette[d.JobId]
220+
});
221+
plt.Add.Bars(bars);
222+
223+
// Tell the plot to autoscale with no padding beneath the bars
224+
plt.Axes.Margins(bottom: 0, right: .2);
225+
226+
plt.PlottableList.AddRange(annotations);
227+
228+
plt.SavePng(fileName, this.Width, this.Height);
229+
return Path.GetFullPath(fileName);
230+
}
231+
232+
/// <summary>
233+
/// Provides a list of annotations to put over the data area.
234+
/// </summary>
235+
/// <param name="version">The version to be displayed.</param>
236+
/// <returns>A list of annotations for every plot.</returns>
237+
private IReadOnlyList<Annotation> GetAnnotations(string version)
238+
{
239+
var versionAnnotation = new Annotation()
240+
{
241+
Label =
242+
{
243+
Text = version,
244+
FontSize = 14,
245+
ForeColor = new Color(0, 0, 0, 100)
246+
},
247+
OffsetY = 10,
248+
OffsetX = 20,
249+
Alignment = Alignment.LowerRight
250+
};
251+
252+
253+
return new[] { versionAnnotation };
254+
}
255+
}
256+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
<Import Project="..\..\build\common.props" />
3+
<PropertyGroup>
4+
<TargetFrameworks>net8.0;net462</TargetFrameworks>
5+
<IsPackable>false</IsPackable>
6+
<IsTestProject>true</IsTestProject>
7+
<GenerateRuntimeConfigurationFiles>true</GenerateRuntimeConfigurationFiles>
8+
<GenerateDocumentationFile>false</GenerateDocumentationFile>
9+
</PropertyGroup>
10+
<ItemGroup Condition=" '$(TargetFrameworkIdentifier)' == '.NETFramework' ">
11+
<PackageReference Include="Microsoft.NETCore.Platforms" Version="6.0.0" />
12+
</ItemGroup>
13+
<ItemGroup>
14+
<ProjectReference Include="..\..\src\BenchmarkDotNet.Exporters.Plotting\BenchmarkDotNet.Exporters.Plotting.csproj" />
15+
<ProjectReference Include="..\..\src\BenchmarkDotNet\BenchmarkDotNet.csproj" />
16+
<ProjectReference Include="..\BenchmarkDotNet.Tests\BenchmarkDotNet.Tests.csproj" />
17+
</ItemGroup>
18+
<ItemGroup>
19+
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
20+
<PackageReference Include="xunit" Version="2.6.2" />
21+
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.4">
22+
<PrivateAssets>all</PrivateAssets>
23+
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
24+
</PackageReference>
25+
</ItemGroup>
26+
</Project>

0 commit comments

Comments
 (0)