1 /** Extensions to std.datetime.benchmark.
2 
3 	Copyright: Per Nordlöw 2022-.
4 	License: $(WEB boost.org/LICENSE_1_0.txt, Boost License 1.0).
5 	Authors: $(WEB Per Nordlöw)
6 
7 	TODO: Mimic benchmark.run at https://docs.modular.com/mojo/stdlib/benchmark/benchmark
8 	TODO: Use ggplot or similar to visualize results.
9 	TODO: Use `nxt.sampling` to generate test data.
10 */
11 module nxt.benchmark;
12 
13 /** Behavior or reservation of space for a specific implicit length/size/count.
14  */
15 enum ReserveSupport {
16 	no,							///< Reservation is not (yet) supported.
17 	yes,						///< Reservation is supported.
18 	always,	///< Reservation is not needed because of unconditional pre-allocation, either static or dynamic.
19 }
20 
21 struct Results {
22 	import core.time : Duration;
23 	@property void toString(Sink)(ref scope Sink sink) const {
24 		import std.format : formattedWrite;
25 		foreach (memberName; __traits(allMembers, typeof(this))) {
26 			const member = __traits(getMember, this, memberName);
27 			alias Member = typeof(member);
28 			static if (is(immutable Member == immutable Duration)) {
29 				if (member == Member.init)
30 					continue;
31 				sink.formattedWrite("%s: %s ns/op",
32 									memberName,
33 									cast(double)(member).total!"nsecs" / elementCount);
34 			}
35 		}
36 	}
37 	string typeName;
38 	size_t elementCount;
39 	size_t runCount;
40 	private Duration _insertWithoutGrowthTime;
41 	private Duration _insertWithGrowthTime;
42 	private Duration _removeTime;
43 	private Duration _containsTime;
44 	private Duration _inTime;
45 	private Duration _rehashTime;
46 	private Duration _inAfterRehashTime;
47 	private Duration _indexTime;
48 	private Duration _appendTime;
49 }
50 
51 /// Number of run per benchmark.
52 debug
53 	static immutable runCountDefault = 3; // lighter test in debug mode
54 else
55 	static immutable runCountDefault = 10;
56 
57 /// Formatting uses some extra space but should be removed when outputting to plots.
58 static immutable formatNsPerOp = "%6.1f ns/op";
59 
60 /** Benchmark append operation available in type `A` with test source `S`.
61  */
62 Results benchmarkAppendable(A, Sample, Source)(in Source testSource, in size_t runCount = runCountDefault) {
63 	import core.time : Duration;
64 	import std.datetime : MonoTime;
65 	import std.conv : to;
66 	import std.algorithm.searching : minElement, maxElement;
67 	import std.stdio : writef, writefln;
68 
69 	ReserveSupport reserveSupport;
70 	auto results = typeof(return)(A.stringof, testSource.length, runCount);
71 
72 	writef("- ");
73 
74 	scope spans = new Duration[runCount];
75 
76 	A _ = makeWithRequestedCapacity!(A)(0, reserveSupport);
77 	foreach (const runIx; 0 .. runCount) {
78 		A a = makeWithRequestedCapacity!(A)(results.elementCount, reserveSupport);
79 		const start = MonoTime.currTime();
80 		foreach (immutable i; testSource) {
81 			static	  if (is(typeof(a ~= i)))
82 				a ~= i;
83 			else static if (is(typeof(a ~= i.to!Sample)))
84 				a ~= i.to!Sample;
85 			else static if (is(typeof(a.put(i))))
86 				a.put(i);
87 			else static if (is(typeof(a.put(i.to!Sample))))
88 				a.put(i.to!Sample);
89 			else
90 				static assert(0, "Cannot append a `" ~ typeof(i).stringof ~ "` to a `" ~ A.stringof ~ "`");
91 		}
92 		spans[runIx] = MonoTime.currTime() - start;
93 		static if (__traits(hasMember, A, `clear`) &&
94 				   is(typeof(a.clear())))
95 			a.clear();
96 	}
97 	results._appendTime = spans[].minElement();
98 	writef("append (%s) (~=): "~formatNsPerOp,
99 		   reserveSupport == ReserveSupport.no ? "no growth" : "with growth",
100 		   cast(double)(results._appendTime).total!"nsecs" / results.elementCount);
101 
102 	writefln(` for %s`, A.stringof);
103 
104 	return results;
105 }
106 
107 /** Benchmark set (container) type `A` with test source `S`.
108  */
109 Results benchmarkSet(A, Sample, Source)(in Source testSource, in size_t runCount = runCountDefault)
110 /+ TODO: if (isSet!A) +/
111 {
112 	import core.time : Duration;
113 	import std.datetime : MonoTime;
114 	import std.conv : to;
115 	import std.stdio : writef, writefln, writeln;
116 	import std.algorithm.searching : minElement, maxElement;
117 	import nxt.address : AlignedAddress;
118 
119 	alias Address = AlignedAddress!1;
120 
121 	ReserveSupport reserveSupport;
122 	scope spans = new Duration[runCount];
123 	auto results = typeof(return)(A.stringof, testSource.length, runCount);
124 
125 	writef("- ");
126 
127 	A a;
128 	{							// needs scope
129 		foreach (const runIx; 0 .. runCount) {
130 			const start = MonoTime.currTime();
131 			foreach (immutable i; testSource) {
132 				static if (__traits(hasMember, A, `ElementType`) &&
133 						   is(A.ElementType == ubyte[]))
134 					a.insert(i.toUbytes);
135 				else {
136 					static if (__traits(hasMember, A, `ElementType`)) {
137 						static if (is(A.ElementType == Address))
138 							const element = A.ElementType(i + 1); ///< Start at 1 instead of 0 because `Address` uses 0 for `nullValue`.
139 						else static if (is(A.ElementType : string) ||
140 										is(typeof(A.ElementType(string.init))))
141 							const element = A.ElementType(to!string(i));
142 						else
143 							const element = A.ElementType(i);
144 					}
145 					else
146 						const element = i;
147 					a.insert(element);
148 				}
149 			}
150 			spans[runIx] = MonoTime.currTime() - start;
151 			static if (__traits(hasMember, A, `clear`) &&
152 					   is(typeof(a.clear())))
153 				if (runIx+1 != runCount)
154 					a.clear();	// clear all but the last one needed in contains below
155 		}
156 		results._insertWithGrowthTime = spans[].minElement();
157 		writef("insert (with growth): "~formatNsPerOp,
158 			   cast(double)(results._insertWithGrowthTime).total!"nsecs" / results.elementCount);
159 	}
160 
161 	{							// needs scope
162 		const start = MonoTime.currTime();
163 		size_t hitCount = 0;
164 		foreach (immutable i; testSource) {
165 			static if (__traits(hasMember, A, `ElementType`) &&
166 					   is(A.ElementType == ubyte[]))
167 				hitCount += a.contains(i.toUbytes);
168 			else {
169 				static if (__traits(hasMember, A, `ElementType`)) {
170 					static if (is(A.ElementType == Address))
171 						const element = A.ElementType(i + 1); ///< Start at 1 instead of 0 because `Address` uses 0 for `nullValue`.
172 					else static if (is(A.ElementType : string) ||
173 									is(typeof(A.ElementType(string.init))))
174 						const element = A.ElementType(to!string(i));
175 					else
176 						const element = A.ElementType(i); // wrap in `i` in `Nullable`
177 				}
178 				else
179 					const element = i;
180 				static if (__traits(hasMember, A, "contains"))
181 					hitCount += a.contains(element);
182 				else static if (is(typeof(a.contains(element)) == bool))
183 					hitCount += a.contains(element);
184 				else static if (is(typeof(element in a) == bool))
185 					hitCount += element in a;
186 				else
187 					static assert(0,
188 								  "Cannot check that " ~
189 								  typeof(a).stringof ~
190 								  " contains " ~
191 								  typeof(element).stringof);
192 			}
193 		}
194 		const ok = hitCount == results.elementCount; // for side effect in output
195 		results._containsTime = MonoTime.currTime() - start;
196 		writef(", contains: "~formatNsPerOp~" ns/op (%s)",
197 			   cast(double)(results._containsTime).total!"nsecs" / results.elementCount,
198 			   ok ? "OK" : "ERR");
199 	}
200 
201 	const _ = makeWithRequestedCapacity!(A)(0, reserveSupport);
202 	if (reserveSupport) {
203 		foreach (const runIx; 0 .. runCount) {
204 			A b = makeWithRequestedCapacity!(A)(results.elementCount, reserveSupport);
205 			const start = MonoTime.currTime();
206 			foreach (immutable i; testSource) {
207 				static if (__traits(hasMember, A, `ElementType`) &&
208 						   is(A.ElementType == ubyte[]))
209 					b.insert(i.toUbytes);
210 				else {
211 					static if (__traits(hasMember, A, `ElementType`)) {
212 						static if (is(A.ElementType == Address))
213 							const element = A.ElementType(i + 1); ///< Start at 1 instead of 0 because `Address` uses 0 for `nullValue`.
214 						else static if (is(A.ElementType : string) ||
215 										is(typeof(A.ElementType(string.init))))
216 							const element = A.ElementType(to!string(i));
217 						else
218 							const element = A.ElementType(i); // wrap in `i` in `Nullable`
219 					}
220 					else
221 						const element = i;
222 					b.insert(element);
223 				}
224 			}
225 			spans[runIx] = MonoTime.currTime() - start;
226 			static if (__traits(hasMember, A, `clear`) &&
227 					   is(typeof(b.clear())))
228 				b.clear();		  // TODO why does this fail for `RadixTreeMap`?
229 
230 		}
231 		results._insertWithoutGrowthTime = spans[].minElement();
232 		writef(", insert (no growth): "~formatNsPerOp,
233 			   cast(double)(results._insertWithoutGrowthTime).total!"nsecs" / results.elementCount);
234 	}
235 
236 	writef(` for %s`, A.stringof);
237 
238 	static if (__traits(hasMember, A, `binCounts`))
239 		writef(" %s", a.binCounts());
240 	static if (__traits(hasMember, A, `smallBinCapacity`))
241 		writef(" smallBinCapacity:%s", A.smallBinCapacity);
242 	static if (__traits(hasMember, A, `averageProbeCount`))
243 		writef(" averageProbeCount:%s", a.averageProbeCount);
244 
245 	writeln();
246 
247 	return results;
248 }
249 
250 /** Benchmark map (container) type `A` with test source `S`.
251  */
252 Results benchmarkMap(A, Sample, Source)(in Source testSource, in size_t runCount = runCountDefault)
253 /+ TODO: if (isMap!A || __traits(isAssociativeArray, A)) +/
254 {
255 	import core.time : Duration;
256 	import std.datetime : MonoTime;
257 	import std.conv : to;
258 	import std.stdio : writef, writefln, writeln;
259 	import std.algorithm.searching : minElement, maxElement;
260 	import nxt.address : AlignedAddress;
261 
262 	alias Address = AlignedAddress!1;
263 
264 	ReserveSupport reserveSupport;
265 	scope spans = new Duration[runCount];
266 	auto results = typeof(return)(A.stringof, testSource.length, runCount);
267 
268 	writef("- ");
269 
270 	// determine key and value type. TODO: extract to trait `KeyValueType(A)`
271 	static if (is(A : V[K], K, V)) {
272 		alias KeyType = K;
273 		alias ValueType = K;
274 	} else {
275 		// determine key type. TODO: extract to trait `KeyType(A)`
276 		static if (is(A.KeyType)) {
277 			alias KeyType = A.KeyType;
278 		} else static if (is(typeof(A.KeyValue.key))) { // StringMap
279 			alias KeyType = typeof(A.KeyValue.key);
280 		} else static if (is(immutable typeof(A.keys()) == immutable K[], K)) { // emsi HashMap
281 			alias KeyType = K;
282 		} else static if (__traits(hasMember, A, "byKey")) { // emsi HashMap
283 			import std.range.primitives : std_ElementType = ElementType;
284 			alias KeyType = std_ElementType!(typeof(A.init.byKey()));
285 		} else {
286 			static assert(0, "Could not determine key type of " ~ A.stringof ~ " " ~ typeof(A.keys()).stringof);
287 		}
288 		// determine value type. TODO: extract to trait `ValueType(A)`
289 		static if (is(A.ValueType)) {
290 			alias ValueType = A.ValueType;
291 		} else static if (is(typeof(A.KeyValue.value))) { // StringMap
292 			alias ValueType = typeof(A.KeyValue.value);
293 		} else static if (__traits(hasMember, A, "byValue")) { // emsi HashMap
294 			import std.range.primitives : std_ElementType = ElementType;
295 			alias ValueType = std_ElementType!(typeof(A.init.byValue()));
296 		} else {
297 			static assert(0, "Could not determine value type of " ~ A.stringof);
298 		}
299 	}
300 
301 	// determine element type. TODO: extract to trait `ElementType(A)` or reuse `std.primitives.ElementType`
302 	static if (is(A.ElementType)) {
303 		alias ElementType = A.ElementType;
304 	} else static if (is(A.KeyValue)) { // StringMap
305 		alias ElementType = A.KeyValue;
306 	} else static if (__traits(hasMember, A, "byKeyValue") || // emsi HashMap
307 					  !is(typeof(A.init.byKeyValue) == void)) {
308 		import std.range.primitives : std_ElementType = ElementType;
309 		alias ElementType = std_ElementType!(typeof(A.init.byKeyValue()));
310 	} else {
311 		static assert(0, "Could not determine element type of " ~ A.stringof);
312 	}
313 
314 	// allocate
315 	const keys = iotaArrayOf!(KeyType)(0, results.elementCount);
316 
317 	A a;
318 
319 	// insert/opIndex without preallocation via void capacity(size_t)
320 	{
321 		foreach (const runIx; 0 .. runCount) {
322 			const start = MonoTime.currTime();
323 			foreach (immutable i; testSource) {
324 				static if (is(typeof(A.init.insert(ElementType.init)))) {
325 					static if (is(KeyType == Address))
326 						const element = ElementType(Address(keys[i] + 1), // avoid `Address.null Value`
327 													ValueType.init);
328 					else
329 						const element = ElementType(keys[i], ValueType.init);
330 					a.insert(element);
331 				}
332 				else
333 					a[keys[i]] = ValueType.init; // AAs
334 			}
335 			spans[runIx] = MonoTime.currTime() - start;
336 		}
337 		results._insertWithGrowthTime = spans[].minElement();
338 		writef("insert (with growth): "~formatNsPerOp,
339 			   cast(double)(results._insertWithGrowthTime).total!"nsecs" / results.elementCount);
340 	}
341 
342 	// contains
343 	static if (is(typeof(A.init.contains(KeyType.init)) : bool)) {
344 		{
345 			bool okAll = true;
346 			foreach (const runIx; 0 .. runCount) {
347 				const start = MonoTime.currTime();
348 				size_t hitCount = 0;
349 				foreach (immutable i; testSource) {
350 					static if (is(KeyType == Address))
351 						hitCount += a.contains(Address(keys[i] + 1)); // avoid `Address.nullValue`
352 					else
353 						hitCount += a.contains(keys[i]) ? 1 : 0;
354 				}
355 				const ok = hitCount == results.elementCount; // for side effect in output
356 				if (!ok)
357 					okAll = false;
358 				spans[runIx] = MonoTime.currTime() - start;
359 			}
360 			results._containsTime = spans[].minElement();
361 			writef(", contains: "~formatNsPerOp~" ns/op (%s)",
362 				   cast(double)(results._containsTime).total!"nsecs" / results.elementCount,
363 				   okAll ? "OK" : "ERR");
364 		}
365 	}
366 
367 	// in
368 	{
369 		bool okAll = true;
370 		foreach (const runIx; 0 .. runCount) {
371 			const start = MonoTime.currTime();
372 			size_t hitCount = 0;
373 			foreach (immutable i; testSource) {
374 				static if (is(KeyType == Address))
375 					hitCount += cast(bool)(Address(keys[i] + 1) in a); // avoid `Address.nullValue`
376 				else
377 					hitCount += cast(bool)(keys[i] in a);
378 			}
379 			const ok = hitCount == results.elementCount; // for side effect in output
380 			if (!ok)
381 				okAll = false;
382 			spans[runIx] = MonoTime.currTime() - start;
383 		}
384 		results._inTime = spans[].minElement();
385 		writef(",	   in: "~formatNsPerOp~" ns/op (%s)",
386 			   cast(double)(results._inTime).total!"nsecs" / results.elementCount,
387 			   okAll ? "OK" : "ERR");
388 	}
389 
390 	// rehash (AAs) + in
391 	static if (is(typeof(a.rehash()))) {
392 		// rehash
393 		foreach (const runIx; 0 .. runCount) {
394 			const start = MonoTime.currTime();
395 			a.rehash();
396 			spans[runIx] = MonoTime.currTime() - start;
397 		}
398 		results._rehashTime = spans[].minElement();
399 		writef(", rehash: "~formatNsPerOp,
400 			   cast(double)(results._rehashTime).total!"nsecs" / results.elementCount);
401 		// in
402 		foreach (const runIx; 0 .. runCount) {
403 			const start = MonoTime.currTime();
404 			foreach (immutable i; testSource)
405 				const hit = keys[i] in a;
406 			spans[runIx] = MonoTime.currTime() - start;
407 		}
408 		results._inAfterRehashTime = spans[].minElement();
409 		writef(", in (after rehash): "~formatNsPerOp,
410 			   cast(double)(results._inAfterRehashTime).total!"nsecs" / results.elementCount);
411 	}
412 
413 	A _ = makeWithRequestedCapacity!(A)(0, reserveSupport);
414 	if (reserveSupport) {
415 		bool unsupported = false;
416 		// insert/opIndex with preallocation via void capacity(size_t)
417 		foreach (const runIx; 0 .. runCount) {
418 			A b = makeWithRequestedCapacity!(A)(results.elementCount, reserveSupport);
419 			const start = MonoTime.currTime();
420 			foreach (immutable i; testSource) {
421 				static if (__traits(hasMember, A, "insert")) {
422 					static if (is(immutable A.KeyType == immutable Address))
423 						const key = Address(keys[i] + 1); // avoid `Address.nullValue`
424 					else
425 						const key = keys[i];
426 					static if (is(typeof(b.insert(key, ValueType.init))))
427 						b.insert(key, ValueType.init);
428 					else static if (is(typeof(b.insert(ElementType(key, ValueType.init)))))
429 						b.insert(ElementType(key, ValueType.init));
430 					else
431 					{
432 						pragma(msg, "Skipping unsupported insert for " ~ A.stringof);
433 						unsupported = true;
434 					}
435 				}
436 				else
437 					b[keys[i]] = ValueType.init;
438 			}
439 			spans[runIx] = MonoTime.currTime() - start;
440 			static if (__traits(hasMember, A, `clear`) &&
441 					   is(typeof(b.clear())))
442 				b.clear();
443 		}
444 		if (!unsupported) {
445 			results._insertWithoutGrowthTime = spans[].minElement();
446 			writef(", insert (no growth): "~formatNsPerOp,
447 				   cast(double)(results._insertWithoutGrowthTime).total!"nsecs" / results.elementCount);
448 		}
449 	}
450 
451 	writef(` for %s`, A.stringof);
452 
453 	static if (__traits(hasMember, A, `binCounts`))
454 		writef(" %s", a.binCounts());
455 	static if (__traits(hasMember, A, `smallBinCapacity`))
456 		writef(" smallBinCapacity:%s", A.smallBinCapacity);
457 	static if (__traits(hasMember, A, `totalProbeCount`))
458 		writef(" averageProbeCount:%s", cast(double)a.totalProbeCount/a.length);
459 
460 	writeln();
461 
462 	return results;
463 }
464 
465 alias benchmarkAssociativeArray = benchmarkMap;
466 
467 /** Make `A` and try setting its capacity to `results.elementCount`.
468  */
469 auto makeWithRequestedCapacity(A)(size_t elementCount, out ReserveSupport reserveSupport) {
470 	static if (is(typeof(A.init.withCapacity(elementCount)))) {
471 		reserveSupport = ReserveSupport.yes;
472 		return A.withCapacity(elementCount);
473 	}
474 	else
475 	{
476 		static if (is(A == class))
477 			auto a = new A();
478 		else static if (is(A == struct))
479 			auto a = A();
480 		else
481 			A a;
482 
483 		static if (__traits(hasMember, A, `reserve`)) {
484 			static if (__traits(compiles, { a.reserve(elementCount); })) {
485 				a.reserve(elementCount);
486 				reserveSupport = ReserveSupport.yes;
487 			}
488 			else static if (__traits(compiles, { a.reserve!uint(elementCount); })) {
489 				a.reserve!uint(elementCount);
490 				reserveSupport = ReserveSupport.yes;
491 			}
492 		}
493 		else static if (is(A == U[], U)) {
494 			// this doesn’t work aswell:
495 			// import std.range.primitives : ElementType;
496 			// return new ElementType!A[elementCount];
497 			a.reserve(elementCount); // See_Also: https://dlang.org/library/object/reserve.html
498 			reserveSupport = ReserveSupport.yes;
499 			return a;
500 		}
501 		else static if (is(A : V[K], K, V)) {
502 			static if (__traits(compiles, { a.reserve(elementCount); })) {
503 				a.reserve(elementCount); // builtin AA’s might get this later on
504 				reserveSupport = ReserveSupport.yes;
505 			}
506 		}
507 		else static if (is(typeof(a.reserve(elementCount)))) {
508 			a.reserve(elementCount);
509 			reserveSupport = ReserveSupport.yes;
510 		}
511 
512 		static if (__traits(isPOD, A))
513 			return a;
514 		else
515 		{
516 			import std.algorithm.mutation : move;
517 			return move(a);
518 		}
519 	}
520 }
521 
522 private T[] iotaArrayOf(T)(in size_t begin, in size_t end) {
523 	import std.typecons : Nullable;
524 
525 	typeof(return) es = new T[end];
526 	foreach (immutable i; begin .. end) {
527 		static if (is(typeof(T(i)))) // if possible
528 			es[i] = T(i);	   // try normal construction
529 		else {
530 			import std.conv : to;
531 			static if (is(typeof(T(i))))
532 				es[i] = T(i);
533 			else static if (is(typeof(T(i.to!string))))
534 				es[i] = T(i.to!string);
535 			else static if (is(typeof(i.to!T)))
536 				es[i] = i.to!T;
537 			else static if (is(T == Nullable!(uint, uint.max))) /+ TODO: avoid this +/
538 				es[i] = T(cast(uint)i);
539 			else
540 				static assert(0, "Cannot convert `" ~ typeof(i).stringof ~ "` to `" ~ T.stringof ~ "`");
541 		}
542 	}
543 	return es;
544 }