1717
1818final readonly class MarkdownRenderer implements RendererInterface
1919{
20+ private const array COMPACT_HEADERS = ['Benchmark ' , 'Set ' , 'Mem. Peak ' , 'Time ' , 'Variability ' ];
21+
22+ private const array COMPACT_SOURCE_COLUMNS = ['benchmark ' , 'subject ' , 'set ' , 'mem_peak ' , 'mode ' , 'rstdev ' ];
23+
24+ private const int COMPACT_TIME_COLUMN_INDEX = 3 ;
25+
2026 public function __construct (
2127 private OutputInterface $ output ,
2228 private Printer $ printer ,
2329 ) {}
2430
2531 public function render (Reports $ reports , Config $ config ): void
2632 {
27- $ content = $ this ->renderContent ($ reports );
33+ $ content = $ this ->renderContent ($ reports, $ this -> resolveOutlierMinDiff ( $ config ) );
2834 $ file = $ config ['file ' ];
2935
3036 if ($ file === null ) {
@@ -40,22 +46,24 @@ public function configure(OptionsResolver $options): void
4046 {
4147 $ options ->setDefaults ([
4248 'file ' => null ,
49+ 'outlier_min_diff ' => null ,
4350 ]);
4451 $ options ->setAllowedTypes ('file ' , ['null ' , 'string ' ]);
52+ $ options ->setAllowedTypes ('outlier_min_diff ' , ['null ' , 'float ' , 'int ' ]);
4553 }
4654
47- private function renderContent (Reports $ reports ): string
55+ private function renderContent (Reports $ reports, ? float $ outlierMinDiff ): string
4856 {
4957 $ lines = [];
5058
5159 foreach ($ reports ->tables () as $ table ) {
52- array_push ($ lines , ...$ this ->renderTable ($ table ));
60+ array_push ($ lines , ...$ this ->renderTable ($ table, $ outlierMinDiff ));
5361 }
5462
5563 return implode ("\n" , $ lines ) . "\n" ;
5664 }
5765
58- private function renderTable (Table $ table ): array
66+ private function renderTable (Table $ table, ? float $ outlierMinDiff ): array
5967 {
6068 $ lines = [];
6169 $ title = $ table ->title ();
@@ -71,11 +79,22 @@ private function renderTable(Table $table): array
7179 return $ lines ;
7280 }
7381
82+ $ rows = array_map ($ this ->renderTableRow (...), $ table ->rows ());
83+ [$ columns , $ rows , $ isCompactTable ] = $ this ->compactAggregateReportTable ($ columns , $ rows );
84+ $ rows = $ this ->filterOutlierRows ($ rows , $ outlierMinDiff , $ isCompactTable );
85+
86+ if ($ rows === [] && $ isCompactTable && $ outlierMinDiff !== null && $ outlierMinDiff > 0.0 ) {
87+ $ lines [] = sprintf ('_No benchmark changes above ±%s%%._ ' , $ this ->formatPercentage ($ outlierMinDiff ));
88+ $ lines [] = '' ;
89+
90+ return $ lines ;
91+ }
92+
7493 $ lines [] = $ this ->renderRow ($ columns );
7594 $ lines [] = $ this ->renderSeparatorRow ($ columns );
7695
77- foreach ($ table as $ row ) {
78- $ lines [] = $ this ->renderDataRow ($ row );
96+ foreach ($ rows as $ row ) {
97+ $ lines [] = $ this ->renderRow ($ row );
7998 }
8099
81100 $ lines [] = '' ;
@@ -96,11 +115,92 @@ private function renderSeparatorRow(array $columns): string
96115 ));
97116 }
98117
99- private function renderDataRow (TableRow $ row ): string
118+ private function renderTableRow (TableRow $ row ): array
119+ {
120+ return array_values (array_map ($ this ->formatCell (...), iterator_to_array ($ row )));
121+ }
122+
123+ private function compactAggregateReportTable (array $ columns , array $ rows ): array
100124 {
101- $ cells = array_map ($ this ->formatCell (...), iterator_to_array ($ row ));
125+ $ columnIndexes = $ this ->resolveCompactSourceColumnIndexes ($ columns );
126+
127+ if ($ columnIndexes === null ) {
128+ return [$ columns , $ rows , false ];
129+ }
130+
131+ $ rows = array_map (function (array $ row ) use ($ columnIndexes ): array {
132+ $ set = trim ((string ) $ row [$ columnIndexes ['set ' ]]);
133+
134+ return [
135+ sprintf ('%s(%s) ' , $ row [$ columnIndexes ['benchmark ' ]], $ row [$ columnIndexes ['subject ' ]]),
136+ $ set === '' ? '- ' : $ set ,
137+ $ row [$ columnIndexes ['mem_peak ' ]],
138+ $ row [$ columnIndexes ['mode ' ]],
139+ $ row [$ columnIndexes ['rstdev ' ]],
140+ ];
141+ }, $ rows );
142+
143+ return [self ::COMPACT_HEADERS , $ rows , true ];
144+ }
145+
146+ private function filterOutlierRows (array $ rows , ?float $ outlierMinDiff , bool $ isCompactTable ): array
147+ {
148+ if (! $ isCompactTable || $ outlierMinDiff === null || $ outlierMinDiff <= 0.0 ) {
149+ return $ rows ;
150+ }
151+
152+ return array_values (array_filter ($ rows , function (array $ row ) use ($ outlierMinDiff ): bool {
153+ $ diff = $ this ->extractTrailingPercentage ($ row [self ::COMPACT_TIME_COLUMN_INDEX ]);
154+
155+ if ($ diff === null ) {
156+ return true ;
157+ }
158+
159+ return abs ($ diff ) >= $ outlierMinDiff ;
160+ }));
161+ }
162+
163+ private function extractTrailingPercentage (string $ cell ): ?float
164+ {
165+ if (preg_match ('/([+-]?\d+(?:\.\d+)?)%\s*$/ ' , $ cell , $ matches ) !== 1 ) {
166+ return null ;
167+ }
168+
169+ return (float ) $ matches [1 ];
170+ }
171+
172+ private function resolveOutlierMinDiff (Config $ config ): ?float
173+ {
174+ if (! $ config ->offsetExists ('outlier_min_diff ' )) {
175+ return null ;
176+ }
177+
178+ $ value = $ config ['outlier_min_diff ' ];
179+
180+ return $ value === null ? null : (float ) $ value ;
181+ }
182+
183+ private function formatPercentage (float $ value ): string
184+ {
185+ return rtrim (rtrim (sprintf ('%.2f ' , $ value ), '0 ' ), '. ' );
186+ }
187+
188+ private function resolveCompactSourceColumnIndexes (array $ columns ): ?array
189+ {
190+ $ columnIndexes = array_flip ($ columns );
191+
192+ if (array_any (self ::COMPACT_SOURCE_COLUMNS , fn ($ column ) => ! array_key_exists ($ column , $ columnIndexes ))) {
193+ return null ;
194+ }
102195
103- return $ this ->renderRow ($ cells );
196+ return [
197+ 'benchmark ' => $ columnIndexes ['benchmark ' ],
198+ 'subject ' => $ columnIndexes ['subject ' ],
199+ 'set ' => $ columnIndexes ['set ' ],
200+ 'mem_peak ' => $ columnIndexes ['mem_peak ' ],
201+ 'mode ' => $ columnIndexes ['mode ' ],
202+ 'rstdev ' => $ columnIndexes ['rstdev ' ],
203+ ];
104204 }
105205
106206 private function formatCell (Node $ node ): string
0 commit comments