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