1 /// Parameterizable benchmarking with support for parallel, global results, reports, etc.
2 module tern.benchmark;
3 
4 import tern.traits;
5 import tern.meta;
6 import std.algorithm;
7 import std.parallelism;
8 import std.datetime;
9 import std.stdio;
10 import std.conv;
11 import std.range;
12 
13 public struct BenchmarkConfig
14 {
15 public:
16 final:
17     size_t warmup = 100;
18     size_t iterations = 1000;
19     bool parallel = false;
20 }
21 
22 public struct BenchmarkResult
23 {
24 public:
25 final:
26     string functionName;
27     size_t index;
28     Duration duration;
29     BenchmarkConfig config;
30 }
31 
32 private:
33 BenchmarkResult[] results;
34 
35 public:
36 /**
37  * Benchmarks all functions in `FUNCS` with the given config.
38  *
39  * Params:
40  *  FUNCS = Sequence of functions to be benchmarked.
41  *  config = Benchmark configuration.
42  *
43  * Remarks:
44  *  May not be parameterized.
45  */
46 BenchmarkResult[] benchmark(FUNCS...)(const scope BenchmarkConfig config)
47     if (allSatisfy!(isCallable, FUNCS))
48 {
49     BenchmarkResult[FUNCS.length] ret;
50     if (!config.parallel)
51     {
52         foreach (i, F; FUNCS)
53         {
54             auto timestamp = Clock.currTime;
55             writeln("[", timestamp.hour, ":", timestamp.minute, ":", timestamp.second, "] ", identifier!F, "() benchmarking...");
56             
57             foreach (j; 0..config.warmup)
58                 F();
59 
60             auto start = Clock.currTime;
61             foreach (j; 0..config.iterations)
62                 F();
63 
64             auto duration = (Clock.currTime - start);
65             ret[i] = BenchmarkResult(identifier!F~"()", i, duration, config);
66             results ~= BenchmarkResult(identifier!F~"()", results.length, duration, config);
67 
68             timestamp = Clock.currTime;
69             writeln("[", timestamp.hour, ":", timestamp.minute, ":", timestamp.second, "] ", identifier!F, "() finished benchmark!");
70         }
71     }
72     else 
73     {
74         foreach (i; parallel(iota(0, FUNCS.length)))
75         {
76             static foreach (j, F; FUNCS)
77                 mixin("if (i == "~j.to!string~")
78                 {
79                     auto timestamp = Clock.currTime;
80                     writeln(\"[\", timestamp.hour, \":\", timestamp.minute, \":\", timestamp.second, \"] \", identifier!F, \"() benchmarking...\");
81                     foreach (k; 0..config.warmup)
82                         FUNCS["~j.to!string~"]();
83 
84                     auto start = Clock.currTime;
85                     foreach (k; 0..config.iterations)
86                         FUNCS["~j.to!string~"]();
87 
88                     auto duration = (Clock.currTime - start);
89                     ret[i] = BenchmarkResult(identifier!F~\"()\", i, duration, config);
90                     results ~= BenchmarkResult(identifier!F~\"()\", results.length, duration, config);
91 
92                     timestamp = Clock.currTime;
93                     writeln(\"[\", timestamp.hour, \":\", timestamp.minute, \":\", timestamp.second, \"] \",  identifier!F, \"() finished benchmark!\");
94                 }");
95         }
96     }
97     return ret.dup;
98 }
99 
100 /**
101  * Benchmarks `F` with the given config and arguments.
102  *
103  * Params:
104  *  F = The function to be benchmarked.
105  *  config = Benchmark configuration.
106  *  args = The arguments to invoke `F` with.
107  */
108 BenchmarkResult[] benchmark(alias F, ARGS...)(const scope BenchmarkConfig config, ARGS args)
109 {
110     auto timestamp = Clock.currTime;
111     writeln("[", timestamp.hour, ":", timestamp.minute, ":", timestamp.second, "] ", SignatureOf!F, " benchmarking...");
112 
113     foreach (i; 0..config.warmup)
114         F(args);
115 
116     auto start = Clock.currTime;
117     foreach (i; 0..config.iterations)
118         F(args);
119 
120     auto duration = (Clock.currTime - start);
121     auto result = BenchmarkResult(SignatureOf!F, 0, duration, config);
122     results ~= BenchmarkResult(SignatureOf!F, results.length, duration, config);
123 
124     timestamp = Clock.currTime;
125     writeln("[", timestamp.hour, ":", timestamp.minute, ":", timestamp.second, "] ",  SignatureOf!F, " finished benchmark!");
126     
127     return [result];
128 }
129 
130 /**
131  * Writes a report of `results` to the console.
132  *
133  * Params:
134  *  results = The benchmark results to be written.
135  */
136 void report(BenchmarkResult[] results) 
137 {
138     writeln("BENCHMARK ["~__VENDOR__~" "~__VERSION__.to!string~"]");
139     writeln("-----------------------------------------------------------------------------");
140     writeln("| Index | Function                                 | Duration               |");
141     writeln("-----------------------------------------------------------------------------");
142 
143     foreach (res; results)
144     {
145         auto duration = res.duration.split!("seconds", "msecs", "usecs");
146 
147         writef("| %-6s| %-40s | ", res.index, res.functionName);
148 
149         if (duration.seconds > 1)
150             writefln("%-10.3fs            |", duration.seconds);
151         else if (duration.msecs > 1)
152             writefln("%-10.3fms           |", duration.msecs);
153         else
154             writefln("%-10.3fus           |", duration.usecs);
155     }
156 
157     writeln("-----------------------------------------------------------------------------");
158 }
159 
160 /// Writes a report of all benchmarks that have been run to the console.
161 void reportAll() 
162 {
163     report(results);
164 }