1 /*
2 *             Copyright Lodovico Giaretta 2016 - .
3 *  Distributed under the Boost Software License, Version 1.0.
4 *      (See accompanying file LICENSE_1_0.txt or copy at
5 *            http://www.boost.org/LICENSE_1_0.txt)
6 */
7 
8 module random_benchmark;
9 
10 import genxml;
11 import std.experimental.statistical;
12 
13 import std.experimental.xml;
14 
15 import std.array;
16 import std.stdio;
17 import std.file;
18 import std.path: buildPath, exists;
19 import std.conv;
20 import core.time: Duration, nsecs;
21 
22 // BENCHMARK CONFIGURATIONS: TWEAK AS NEEDED
23 
24 enum BenchmarkConfig theBenchmark = {
25     components: [
26         ComponentConfig("Memory_bound", "to!string", "iterateString"),
27         ComponentConfig("Parser_BufferedLexer_1K", "makeDumbBufferedReader!1024", "parserTest!(BufferedLexer, DumbBufferedReader)"),
28         ComponentConfig("Parser_BufferedLexer_64K", "makeDumbBufferedReader!65536", "parserTest!(BufferedLexer, DumbBufferedReader)"),
29         ComponentConfig("Parser_SliceLexer", "to!string", "parserTest!SliceLexer"),
30         ComponentConfig("Parser_ForwardLexer", "to!string", "parserTest!ForwardLexer"),
31         ComponentConfig("Parser_RangeLexer", "to!string", "parserTest!RangeLexer"),
32         ComponentConfig("Cursor_BufferedLexer_1K", "makeDumbBufferedReader!1024", "cursorTest!(BufferedLexer, DumbBufferedReader)"),
33         ComponentConfig("Cursor_BufferedLexer_64K", "makeDumbBufferedReader!65536", "cursorTest!(BufferedLexer, DumbBufferedReader)"),
34         ComponentConfig("Cursor_SliceLexer", "to!string", "cursorTest!SliceLexer"),
35         ComponentConfig("Cursor_ForwardLexer", "to!string", "cursorTest!ForwardLexer"),
36         ComponentConfig("Cursor_RangeLexer", "to!string", "cursorTest!RangeLexer"),
37         ComponentConfig("Legacy_SAX_API", "to!string", "oldSAXTest!\"std.xml\""),
38         ComponentConfig("Legacy_SAX_Emulator", "to!string","oldSAXTest!\"std.experimental.xml.legacy\""),
39         ComponentConfig("Legacy_DOM_API", "to!string", "buildOldDOM"),
40         ComponentConfig("DOMBuilder_SliceLexer", "to!string", "buildDOM"),
41     ],
42     configurations: [
43         K100,
44         M1,
45         M10,
46         M100,
47     ],
48     filesPerConfig: 3,
49     runsPerFile: 5,
50 };
51 
52 enum GenXmlConfig M100 = { name: "M100",
53                            minDepth:         6,
54                            maxDepth:        14,
55                            minChilds:        3,
56                            maxChilds:        9,
57                            minAttributeNum:  0,
58                            maxAttributeNum:  5};
59 
60 enum GenXmlConfig M10 = { name: "M10",
61                           minDepth:         5,
62                           maxDepth:        13,
63                           minChilds:        3,
64                           maxChilds:        8,
65                           minAttributeNum:  0,
66                           maxAttributeNum:  4};
67 
68 enum GenXmlConfig M1 = { name: "M1",
69                          minDepth:         5,
70                          maxDepth:        12,
71                          minChilds:        4,
72                          maxChilds:        7,
73                          minAttributeNum:  1,
74                          maxAttributeNum:  4};
75 
76 enum GenXmlConfig K100 = { name: "K100",
77                            minDepth:         4,
78                            maxDepth:        11,
79                            minChilds:        3,
80                            maxChilds:        6,
81                            minAttributeNum:  1,
82                            maxAttributeNum:  3};
83 
84 // FUNCTIONS USED FOR TESTING
85 
86 struct DumbBufferedReader
87 {
88     string content;
89     size_t chunk_size;
90 
91     void popFront() @nogc
92     {
93         if (content.length > chunk_size)
94             content = content[chunk_size..$];
95         else
96             content = [];
97     }
98     string front() const @nogc
99     {
100         if (content.length >= chunk_size)
101             return content[0..chunk_size];
102         else
103             return content[0..$];
104     }
105     bool empty() const @nogc
106     {
107         return !content.length;
108     }
109 }
110 
111 DumbBufferedReader makeDumbBufferedReader(size_t bufferSize)(string s)
112 {
113     return DumbBufferedReader(s, bufferSize);
114 }
115 
116 void parserTest(alias Lexer, T = string)(T data)
117 {
118     auto parser = Parser!(Lexer!(T, void delegate()), void delegate())();
119     parser.setSource(data);
120     foreach(e; parser)
121     {
122         doNotOptimize(e);
123     }
124 }
125 
126 void cursorTest(alias Lexer, T = string)(T data)
127 {
128     auto cursor = Cursor!(Parser!(Lexer!(T, void delegate()), void delegate()))();
129     cursor.setSource(data);
130     inspectOneLevel(cursor);
131 }
132 void inspectOneLevel(T)(ref T cursor)
133 {
134     do
135     {
136         foreach(attr; cursor.attributes)
137             doNotOptimize(attr);
138 
139         if (cursor.enter)
140         {
141             inspectOneLevel(cursor);
142             cursor.exit();
143         }
144     }
145     while (cursor.next());
146 }
147 
148 void oldSAXTest(string api)(string data)
149 {
150     mixin("import " ~ api ~ ";\n");
151 
152     void recursiveParse(ElementParser parser)
153     {
154         doNotOptimize(cast(Tag)(parser.tag));
155         parser.onStartTag[null] = &recursiveParse;
156         parser.parse;
157     }
158 
159     DocumentParser parser = new DocumentParser(data);
160     recursiveParse(parser);
161 }
162 
163 void buildOldDOM(string data)
164 {
165     import std.xml;
166     doNotOptimize(new Document(data));
167 }
168 
169 void buildDOM(string data)
170 {
171     auto builder =
172          data
173         .parser
174         .cursor
175         .domBuilder;
176     builder.setSource(data);
177 
178     builder.buildRecursive;
179     doNotOptimize(builder.getDocument);
180 }
181 
182 void iterateString(string data)
183 {
184     foreach (i; 0..data.length)
185         doNotOptimize(data[i]);
186 }
187 
188 void doNotOptimize(T)(auto ref T result)
189 {
190     import std.process: thisProcessID;
191     if (thisProcessID == 1)
192         writeln(result);
193 }
194 
195 // MAIN TEST DRIVER
196 void main(string[] args)
197 {
198     void delegate(FileStats[string], ComponentResults[string]) printFunction;
199     if (args.length > 1)
200     {
201         switch(args[1])
202         {
203             case "csv":
204                 printFunction = (stats, results) { printResultsCSV(theBenchmark, results); };
205                 break;
206             case "pretty":
207                 printFunction = (stats, results)
208                 {
209                     printResultsByConfiguration(theBenchmark, stats, results);
210                     printResultsSpeedSummary(theBenchmark, results);
211                     writeln("\n\n If you are watching this on a terminal, you are encouraged to redirect the standard output to a file instead.\n");
212                 };
213                 break;
214             default:
215                 stderr.writeln("Error: output format ", args[1], "is not supported!");
216                 return;
217         }
218     }
219     else
220     {
221         printFunction = (stats, results)
222         {
223             printResultsByConfiguration(theBenchmark, stats, results);
224             printResultsSpeedSummary(theBenchmark, results);
225             writeln("\n\n If you are watching this on a terminal, you are encouraged to redirect the standard output to a file instead.\n");
226         };
227     }
228     stderr.writeln("Generating test files...");
229     auto stats = generateTestFiles(theBenchmark);
230     stderr.writeln("\nPerforming tests...");
231     auto results = performBenchmark!theBenchmark;
232     stderr.writeln();
233     printFunction(stats, results);
234 }
235 
236 // STRUCTURES HOLDING PARAMETERS AND RESULTS
237 
238 struct BenchmarkConfig
239 {
240     uint runsPerFile;
241     uint filesPerConfig;
242     ComponentConfig[] components;
243     GenXmlConfig[] configurations;
244 }
245 
246 struct ComponentConfig
247 {
248     string name;
249     string inputFunction;
250     string benchmarkFunction;
251 }
252 
253 struct ComponentResults
254 {
255     PreciseStatisticData!double speedStat;
256     ConfigResults[string] configResults;
257 }
258 
259 struct ConfigResults
260 {
261     PreciseStatisticData!double speedStat;
262     FileResults[string] fileResults;
263 }
264 
265 struct FileResults
266 {
267     PreciseStatisticData!(((long x) => nsecs(x)), (Duration d) => d.total!"nsecs") timeStat;
268     PreciseStatisticData!double speedStat;
269     Duration[] times;
270     double[] speeds;
271 }
272 
273 // CODE FOR TESTING
274 
275 mixin template BenchmarkFunctions(ComponentConfig[] comps, size_t pos = 0)
276 {
277     mixin("auto " ~ comps[pos].name ~ "_BenchmarkFunction(string data) {" ~
278             "import core.time: MonoTime;" ~
279             "auto input = " ~ comps[pos].inputFunction ~ "(data);" ~
280             "MonoTime before = MonoTime.currTime;"
281             ~ comps[pos].benchmarkFunction ~ "(input);" ~
282             "MonoTime after = MonoTime.currTime;" ~
283             "return after - before;" ~
284             "}"
285         );
286 
287     static if (pos + 1 < comps.length)
288         mixin BenchmarkFunctions!(comps, pos + 1);
289 }
290 
291 auto performBenchmark(BenchmarkConfig benchmark)()
292 {
293     import std.meta;
294 
295     enum ComponentConfig[] theComponents = theBenchmark.components;
296     mixin BenchmarkFunctions!(theComponents);
297 
298     total_tests = benchmark.runsPerFile * benchmark.filesPerConfig * benchmark.configurations.length * benchmark.components.length;
299     ComponentResults[string] results;
300     foreach(component; aliasSeqOf!(theComponents))
301         results[component.name] = testComponent(benchmark, mixin("&" ~ component.name ~ "_BenchmarkFunction"));
302 
303     return results;
304 }
305 
306 auto testComponent(BenchmarkConfig benchmark, Duration delegate(string) fun)
307 {
308     import std.algorithm: map, joiner;
309 
310     ComponentResults results;
311     foreach (config; benchmark.configurations)
312         results.configResults[config.name] = testConfiguration(config.name, benchmark.filesPerConfig, benchmark.runsPerFile, fun);
313 
314     results.speedStat = PreciseStatisticData!double(results.configResults.byValue.map!"a.fileResults.byValue".joiner.map!"a.speeds".joiner);
315     return results;
316 }
317 
318 auto testConfiguration(string config, uint files, uint runs, Duration delegate(string) fun)
319 {
320     import std.algorithm: map, joiner;
321 
322     ConfigResults results;
323     foreach (filename; getConfigFiles(config, files))
324         results.fileResults[filename] = testFile(filename, runs, fun);
325 
326     results.speedStat = PreciseStatisticData!double(results.fileResults.byValue.map!"a.speeds".joiner);
327     return results;
328 }
329 
330 ulong total_tests;
331 ulong performed_tests;
332 auto testFile(string name, uint runs, Duration delegate(string) fun)
333 {
334     import core.memory;
335 
336     FileResults results;
337     auto input = readText(name);
338     foreach (run; 0..runs)
339     {
340         GC.disable;
341         auto time = fun(input);
342         GC.enable;
343 
344         results.times ~= time;
345         results.speeds ~= (cast(double)getSize(name)) / time.total!"usecs";
346         stderr.writef("\r%d out of %d tests performed", ++performed_tests, total_tests);
347 
348         GC.collect();
349     }
350     results.timeStat = typeof(results.timeStat)(results.times);
351     results.speedStat = PreciseStatisticData!double(results.speeds);
352     return results;
353 }
354 
355 string[] getConfigFiles(string config, uint maxFiles)
356 {
357     string[] results = [];
358     int count = 1;
359     while(count <= maxFiles && buildPath("random-benchmark", config ~ "_" ~ to!string(count) ~ ".xml").exists)
360     {
361         results ~= buildPath("random-benchmark", config ~ "_" ~ to!string(count) ~ ".xml");
362         count++;
363     }
364     return results;
365 }
366 
367 auto generateTestFiles(BenchmarkConfig benchmark)
368 {
369     if(!exists("random-benchmark"))
370         mkdir("random-benchmark");
371 
372     FileStats[string] results;
373 
374     total_files = benchmark.filesPerConfig * benchmark.configurations.length;
375     foreach (config; benchmark.configurations)
376         results.merge!"a"(generateTestFiles(config, config.name, benchmark.filesPerConfig));
377 
378     return results;
379 }
380 
381 ulong total_files;
382 ulong generated_files;
383 auto generateTestFiles(GenXmlConfig config, string name, int count)
384 {
385     FileStats[string] results;
386 
387     foreach (i; 0..count)
388     {
389         auto filename = buildPath("random-benchmark", name ~ "_" ~ to!string(i+1) ~".xml" );
390         if(!filename.exists)
391         {
392             auto file = File(filename, "w");
393             results[filename] = genDocument(file.lockingTextWriter, config);
394         }
395         else
396             results[filename] = FileStats.init;
397         stderr.writef("\r%d out of %d files generated", ++generated_files, total_files);
398     }
399     return results;
400 }
401 
402 void merge(alias f, V, K)(ref V[K] first, const V[K] second)
403 {
404     import std.functional: binaryFun;
405     alias fun = binaryFun!f;
406 
407     foreach (kv; second.byKeyValue)
408         if (kv.key in first)
409             first[kv.key] = fun(first[kv.key], kv.value);
410         else
411             first[kv.key] = kv.value;
412 }
413 
414 // CODE FOR PRETTY PRINTING
415 
416 string center(string str, ulong totalSpace)
417 {
418     Appender!string result;
419     auto whiteSpace = totalSpace - str.length;
420     ulong before = whiteSpace / 2;
421     foreach (i; 0..before)
422         result.put(' ');
423     result.put(str);
424     foreach (i; before..whiteSpace)
425         result.put(' ');
426     return result.data;
427 }
428 
429 Duration round(string unit)(Duration d)
430 {
431     import core.time: dur;
432     return dur!unit(d.total!unit);
433 }
434 
435 void printTimesForConfiguration(BenchmarkConfig benchmark, ComponentResults[string] results, string config)
436 {
437     import std.algorithm: max, map, maxCount;
438     import std.range: repeat, take;
439     auto component_width = max(results.byKey.map!"a.length".maxCount[0], 8);
440     auto std_width = 16;
441 
442     string formatDuration(Duration d)
443     {
444         import std.format: format;
445         auto millis = d.total!"msecs";
446         if (millis < 1000)
447             return format("%3u ms", millis);
448         else if (millis < 10000)
449             return format("%.2f s", millis/1000.0);
450         else if (millis < 100000)
451             return format("%.1f s", millis/1000.0);
452         else
453             return to!string(millis/1000) ~ " s";
454     }
455 
456     foreach (component; benchmark.components)
457     {
458         write("\r  " , component.name, ":", repeat(' ').take(component_width - component.name.length));
459         foreach (file; config.getConfigFiles(benchmark.filesPerConfig))
460         {
461             auto res = results[component.name].configResults[config].fileResults[file].timeStat;
462             write(center("min: " ~ formatDuration(res.min), std_width));
463             write(center("avg: " ~ formatDuration(res.mean), std_width));
464             write(center("max: " ~ formatDuration(res.max), std_width));
465             write(center("median: " ~ formatDuration(res.median), std_width + 3));
466             writeln(center("deviation: " ~ formatDuration(res.deviation), std_width + 6));
467             write(repeat(' ').take(component_width+3));
468         }
469     }
470     writeln();
471 }
472 
473 void printSpeedsForConfiguration(BenchmarkConfig benchmark, ComponentResults[string] results, string config)
474 {
475     import std.range: repeat, take;
476     import std.algorithm: max, map, maxCount;
477     import std.format: format;
478 
479     auto component_width = max(results.byKey.map!"a.length".maxCount[0], 13);
480     auto speed_column_width = 8UL;
481     auto large_column_width = 12;
482     auto spaces = repeat(' ');
483     auto lines = repeat('-');
484     auto boldLines = repeat('=');
485     write(spaces.take(component_width));
486     foreach (i; 0..benchmark.filesPerConfig)
487     {
488         write("|");
489         write(center("file " ~ to!string(i+1), 3*speed_column_width + 2));
490     }
491     writeln();
492     write(spaces.take(component_width));
493     foreach (i; 0..benchmark.filesPerConfig)
494     {
495         write("|");
496         write(lines.take(3*speed_column_width + 2));
497     }
498     writeln();
499     write(center("Speeds (MB/s)", component_width));
500     foreach (i; 0..benchmark.filesPerConfig)
501     {
502         write("|");
503         write(center("min", speed_column_width));
504         write("|");
505         write(center("avg", speed_column_width));
506         write("|");
507         write(center("max", speed_column_width));
508     }
509     writeln();
510     write(spaces.take(component_width));
511     foreach (i; 0..benchmark.filesPerConfig)
512     {
513         write("|");
514         write(lines.take(3*speed_column_width + 2));
515     }
516     writeln();
517     write(spaces.take(component_width));
518     foreach (i; 0..benchmark.filesPerConfig)
519     {
520         write("|");
521         write(center("median", large_column_width));
522         write("|");
523         write(center("deviation", large_column_width + 1));
524     }
525     writeln();
526     foreach (component; benchmark.components)
527     {
528         writeln(boldLines.take(component_width + benchmark.filesPerConfig*(3 + 3*speed_column_width)));
529         write(spaces.take(component_width));
530         foreach (file; getConfigFiles(config, benchmark.filesPerConfig))
531         {
532             auto fres = results[component.name].configResults[config].fileResults[file];
533             write("|");
534             write(center(format("%6.2f", fres.speedStat.min), speed_column_width));
535             write("|");
536             write(center(format("%6.2f", fres.speedStat.mean), speed_column_width));
537             write("|");
538             write(center(format("%6.2f", fres.speedStat.max), speed_column_width));
539         }
540         writeln();
541         write(center(component.name, component_width));
542         foreach (i; 0..benchmark.filesPerConfig)
543         {
544             write("|");
545             write(lines.take(3*speed_column_width + 2));
546         }
547         writeln();
548         write(spaces.take(component_width));
549         foreach (file; getConfigFiles(config, benchmark.filesPerConfig))
550         {
551             auto fres = results[component.name].configResults[config].fileResults[file];
552             write("|");
553             write(center(format("%6.2f", fres.speedStat.median), large_column_width));
554             write("|");
555             write(center(format("%6.2f", fres.speedStat.deviation), large_column_width + 1));
556         }
557         writeln();
558     }
559 }
560 
561 void printFilesForConfiguration(string config, uint maxFiles, FileStats[string] filestats)
562 {
563     import std.algorithm: each, map, max, maxCount;
564     import std.path: pathSplitter, stripExtension;
565     import std.file: getSize;
566 
567     ulong columnWidth = 16;
568 
569     void writeSized(ulong value, string measure = " ")
570     {
571         import std.format: format;
572         import std.range: repeat, take;
573         import std.math: ceil, log10;
574 
575         string formatted;
576         if (!value)
577         {
578             formatted = "  0 ";
579         }
580         else
581         {
582             static immutable string[] order = [" ", " k", " M", " G", " T"];
583             auto lg = cast(int)ceil(log10(value));
584             auto dec = (lg%3 == 0)? 0 : 3 - lg%3;
585             auto ord = (lg-1)/3;
586             auto val = value/(1000.0^^ord);
587             auto fmt = (dec?"":" ") ~ "%." ~ to!string(dec) ~ "f";
588             auto f = format(fmt, val);
589             formatted = f ~ order[ord];
590         }
591         formatted ~= measure;
592         formatted.center(columnWidth).write;
593     }
594     void writeAttribute(string attr, string measure = " ")()
595     {
596         foreach(name; config.getConfigFiles(maxFiles))
597         {
598             if (filestats[name] != FileStats.init)
599                 mixin("writeSized(filestats[name]." ~ attr ~ ", \"" ~ measure ~ "\");");
600             else
601                 center("-", columnWidth).write;
602         }
603     }
604 
605     write("\n  names:             ");
606     foreach(name; config.getConfigFiles(maxFiles))
607         name.pathSplitter.back.stripExtension.center(columnWidth).write;
608 
609     write("\n  total file size:   ");
610     foreach(name; config.getConfigFiles(maxFiles))
611         writeSized(name.getSize(), "B");
612 
613     write("\n  raw text content:  ");
614     writeAttribute!("textChars", "B");
615 
616     write("\n  useless spacing:   ");
617     writeAttribute!("spaces", "B");
618 
619     write("\n  total nodes:       ");
620     foreach(name; config.getConfigFiles(maxFiles))
621         if (filestats[name] != FileStats.init)
622         {
623             auto stat = filestats[name];
624             auto total = stat.elements + stat.textNodes + stat.cdataNodes + stat.processingInstructions + stat.comments + stat.attributes;
625             writeSized(total);
626         }
627         else
628             center("-", columnWidth).write;
629 
630     write("\n  element nodes:     ");
631     writeAttribute!("elements");
632 
633     write("\n  attribute nodes:   ");
634     writeAttribute!("attributes", "B");
635 
636     write("\n  text nodes:        ");
637     writeAttribute!("textNodes", "B");
638 
639     write("\n  cdata nodes:       ");
640     writeAttribute!("cdataNodes", "B");
641 
642     write("\n  comment nodes:     ");
643     writeAttribute!("comments", "B");
644     writeln();
645 }
646 
647 void printResultsByConfiguration(BenchmarkConfig benchmark, FileStats[string] filestats, ComponentResults[string] results)
648 {
649     uint i = 1;
650     foreach (config; benchmark.configurations)
651     {
652         writeln("\n=== CONFIGURATION " ~ to!string(i) ~ ": " ~ config.name ~ " ===\n");
653         writeln("Timings:\n");
654         printTimesForConfiguration(benchmark, results, config.name);
655         printSpeedsForConfiguration(benchmark, results, config.name);
656         writeln("\nConfiguration Parameters:");
657         config.prettyPrint(2).write;
658         writeln("\nFile Statistics (only for newly created files):");
659         printFilesForConfiguration(config.name, benchmark.filesPerConfig, filestats);
660         i++;
661     }
662 }
663 
664 void printResultsSpeedSummary(BenchmarkConfig benchmark, ComponentResults[string] results)
665 {
666     import std.range: repeat, take;
667     import std.algorithm: max, map, maxCount;
668     import std.format: format;
669 
670     auto component_width = max(results.byKey.map!"a.length".maxCount[0], 13);
671     auto speed_column_width = 8UL;
672     auto large_column_width = 12;
673     auto spaces = repeat(' ');
674     auto lines = repeat('-');
675     auto boldLines = repeat('=');
676 
677     writeln("\n=== CONFIGURATIONS SUMMARY ===\n");
678 
679     write(spaces.take(component_width));
680     foreach (i; 0..benchmark.configurations.length)
681     {
682         write("|");
683         write(center("config " ~ to!string(i+1), 3*speed_column_width + 2));
684     }
685     writeln();
686     write(spaces.take(component_width));
687     foreach (i; 0..benchmark.configurations.length)
688     {
689         write("|");
690         write(lines.take(3*speed_column_width + 2));
691     }
692     writeln();
693     write(center("Speeds (MB/s)", component_width));
694     foreach (i; 0..benchmark.configurations.length)
695     {
696         write("|");
697         write(center("min", speed_column_width));
698         write("|");
699         write(center("avg", speed_column_width));
700         write("|");
701         write(center("max", speed_column_width));
702     }
703     writeln();
704     write(spaces.take(component_width));
705     foreach (i; 0..benchmark.configurations.length)
706     {
707         write("|");
708         write(lines.take(3*speed_column_width + 2));
709     }
710     writeln();
711     write(spaces.take(component_width));
712     foreach (i; 0..benchmark.configurations.length)
713     {
714         write("|");
715         write(center("median", large_column_width));
716         write("|");
717         write(center("deviation", large_column_width + 1));
718     }
719     writeln();
720     foreach (component; benchmark.components)
721     {
722         writeln(boldLines.take(component_width + benchmark.configurations.length*(3 + 3*speed_column_width)));
723         write(spaces.take(component_width));
724         foreach (config; benchmark.configurations)
725         {
726             auto fres = results[component.name].configResults[config.name];
727             write("|");
728             write(center(format("%6.2f", fres.speedStat.min), speed_column_width));
729             write("|");
730             write(center(format("%6.2f", fres.speedStat.mean), speed_column_width));
731             write("|");
732             write(center(format("%6.2f", fres.speedStat.max), speed_column_width));
733         }
734         writeln();
735         write(center(component.name, component_width));
736         foreach (i; 0..benchmark.configurations.length)
737         {
738             write("|");
739             write(lines.take(3*speed_column_width + 2));
740         }
741         writeln();
742         write(spaces.take(component_width));
743         foreach (config; benchmark.configurations)
744         {
745             auto fres = results[component.name].configResults[config.name];
746             write("|");
747             write(center(format("%6.2f", fres.speedStat.median), large_column_width));
748             write("|");
749             write(center(format("%6.2f", fres.speedStat.deviation), large_column_width + 1));
750         }
751         writeln();
752     }
753 }
754 
755 void printResultsCSV(BenchmarkConfig benchmark, ComponentResults[string] results)
756 {
757     import std.datetime: Clock;
758     import core.time: ClockType;
759     auto now = Clock.currTime!(ClockType.second);
760     auto timeStr = now.toISOExtString;
761 
762     import std.format: format;
763     auto fmtP = "P, " ~ timeStr ~ ", %s, %f, %f, %f, %f, %f";
764     auto fmtC = "C, " ~ timeStr ~ ", %s, %s, %f, %f, %f, %f, %f";
765     auto fmtF = "F, " ~ timeStr ~ ", %s, %s, %s, %f, %f, %f, %f, %f";
766 
767     foreach (component; benchmark.components)
768     {
769         auto compStats = results[component.name].speedStat;
770         fmtP.format(component.name, compStats.min, compStats.mean, compStats.max, compStats.median, compStats.deviation).writeln;
771 
772         foreach (config; benchmark.configurations)
773         {
774             auto confStats = results[component.name].configResults[config.name].speedStat;
775             fmtC.format(component.name, config.name, confStats.min, confStats.mean, confStats.max, confStats.median, confStats.deviation).writeln;
776 
777             foreach (file, fileResults; results[component.name].configResults[config.name].fileResults)
778             {
779                 auto fileStats = fileResults.speedStat;
780                 fmtF.format(component.name, config.name, file, fileStats.min, fileStats.mean, fileStats.max, fileStats.median, fileStats.deviation).writeln;
781             }
782         }
783     }
784 }