Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Exporters.Plotting: ScottPlot histogram export #2711

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 74 additions & 0 deletions src/BenchmarkDotNet.Exporters.Plotting/ScottPlotExporter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using BenchmarkDotNet.Reports;
using ScottPlot;
using ScottPlot.Plottables;
using ScottPlot.Statistics;

namespace BenchmarkDotNet.Exporters.Plotting
{
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -78,6 +80,12 @@ public ScottPlotExporter(int width = 1920, int height = 1080)
/// </summary>
public bool IncludeBoxPlot { get; set; }

/// <summary>
/// Gets or sets a value indicating whether a histogram plot for time-per-op
/// measurement values should be exported.
/// </summary>
public bool IncludeHistogramPlot { get; set; }

/// <summary>
/// Not supported.
/// </summary>
Expand Down Expand Up @@ -141,6 +149,18 @@ where measurement.Is(IterationMode.Workload, IterationStage.Result)
annotations);
}

if (this.IncludeHistogramPlot)
{
// <BenchmarkName>-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.
<BenchmarkName>-<MethodName>-density.png
<BenchmarkName>-<MethodName>-facetTimeline.png
Expand Down Expand Up @@ -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<ChartStats> data, IReadOnlyList<Annotation> 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);
}

/// <summary>
/// Provides a list of annotations to put over the data area.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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();
Expand All @@ -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();
Expand All @@ -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
{
Expand Down