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