diff --git a/src/BenchmarkDotNet.Exporters.Plotting/ScottPlotExporter.cs b/src/BenchmarkDotNet.Exporters.Plotting/ScottPlotExporter.cs
index 77695d4ae7..5063a3cd33 100644
--- a/src/BenchmarkDotNet.Exporters.Plotting/ScottPlotExporter.cs
+++ b/src/BenchmarkDotNet.Exporters.Plotting/ScottPlotExporter.cs
@@ -8,6 +8,7 @@
using BenchmarkDotNet.Reports;
using ScottPlot;
using ScottPlot.Plottables;
+using ScottPlot.Statistics;
namespace BenchmarkDotNet.Exporters.Plotting
{
@@ -37,6 +38,7 @@ public ScottPlotExporter(int width = 1920, int height = 1080)
this.Height = height;
this.IncludeBarPlot = true;
this.IncludeBoxPlot = true;
+ this.IncludeHistogramPlot = true;
this.RotateLabels = true;
}
@@ -78,6 +80,12 @@ public ScottPlotExporter(int width = 1920, int height = 1080)
///
public bool IncludeBoxPlot { get; set; }
+ ///
+ /// Gets or sets a value indicating whether a histogram plot for time-per-op
+ /// measurement values should be exported.
+ ///
+ public bool IncludeHistogramPlot { get; set; }
+
///
/// Not supported.
///
@@ -141,6 +149,18 @@ where measurement.Is(IterationMode.Workload, IterationStage.Result)
annotations);
}
+ if (this.IncludeHistogramPlot)
+ {
+ // -histogramplot.png
+ yield return CreateHistogramPlot(
+ $"{title} - {benchmarkName}",
+ Path.Combine(summary.ResultsDirectoryPath, $"{title}-{benchmarkName}-histogramplot.png"),
+ "Count",
+ $"Time ({timeUnit})",
+ timeStats,
+ annotations);
+ }
+
/* TODO: Rest of the RPlotExporter plots.
--density.png
--facetTimeline.png
@@ -347,6 +367,60 @@ private string CreateBoxPlot(string title, string fileName, string yLabel, strin
return Path.GetFullPath(fileName);
}
+ // This doesn't support RotateTicks. I figure it's not necessary, since they're not category labels and thus shouldn't be very long
+ private string CreateHistogramPlot(string title, string fileName, string yLabel, string xLabel, IEnumerable data, IReadOnlyList annotations)
+ {
+ Plot plt = new Plot();
+ plt.Title(title, this.TitleFontSize);
+ plt.YLabel(yLabel, this.FontSize);
+ plt.XLabel(xLabel, this.FontSize);
+
+ var palette = new ScottPlot.Palettes.Category10();
+
+ var legendPalette = data.Select(d => d.JobId)
+ .Distinct()
+ .Select((jobId, index) => (jobId, index))
+ .ToDictionary(t => t.jobId, t => palette.GetColor(t.index).WithAlpha(0.5));
+
+ plt.Legend.IsVisible = true;
+ plt.Legend.Alignment = Alignment.UpperRight;
+ plt.Legend.FontSize = this.FontSize;
+ var legend = data.Select(d => d.JobId)
+ .Distinct()
+ .Select((label, index) => new LegendItem()
+ {
+ LabelText = label,
+ FillColor = legendPalette[label]
+ })
+ .ToList();
+
+ plt.Legend.ManualItems.AddRange(legend);
+
+ var jobCount = plt.Legend.ManualItems.Count;
+
+ plt.Axes.Left.TickLabelStyle.FontSize = this.FontSize;
+ plt.Axes.Bottom.MajorTickStyle.Length = 0;
+ plt.Axes.Bottom.TickLabelStyle.FontSize = this.FontSize;
+
+ // 5 is an arbitrary multiplier, this may need to be responsive to the number of points
+ // There are theoretically optimzal binsizes (e.g. the 2 * IQR * N^(-1/3) rule), but they tend to make significant assumptions about the data
+ var binWidth = data.GroupBy(s => s.Target).SelectMany(tg => tg.Select(stats => 5 * (stats.Max - stats.Min) / stats.Values.Count)).Min();
+
+ foreach (var (targetGroup, targetGroupIndex) in data.GroupBy(s => s.Target).Select((targetGroup, index) => (targetGroup, index)))
+ {
+ var hists = targetGroup.Select((job) => (job, hist: Histogram.WithBinSize(binWidth, job.Values)));
+
+ foreach (var (job, hist) in hists) {
+ plt.Add.Histogram(hist, legendPalette[job.JobId]);
+ }
+ }
+
+ plt.PlottableList.AddRange(annotations);
+
+ plt.SavePng(fileName, this.Width, this.Height);
+ return Path.GetFullPath(fileName);
+ }
+
///
/// Provides a list of annotations to put over the data area.
///
diff --git a/tests/BenchmarkDotNet.Exporters.Plotting.Tests/ScottPlotExporterTests.cs b/tests/BenchmarkDotNet.Exporters.Plotting.Tests/ScottPlotExporterTests.cs
index e717b6b84d..01c35fcc93 100644
--- a/tests/BenchmarkDotNet.Exporters.Plotting.Tests/ScottPlotExporterTests.cs
+++ b/tests/BenchmarkDotNet.Exporters.Plotting.Tests/ScottPlotExporterTests.cs
@@ -35,6 +35,7 @@ public void BarPlots(Type benchmarkType)
{
IncludeBarPlot = true,
IncludeBoxPlot = false,
+ IncludeHistogramPlot = false,
};
var summary = MockFactory.CreateSummary(benchmarkType);
var filePaths = exporter.ExportToFiles(summary, logger).ToList();
@@ -57,6 +58,7 @@ public void BoxPlots(Type benchmarkType)
{
IncludeBarPlot = false,
IncludeBoxPlot = true,
+ IncludeHistogramPlot = false,
};
var summary = MockFactory.CreateSummaryWithBiasedDistribution(benchmarkType, 1, 4, 10, 9);
var filePaths = exporter.ExportToFiles(summary, logger).ToList();
@@ -79,6 +81,7 @@ public void BoxPlotsWithOneMeasurement(Type benchmarkType)
{
IncludeBarPlot = false,
IncludeBoxPlot = true,
+ IncludeHistogramPlot = false,
};
var summary = MockFactory.CreateSummaryWithBiasedDistribution(benchmarkType, 1, 4, 10, 1);
var filePaths = exporter.ExportToFiles(summary, logger).ToList();
@@ -90,6 +93,29 @@ public void BoxPlotsWithOneMeasurement(Type benchmarkType)
output.WriteLine(logger.GetLog());
}
+ [Theory]
+ [MemberData(nameof(GetGroupBenchmarkTypes))]
+ public void HistogramPlots(Type benchmarkType)
+ {
+ var logger = new AccumulationLogger();
+ logger.WriteLine("=== " + benchmarkType.Name + " ===");
+
+ var exporter = new ScottPlotExporter()
+ {
+ IncludeBarPlot = false,
+ IncludeBoxPlot = false,
+ IncludeHistogramPlot = true,
+ };
+ var summary = MockFactory.CreateSummaryWithBiasedDistribution(benchmarkType, 1, 4, 10, 9);
+ var filePaths = exporter.ExportToFiles(summary, logger).ToList();
+ Assert.NotEmpty(filePaths);
+ Assert.All(filePaths, f => File.Exists(f));
+
+ foreach (string filePath in filePaths)
+ logger.WriteLine($"* {filePath}");
+ output.WriteLine(logger.GetLog());
+ }
+
[SuppressMessage("ReSharper", "InconsistentNaming")]
public static class BaselinesBenchmarks
{