1 module unit_threaded.randomized.benchmark;
2 
3 import unit_threaded.from;
4 
5 /* This function used $(D MonoTimeImpl!(ClockType.precise).currTime) to time
6 how long $(D MonoTimeImpl!(ClockType.precise).currTime) takes to return
7 the current time.
8 */
9 private auto medianStopWatchTime()
10 {
11     import core.time;
12     import std.algorithm : sort;
13     import std.datetime: Duration, MonoTimeImpl;
14 
15     enum numRounds = 51;
16     Duration[numRounds] times;
17 
18     MonoTimeImpl!(ClockType.precise) dummy;
19     for (size_t i = 0; i < numRounds; ++i)
20     {
21         auto sw = MonoTimeImpl!(ClockType.precise).currTime;
22         dummy = MonoTimeImpl!(ClockType.precise).currTime;
23         dummy = MonoTimeImpl!(ClockType.precise).currTime;
24         doNotOptimizeAway(dummy);
25         times[i] = MonoTimeImpl!(ClockType.precise).currTime - sw;
26     }
27 
28     sort(times[]);
29 
30     return times[$ / 2].total!"hnsecs";
31 }
32 
33 private from!"std.datetime".Duration getQuantilTick
34     (double q)
35     (from!"std.datetime".Duration[] ticks)
36     pure @safe
37 {
38     size_t idx = cast(size_t)(ticks.length * q);
39 
40     if (ticks.length % 2 == 1)
41     {
42         return ticks[idx];
43     }
44     else
45     {
46         return (ticks[idx] + ticks[idx - 1]) / 2;
47     }
48 }
49 
50 // @Name("Quantil calculations")
51 // unittest
52 // {
53 //     static import std.conv;
54 //     import std.algorithm.iteration : map;
55 
56 //     auto ticks = [1, 2, 3, 4, 5].map!(a => dur!"seconds"(a)).array;
57 
58 //     Duration q25 = getQuantilTick!0.25(ticks);
59 //     assert(q25 == dur!"seconds"(2), q25.toString());
60 
61 //     Duration q50 = getQuantilTick!0.50(ticks);
62 //     assert(q50 == dur!"seconds"(3), q25.toString());
63 
64 //     Duration q75 = getQuantilTick!0.75(ticks);
65 //     assert(q75 == dur!"seconds"(4), q25.toString());
66 
67 //     q25 = getQuantilTick!0.25(ticks[0 .. 4]);
68 //     assert(q25 == dur!"seconds"(1) + dur!"msecs"(500), q25.toString());
69 
70 //     q50 = getQuantilTick!0.50(ticks[0 .. 4]);
71 //     assert(q50 == dur!"seconds"(2) + dur!"msecs"(500), q25.toString());
72 
73 //     q75 = getQuantilTick!0.75(ticks[0 .. 4]);
74 //     assert(q75 == dur!"seconds"(3) + dur!"msecs"(500), q25.toString());
75 // }
76 
77 /** The options  controlling the behaviour of benchmark. */
78 struct BenchmarkOptions
79 {
80     import std.datetime: Duration, seconds;
81 
82     string funcname; // the name of the function to benchmark
83     string filename; // the name of the file the results will be appended to
84     Duration duration = 1.seconds; // the time after which the function to
85                                    // benchmark is not executed anymore
86     size_t maxRounds = 10000; // the maximum number of times the function
87                               // to benchmark is called
88     int seed = 1337; // the seed to the random number generator
89 
90     this(string funcname)
91     {
92         this.funcname = funcname;
93     }
94 }
95 
96 /** This $(D struct) takes care of the time taking and outputting of the
97 statistics.
98 */
99 struct Benchmark
100 {
101     import std.array : Appender;
102     import std.datetime: Duration, MonoTimeImpl, ClockType;
103 
104     string filename; // where to write the benchmark result to
105     string funcname; // the name of the benchmark
106     size_t rounds; // the number of times the functions is supposed to be
107     //executed
108     string timeScale; // the unit the benchmark is measuring in
109     real medianStopWatch; // the median time it takes to get the clocktime twice
110     bool dontWrite; // if set, no data is written to the the file name "filename"
111     // true if, RndValueGen opApply was interrupt unexpectitally
112     Appender!(Duration[]) ticks; // the stopped times, there will be rounds ticks
113     size_t ticksIndex = 0; // the index into ticks
114     size_t curRound = 0; // the number of rounds run
115     MonoTimeImpl!(ClockType.precise) startTime;
116     Duration timeSpend; // overall time spend running the benchmark function
117 
118     /** The constructor for the $(D Benchmark).
119     Params:
120         funcname = The name of the $(D benchmark) instance. The $(D funcname)
121             will be used to associate the results with the function
122         founds = How many rounds.
123         filename = The $(D filename) will be used as a filename to store the
124             results.
125     */
126     this(in string funcname, in size_t rounds, in string filename)
127     {
128         import std.array : appender;
129         this.filename = filename;
130         this.funcname = funcname;
131         this.rounds = rounds;
132         this.timeScale = "hnsecs";
133         this.ticks = appender!(Duration[])();
134         this.medianStopWatch = medianStopWatchTime();
135     }
136 
137     /** A call to this method will start the time taking process */
138     void start()
139     {
140         this.startTime = MonoTimeImpl!(ClockType.precise).currTime;
141     }
142 
143     /** A call to this method will stop the time taking process, and
144     appends the execution time to the $(D ticks) member.
145     */
146     void stop()
147     {
148         auto end = MonoTimeImpl!(ClockType.precise).currTime;
149         Duration dur = end - this.startTime;
150         this.timeSpend += dur;
151         this.ticks.put(dur);
152         ++this.curRound;
153     }
154 
155     ~this()
156     {
157         import std.stdio : File;
158         import std.datetime: Clock;
159 
160         if (!this.dontWrite && this.ticks.data.length)
161         {
162             import std.algorithm : sort;
163 
164             auto sortedTicks = this.ticks.data;
165             sortedTicks.sort();
166 
167             auto f = File(filename ~ "_bechmark.csv", "a");
168             scope (exit)
169                 f.close();
170 
171             auto q0 = sortedTicks[0].total!("hnsecs")() /
172                 cast(double) this.rounds;
173             auto q25 = getQuantilTick!0.25(sortedTicks).total!("hnsecs")() /
174                 cast(double) this.rounds;
175             auto q50 = getQuantilTick!0.50(sortedTicks).total!("hnsecs")() /
176                    cast(double) this.rounds;
177             auto q75 = getQuantilTick!0.75(sortedTicks).total!("hnsecs")() /
178                 cast(double) this.rounds;
179             auto q100 = sortedTicks[$ - 1].total!("hnsecs")() /
180                 cast(double) this.rounds;
181 
182             // funcname, the data when the benchmark was created, unit of time,
183             // rounds, medianStopWatch, low, 0.25 quantil, median,
184             // 0.75 quantil, high
185             f.writefln(
186                 "\"%s\",\"%s\",\"%s\",\"%s\",\"%s\",\"%s\",\"%s\",\"%s\",\"%s\""
187                 ~ ",\"%s\"",
188                 this.funcname, Clock.currTime.toISOExtString(),
189                 this.timeScale, this.curRound, this.medianStopWatch,
190                 q0 > this.medianStopWatch ? q0 - this.medianStopWatch : 0,
191                 q25 > this.medianStopWatch ? q25 - this.medianStopWatch : 0,
192                 q50 > this.medianStopWatch ? q50 - this.medianStopWatch : 0,
193                 q75 > this.medianStopWatch ? q75 - this.medianStopWatch : 0,
194                 q100 > this.medianStopWatch ? q100 - this.medianStopWatch : 0);
195         }
196     }
197 }
198 
199 void doNotOptimizeAway(T...)(ref T t)
200 {
201     foreach (ref it; t)
202     {
203         doNotOptimizeAwayImpl(&it);
204     }
205 }
206 
207 private void doNotOptimizeAwayImpl(void* p) {
208         import core.thread : getpid;
209         import std.stdio : writeln;
210         if(getpid() == 1) {
211                 writeln(*cast(char*)p);
212         }
213 }
214 
215 // unittest
216 // {
217 //     static void funToBenchmark(int a, float b, Gen!(int, -5, 5) c, string d,
218 //         GenASCIIString!(1, 10) e)
219 //     {
220 //         import core.thread;
221 
222 //         Thread.sleep(1.seconds / 100000);
223 //         doNotOptimizeAway(a, b, c, d, e);
224 //     }
225 
226 //     benchmark!funToBenchmark();
227 //     benchmark!funToBenchmark("Another Name");
228 //     benchmark!funToBenchmark("Another Name", 2.seconds);
229 //     benchmark!funToBenchmark(2.seconds);
230 // }
231 
232 /** This function runs the passed callable $(D T) for the duration of
233 $(D maxRuntime). It will count how often $(D T) is run in the duration and
234 how long each run took to complete.
235 
236 Unless compiled in release mode, statistics will be printed to $(D stderr).
237 If compiled in release mode the statistics are appended to a file called
238 $(D name).
239 
240 Params:
241     opts = A $(D BenchmarkOptions) instance that encompasses all possible
242         parameters of benchmark.
243     name = The name of the benchmark. The name is also used as filename to
244         save the benchmark results.
245     maxRuntime = The maximum time the benchmark is executed. The last run will
246         not be interrupted.
247     rndSeed = The seed to the random number generator used to populate the
248         parameter passed to the function to benchmark.
249     rounds = The maximum number of times the callable $(D T) is called.
250 */
251 void benchmark(alias T)(const ref BenchmarkOptions opts)
252 {
253     import std.random : Random;
254     import std.traits: ParameterIdentifierTuple, Parameters;
255     import unit_threaded.randomized.random;
256 
257     auto bench = Benchmark(opts.funcname, opts.maxRounds, opts.filename);
258     auto rnd = Random(opts.seed);
259     enum string[] parameterNames = [ParameterIdentifierTuple!T];
260     auto valueGenerator = RndValueGen!(parameterNames, Parameters!T)(&rnd);
261 
262     while (bench.timeSpend <= opts.duration && bench.curRound < opts.maxRounds)
263     {
264         valueGenerator.genValues();
265 
266         bench.start();
267         try
268         {
269             T(valueGenerator.values);
270         }
271         catch (Throwable t)
272         {
273             import std.experimental.logger : logf;
274 
275             logf("unittest with name %s failed when parameter %s where passed",
276                 opts.funcname, valueGenerator);
277             break;
278         }
279         finally
280         {
281             bench.stop();
282             ++bench.curRound;
283         }
284     }
285 }
286 
287 /// Ditto
288 void benchmark(alias T)(string funcname = "", string filename = __FILE__)
289 {
290     import std..string : empty;
291     import std.traits: fullyQualifiedName;
292 
293     auto opt = BenchmarkOptions(
294         funcname.empty ? fullyQualifiedName!T : funcname
295     );
296     opt.filename = filename;
297     benchmark!(T)(opt);
298 }
299 
300 /// Ditto
301 void benchmark(alias T)(from!"std.datetime".Duration maxRuntime, string filename = __FILE__)
302 {
303     import std.traits: fullyQualifiedName;
304     auto opt = BenchmarkOptions(fullyQualifiedName!T);
305     opt.filename = filename;
306     opt.duration = maxRuntime;
307     benchmark!(T)(opt);
308 }
309 
310 /// Ditto
311 /*void benchmark(alias T)(string name, string filename = __FILE__)
312 {
313     auto opt = BenchmarkOptions(name);
314     opt.filename = filename;
315     benchmark!(T)(opt);
316 }*/
317 
318 /// Ditto
319 void benchmark(alias T)(string name, from!"std.datetime".Duration maxRuntime,
320     string filename = __FILE__)
321 {
322     auto opt = BenchmarkOptions(name);
323     opt.filename = filename;
324     opt.duration = maxRuntime;
325     benchmark!(T)(opt);
326 }