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+ }
0 commit comments