The OpenD Programming Language

1 // Written in the D programming language.
2 /**
3 Allocator that collects useful statistics about allocations, both global and per
4 calling point. The statistics collected can be configured statically by choosing
5 combinations of `Options` appropriately.
6 
7 Source: $(PHOBOSSRC std/experimental/allocator/building_blocks/stats_collector.d)
8 */
9 module std.experimental.allocator.building_blocks.stats_collector;
10 
11 ///
12 @safe unittest
13 {
14     import std.experimental.allocator.gc_allocator : GCAllocator;
15     import std.experimental.allocator.building_blocks.free_list : FreeList;
16     alias Allocator = StatsCollector!(GCAllocator, Options.bytesUsed);
17 }
18 
19 import std.experimental.allocator.common;
20 
21 /**
22 _Options for `StatsCollector` defined below. Each enables during
23 compilation one specific counter, statistic, or other piece of information.
24 */
25 enum Options : ulong
26 {
27     /**
28     Counts the number of calls to `owns`.
29     */
30     numOwns = 1u << 0,
31     /**
32     Counts the number of calls to `allocate`. All calls are counted,
33     including requests for zero bytes or failed requests.
34     */
35     numAllocate = 1u << 1,
36     /**
37     Counts the number of calls to `allocate` that succeeded, i.e. they
38     returned a block as large as requested. (N.B. requests for zero bytes count
39     as successful.)
40     */
41     numAllocateOK = 1u << 2,
42     /**
43     Counts the number of calls to `expand`, regardless of arguments or
44     result.
45     */
46     numExpand = 1u << 3,
47     /**
48     Counts the number of calls to `expand` that resulted in a successful
49     expansion.
50     */
51     numExpandOK = 1u << 4,
52     /**
53     Counts the number of calls to `reallocate`, regardless of arguments or
54     result.
55     */
56     numReallocate = 1u << 5,
57     /**
58     Counts the number of calls to `reallocate` that succeeded.
59     (Reallocations to zero bytes count as successful.)
60     */
61     numReallocateOK = 1u << 6,
62     /**
63     Counts the number of calls to `reallocate` that resulted in an in-place
64     reallocation (no memory moved). If this number is close to the total number
65     of reallocations, that indicates the allocator finds room at the current
66     block's end in a large fraction of the cases, but also that internal
67     fragmentation may be high (the size of the unit of allocation is large
68     compared to the typical allocation size of the application).
69     */
70     numReallocateInPlace = 1u << 7,
71     /**
72     Counts the number of calls to `deallocate`.
73     */
74     numDeallocate = 1u << 8,
75     /**
76     Counts the number of calls to `deallocateAll`.
77     */
78     numDeallocateAll = 1u << 9,
79     /**
80     Counts the number of calls to `alignedAllocate`. All calls are counted,
81     including requests for zero bytes or failed requests.
82     */
83     numAlignedAllocate = 1u << 10,
84     /**
85     Counts the number of calls to `alignedAllocate` that succeeded, i.e. they
86     returned a block as large as requested. (N.B. requests for zero bytes count
87     as successful.)
88     */
89     numAlignedAllocateOk = 1u << 11,
90     /**
91     Chooses all `numXxx` flags.
92     */
93     numAll = (1u << 12) - 1,
94     /**
95     Tracks bytes currently allocated by this allocator. This number goes up
96     and down as memory is allocated and deallocated, and is zero if the
97     allocator currently has no active allocation.
98     */
99     bytesUsed = 1u << 12,
100     /**
101     Tracks total cumulative bytes allocated by means of `allocate`,
102     `expand`, and `reallocate` (when resulting in an expansion). This
103     number always grows and indicates allocation traffic. To compute bytes
104     deallocated cumulatively, subtract `bytesUsed` from `bytesAllocated`.
105     */
106     bytesAllocated = 1u << 13,
107     /**
108     Tracks the sum of all `delta` values in calls of the form
109     $(D expand(b, delta)) that succeed (return `true`).
110     */
111     bytesExpanded = 1u << 14,
112     /**
113     Tracks the sum of all $(D b.length - s) with $(D b.length > s) in calls of
114     the form $(D realloc(b, s)) that succeed (return `true`). In per-call
115     statistics, also unambiguously counts the bytes deallocated with
116     `deallocate`.
117     */
118     bytesContracted = 1u << 15,
119     /**
120     Tracks the sum of all bytes moved as a result of calls to `realloc` that
121     were unable to reallocate in place. A large number (relative to $(D
122     bytesAllocated)) indicates that the application should use larger
123     preallocations.
124     */
125     bytesMoved = 1u << 16,
126     /**
127     Tracks the sum of all bytes NOT moved as result of calls to `realloc`
128     that managed to reallocate in place. A large number (relative to $(D
129     bytesAllocated)) indicates that the application is expansion-intensive and
130     is saving a good amount of moves. However, if this number is relatively
131     small and `bytesSlack` is high, it means the application is
132     overallocating for little benefit.
133     */
134     bytesNotMoved = 1u << 17,
135     /**
136     Measures the sum of extra bytes allocated beyond the bytes requested, i.e.
137     the $(HTTP goo.gl/YoKffF, internal fragmentation). This is the current
138     effective number of slack bytes, and it goes up and down with time.
139     */
140     bytesSlack = 1u << 18,
141     /**
142     Measures the maximum bytes allocated over the time. This is useful for
143     dimensioning allocators.
144     */
145     bytesHighTide = 1u << 19,
146     /**
147     Chooses all `byteXxx` flags.
148     */
149     bytesAll = ((1u << 20) - 1) & ~numAll,
150     /**
151     Combines all flags above.
152     */
153     all = (1u << 20) - 1
154 }
155 
156 /**
157 
158 Allocator that collects extra data about allocations. Since each piece of
159 information adds size and time overhead, statistics can be individually enabled
160 or disabled through compile-time `flags`.
161 
162 All stats of the form `numXxx` record counts of events occurring, such as
163 calls to functions and specific results. The stats of the form `bytesXxx`
164 collect cumulative sizes.
165 
166 In addition, the data `callerSize`, `callerModule`, `callerFile`, $(D
167 callerLine), and `callerTime` is associated with each specific allocation.
168 This data prefixes each allocation.
169 
170 */
171 struct StatsCollector(Allocator, ulong flags = Options.all,
172     ulong perCallFlags = 0)
173 {
174 private:
175     import std.traits : hasMember, Signed;
176     import std.typecons : Ternary;
177 
178     static string define(string type, string[] names...)
179     {
180         string result;
181         foreach (v; names)
182             result ~= "static if (flags & Options."~v~") {"
183                 ~ "private "~type~" _"~v~";"
184                 ~ "public const("~type~") "~v~"() const { return _"~v~"; }"
185                 ~ "}";
186         return result;
187     }
188 
189     void add(string counter)(Signed!size_t n)
190     {
191         mixin("static if (flags & Options." ~ counter
192             ~ ") _" ~ counter ~ " += n;");
193         static if (counter == "bytesUsed" && (flags & Options.bytesHighTide))
194         {
195             if (bytesHighTide < bytesUsed ) _bytesHighTide = bytesUsed;
196         }
197     }
198 
199     void up(string counter)() { add!counter(1); }
200     void down(string counter)() { add!counter(-1); }
201 
202     version (StdDdoc)
203     {
204         /**
205         Read-only properties enabled by the homonym `flags` chosen by the
206         user.
207 
208         Example:
209         ----
210         StatsCollector!(Mallocator,
211             Options.bytesUsed | Options.bytesAllocated) a;
212         auto d1 = a.allocate(10);
213         auto d2 = a.allocate(11);
214         a.deallocate(d1);
215         assert(a.bytesAllocated == 21);
216         assert(a.bytesUsed == 11);
217         a.deallocate(d2);
218         assert(a.bytesAllocated == 21);
219         assert(a.bytesUsed == 0);
220         ----
221         */
222         @property ulong numOwns() const;
223         /// Ditto
224         @property ulong numAllocate() const;
225         /// Ditto
226         @property ulong numAllocateOK() const;
227         /// Ditto
228         @property ulong numExpand() const;
229         /// Ditto
230         @property ulong numExpandOK() const;
231         /// Ditto
232         @property ulong numReallocate() const;
233         /// Ditto
234         @property ulong numReallocateOK() const;
235         /// Ditto
236         @property ulong numReallocateInPlace() const;
237         /// Ditto
238         @property ulong numDeallocate() const;
239         /// Ditto
240         @property ulong numDeallocateAll() const;
241         /// Ditto
242         @property ulong numAlignedAllocate() const;
243         /// Ditto
244         @property ulong numAlignedAllocateOk() const;
245         /// Ditto
246         @property ulong bytesUsed() const;
247         /// Ditto
248         @property ulong bytesAllocated() const;
249         /// Ditto
250         @property ulong bytesExpanded() const;
251         /// Ditto
252         @property ulong bytesContracted() const;
253         /// Ditto
254         @property ulong bytesMoved() const;
255         /// Ditto
256         @property ulong bytesNotMoved() const;
257         /// Ditto
258         @property ulong bytesSlack() const;
259         /// Ditto
260         @property ulong bytesHighTide() const;
261     }
262 
263 public:
264     /**
265     The parent allocator is publicly accessible either as a direct member if it
266     holds state, or as an alias to `Allocator.instance` otherwise. One may use
267     it for making calls that won't count toward statistics collection.
268     */
269     static if (stateSize!Allocator) Allocator parent;
270     else alias parent = Allocator.instance;
271 
272 private:
273     // Per-allocator state
274     mixin(define("ulong",
275         "numOwns",
276         "numAllocate",
277         "numAllocateOK",
278         "numExpand",
279         "numExpandOK",
280         "numReallocate",
281         "numReallocateOK",
282         "numReallocateInPlace",
283         "numDeallocate",
284         "numDeallocateAll",
285         "numAlignedAllocate",
286         "numAlignedAllocateOk",
287         "bytesUsed",
288         "bytesAllocated",
289         "bytesExpanded",
290         "bytesContracted",
291         "bytesMoved",
292         "bytesNotMoved",
293         "bytesSlack",
294         "bytesHighTide",
295     ));
296 
297 public:
298 
299     /// Alignment offered is equal to `Allocator.alignment`.
300     alias alignment = Allocator.alignment;
301 
302     /**
303     Increments `numOwns` (per instance and and per call) and forwards to $(D
304     parent.owns(b)).
305     */
306     static if (hasMember!(Allocator, "owns"))
307     {
308         static if ((perCallFlags & Options.numOwns) == 0)
309         Ternary owns(void[] b)
310         { return ownsImpl(b); }
311         else
312         pragma(inline, true) // LDC: Must inline because of __FILE__ as template parameter
313         Ternary owns(string f = __FILE__, uint n = __LINE__)(void[] b)
314         { return ownsImpl!(f, n)(b); }
315     }
316 
317     private Ternary ownsImpl(string f = null, uint n = 0)(void[] b)
318     {
319         up!"numOwns";
320         addPerCall!(f, n, "numOwns")(1);
321         return parent.owns(b);
322     }
323 
324     /**
325     Forwards to `parent.allocate`. Affects per instance: `numAllocate`,
326     `bytesUsed`, `bytesAllocated`, `bytesSlack`, `numAllocateOK`,
327     and `bytesHighTide`. Affects per call: `numAllocate`, $(D
328     numAllocateOK), and `bytesAllocated`.
329     */
330     static if (!(perCallFlags
331         & (Options.numAllocate | Options.numAllocateOK
332             | Options.bytesAllocated)))
333     {
334         void[] allocate(size_t n)
335         { return allocateImpl(n); }
336     }
337     else
338     {
339         pragma(inline, true) // LDC: Must inline because of __FILE__ as template parameter
340         void[] allocate(string f = __FILE__, ulong n = __LINE__)
341             (size_t bytes)
342         { return allocateImpl!(f, n)(bytes); }
343     }
344 
345     // Common code currently shared between allocateImpl and allocateZeroedImpl.
346     private enum _updateStatsForAllocateResult =
347     q{
348         add!"bytesUsed"(result.length);
349         add!"bytesAllocated"(result.length);
350         immutable slack = this.goodAllocSize(result.length) - result.length;
351         add!"bytesSlack"(slack);
352         up!"numAllocate";
353         add!"numAllocateOK"(result.length == bytes); // allocating 0 bytes is OK
354         addPerCall!(f, n, "numAllocate", "numAllocateOK", "bytesAllocated")
355             (1, result.length == bytes, result.length);
356     };
357 
358     private void[] allocateImpl(string f = null, ulong n = 0)(size_t bytes)
359     {
360         auto result = parent.allocate(bytes);
361         mixin(_updateStatsForAllocateResult);
362         return result;
363     }
364 
365     static if (hasMember!(Allocator, "allocateZeroed"))
366     {
367         static if (!(perCallFlags
368             & (Options.numAllocate | Options.numAllocateOK
369                 | Options.bytesAllocated)))
370         {
371             package(std) void[] allocateZeroed()(size_t n)
372             { return allocateZeroedImpl(n); }
373         }
374         else
375         {
376             pragma(inline, true) // LDC: Must inline because of __FILE__ as template parameter
377             package(std) void[] allocateZeroed(string f = __FILE__, ulong n = __LINE__)
378                 (size_t bytes)
379             { return allocateZeroedImpl!(f, n)(bytes); }
380         }
381 
382         private void[] allocateZeroedImpl(string f = null, ulong n = 0)(size_t bytes)
383         {
384             auto result = parent.allocateZeroed(bytes);
385             // Note: calls to `allocateZeroed` are counted for statistical purposes
386             // as if they were calls to `allocate`. If/when `allocateZeroed` is made
387             // public it might be of interest to count such calls separately.
388             mixin(_updateStatsForAllocateResult);
389             return result;
390         }
391     }
392 
393     /**
394     Forwards to `parent.alignedAllocate`. Affects per instance: `numAlignedAllocate`,
395     `bytesUsed`, `bytesAllocated`, `bytesSlack`, `numAlignedAllocateOk`,
396     and `bytesHighTide`. Affects per call: `numAlignedAllocate`, `numAlignedAllocateOk`,
397     and `bytesAllocated`.
398     */
399     static if (!(perCallFlags
400         & (Options.numAlignedAllocate | Options.numAlignedAllocateOk
401             | Options.bytesAllocated)))
402     {
403         void[] alignedAllocate(size_t n, uint a)
404         { return alignedAllocateImpl(n, a); }
405     }
406     else
407     {
408         pragma(inline, true) // LDC: Must inline because of __FILE__ as template parameter
409         void[] alignedAllocate(string f = __FILE__, ulong n = __LINE__)
410             (size_t bytes, uint a)
411         { return alignedAllocateImpl!(f, n)(bytes, a); }
412     }
413 
414     private void[] alignedAllocateImpl(string f = null, ulong n = 0)(size_t bytes, uint a)
415     {
416         up!"numAlignedAllocate";
417         static if (!hasMember!(Allocator, "alignedAllocate"))
418         {
419             if (bytes == 0)
420                 up!"numAlignedAllocateOk";
421             void[] result = null;
422         }
423         else
424         {
425             auto result = parent.alignedAllocate(bytes, a);
426             add!"bytesUsed"(result.length);
427             add!"bytesAllocated"(result.length);
428             immutable slack = this.goodAllocSize(result.length) - result.length;
429             add!"bytesSlack"(slack);
430             add!"numAlignedAllocateOk"(result.length == bytes); // allocating 0 bytes is OK
431         }
432         addPerCall!(f, n, "numAlignedAllocate", "numAlignedAllocateOk", "bytesAllocated")
433             (1, result.length == bytes, result.length);
434 
435         return result;
436     }
437 
438     /**
439     Defined whether or not `Allocator.expand` is defined. Affects
440     per instance: `numExpand`, `numExpandOK`, `bytesExpanded`,
441     `bytesSlack`, `bytesAllocated`, and `bytesUsed`. Affects per call:
442     `numExpand`, `numExpandOK`, `bytesExpanded`, and
443     `bytesAllocated`.
444     */
445     static if (!(perCallFlags
446         & (Options.numExpand | Options.numExpandOK | Options.bytesExpanded)))
447     {
448         bool expand(ref void[] b, size_t delta)
449         { return expandImpl(b, delta); }
450     }
451     else
452     {
453         pragma(inline, true) // LDC: Must inline because of __FILE__ as template parameter
454         bool expand(string f = __FILE__, uint n = __LINE__)
455             (ref void[] b, size_t delta)
456         { return expandImpl!(f, n)(b, delta); }
457     }
458 
459     private bool expandImpl(string f = null, uint n = 0)(ref void[] b, size_t s)
460     {
461         up!"numExpand";
462         Signed!size_t slack = 0;
463         static if (!hasMember!(Allocator, "expand"))
464         {
465             auto result = s == 0;
466         }
467         else
468         {
469             immutable bytesSlackB4 = this.goodAllocSize(b.length) - b.length;
470             auto result = parent.expand(b, s);
471             if (result)
472             {
473                 up!"numExpandOK";
474                 add!"bytesUsed"(s);
475                 add!"bytesAllocated"(s);
476                 add!"bytesExpanded"(s);
477                 slack = Signed!size_t(this.goodAllocSize(b.length) - b.length
478                     - bytesSlackB4);
479                 add!"bytesSlack"(slack);
480             }
481         }
482         immutable xtra = result ? s : 0;
483         addPerCall!(f, n, "numExpand", "numExpandOK", "bytesExpanded",
484             "bytesAllocated")
485             (1, result, xtra, xtra);
486         return result;
487     }
488 
489     /**
490     Defined whether or not `Allocator.reallocate` is defined. Affects
491     per instance: `numReallocate`, `numReallocateOK`, $(D
492     numReallocateInPlace), `bytesNotMoved`, `bytesAllocated`, $(D
493     bytesSlack), `bytesExpanded`, and `bytesContracted`. Affects per call:
494     `numReallocate`, `numReallocateOK`, `numReallocateInPlace`,
495     `bytesNotMoved`, `bytesExpanded`, `bytesContracted`, and
496     `bytesMoved`.
497     */
498     static if (!(perCallFlags
499         & (Options.numReallocate | Options.numReallocateOK
500             | Options.numReallocateInPlace | Options.bytesNotMoved
501             | Options.bytesExpanded | Options.bytesContracted
502             | Options.bytesMoved)))
503     {
504         bool reallocate(ref void[] b, size_t s)
505         { return reallocateImpl(b, s); }
506     }
507     else
508     {
509         pragma(inline, true) // LDC: Must inline because of __FILE__ as template parameter
510         bool reallocate(string f = __FILE__, ulong n = __LINE__)
511             (ref void[] b, size_t s)
512         { return reallocateImpl!(f, n)(b, s); }
513     }
514 
515     private bool reallocateImpl(string f = null, uint n = 0)
516         (ref void[] b, size_t s)
517     {
518         up!"numReallocate";
519         const bytesSlackB4 = this.goodAllocSize(b.length) - b.length;
520         const oldB = b.ptr;
521         const oldLength = b.length;
522 
523         const result = parent.reallocate(b, s);
524 
525         Signed!size_t slack = 0;
526         bool wasInPlace = false;
527         Signed!size_t delta = 0;
528 
529         if (result)
530         {
531             up!"numReallocateOK";
532             slack = (this.goodAllocSize(b.length) - b.length) - bytesSlackB4;
533             add!"bytesSlack"(slack);
534             add!"bytesUsed"(Signed!size_t(b.length - oldLength));
535             if (oldB == b.ptr)
536             {
537                 // This was an in-place reallocation, yay
538                 wasInPlace = true;
539                 up!"numReallocateInPlace";
540                 add!"bytesNotMoved"(oldLength);
541                 delta = b.length - oldLength;
542                 if (delta >= 0)
543                 {
544                     // Expansion
545                     add!"bytesAllocated"(delta);
546                     add!"bytesExpanded"(delta);
547                 }
548                 else
549                 {
550                     // Contraction
551                     add!"bytesContracted"(-delta);
552                 }
553             }
554             else
555             {
556                 // This was a allocate-move-deallocate cycle
557                 add!"bytesAllocated"(b.length);
558                 add!"bytesMoved"(oldLength);
559             }
560         }
561         addPerCall!(f, n, "numReallocate", "numReallocateOK",
562             "numReallocateInPlace", "bytesNotMoved",
563             "bytesExpanded", "bytesContracted", "bytesMoved")
564             (1, result, wasInPlace, wasInPlace ? oldLength : 0,
565                 delta >= 0 ? delta : 0, delta < 0 ? -delta : 0,
566                 wasInPlace ? 0 : oldLength);
567         return result;
568     }
569 
570     /**
571     Defined whether or not `Allocator.deallocate` is defined. Affects
572     per instance: `numDeallocate`, `bytesUsed`, and `bytesSlack`.
573     Affects per call: `numDeallocate` and `bytesContracted`.
574     */
575     static if (!(perCallFlags &
576             (Options.numDeallocate | Options.bytesContracted)))
577         bool deallocate(void[] b)
578         { return deallocateImpl(b); }
579     else
580         pragma(inline, true) // LDC: Must inline because of __FILE__ as template parameter
581         bool deallocate(string f = __FILE__, uint n = __LINE__)(void[] b)
582         { return deallocateImpl!(f, n)(b); }
583 
584     private bool deallocateImpl(string f = null, uint n = 0)(void[] b)
585     {
586         up!"numDeallocate";
587         add!"bytesUsed"(-Signed!size_t(b.length));
588         add!"bytesSlack"(-(this.goodAllocSize(b.length) - b.length));
589         addPerCall!(f, n, "numDeallocate", "bytesContracted")(1, b.length);
590         static if (hasMember!(Allocator, "deallocate"))
591             return parent.deallocate(b);
592         else
593             return false;
594     }
595 
596     static if (hasMember!(Allocator, "deallocateAll"))
597     {
598         /**
599         Defined only if `Allocator.deallocateAll` is defined. Affects
600         per instance and per call `numDeallocateAll`.
601         */
602         static if (!(perCallFlags & Options.numDeallocateAll))
603             bool deallocateAll()
604             { return deallocateAllImpl(); }
605         else
606             pragma(inline, true) // LDC: Must inline because of __FILE__ as template parameter
607             bool deallocateAll(string f = __FILE__, uint n = __LINE__)()
608             { return deallocateAllImpl!(f, n)(); }
609 
610         private bool deallocateAllImpl(string f = null, uint n = 0)()
611         {
612             up!"numDeallocateAll";
613             addPerCall!(f, n, "numDeallocateAll")(1);
614             static if ((flags & Options.bytesUsed))
615                 _bytesUsed = 0;
616             return parent.deallocateAll();
617         }
618     }
619 
620     /**
621     Defined only if `Options.bytesUsed` is defined. Returns $(D bytesUsed ==
622     0).
623     */
624     static if (flags & Options.bytesUsed)
625     pure nothrow @safe @nogc
626     Ternary empty()
627     {
628         return Ternary(_bytesUsed == 0);
629     }
630 
631     /**
632     Reports per instance statistics to `output` (e.g. `stdout`). The
633     format is simple: one kind and value per line, separated by a colon, e.g.
634     `bytesAllocated:7395404`
635     */
636     void reportStatistics(R)(auto ref R output)
637     {
638         import std.conv : to;
639         import std.traits : EnumMembers;
640         foreach (e; EnumMembers!Options)
641         {
642             static if ((flags & e) && e != Options.numAll
643                     && e != Options.bytesAll && e != Options.all)
644                 output.write(e.to!string, ":", mixin(e.to!string), '\n');
645         }
646     }
647 
648     static if (perCallFlags)
649     {
650         /**
651         Defined if `perCallFlags` is nonzero.
652         */
653         struct PerCallStatistics
654         {
655             /// The file and line of the call.
656             string file;
657             /// Ditto
658             uint line;
659             /// The options corresponding to the statistics collected.
660             Options[] opts;
661             /// The values of the statistics. Has the same length as `opts`.
662             ulong[] values;
663             // Next in the chain.
664             private PerCallStatistics* next;
665 
666             /**
667             Format to a string such as:
668             $(D mymodule.d(655): [numAllocate:21, numAllocateOK:21, bytesAllocated:324202]).
669             */
670             string toString() const
671             {
672                 import std.conv : text, to;
673                 auto result = text(file, "(", line, "): [");
674                 foreach (i, opt; opts)
675                 {
676                     if (i) result ~= ", ";
677                     result ~= opt.to!string;
678                     result ~= ':';
679                     result ~= values[i].to!string;
680                 }
681                 return result ~= "]";
682             }
683         }
684         private static PerCallStatistics* root;
685 
686         /**
687         Defined if `perCallFlags` is nonzero. Iterates all monitored
688         file/line instances. The order of iteration is not meaningful (items
689         are inserted at the front of a list upon the first call), so
690         preprocessing the statistics after collection might be appropriate.
691         */
692         static auto byFileLine()
693         {
694             static struct Voldemort
695             {
696                 PerCallStatistics* current;
697                 bool empty() { return !current; }
698                 ref PerCallStatistics front() { return *current; }
699                 void popFront() { current = current.next; }
700                 auto save() { return this; }
701             }
702             return Voldemort(root);
703         }
704 
705         /**
706         Defined if `perCallFlags` is nonzero. Outputs (e.g. to a `File`)
707         a simple report of the collected per-call statistics.
708         */
709         static void reportPerCallStatistics(R)(auto ref R output)
710         {
711             output.write("Stats for: ", StatsCollector.stringof, '\n');
712             foreach (ref stat; byFileLine)
713             {
714                 output.write(stat, '\n');
715             }
716         }
717 
718         private PerCallStatistics* statsAt(string f, uint n, opts...)()
719         {
720             import std.array : array;
721             import std.range : repeat;
722 
723             static PerCallStatistics s = { f, n, [ opts ],
724                 repeat(0UL, opts.length).array };
725             static bool inserted;
726 
727             if (!inserted)
728             {
729                 // Insert as root
730                 s.next = root;
731                 root = &s;
732                 inserted = true;
733             }
734             return &s;
735         }
736 
737         private void addPerCall(string f, uint n, names...)(ulong[] values...)
738         {
739             import std.array : join;
740             enum ulong mask = mixin("Options."~[names].join("|Options."));
741             static if (perCallFlags & mask)
742             {
743                 // Per allocation info
744                 auto ps = mixin("statsAt!(f, n,"
745                     ~ "Options."~[names].join(", Options.")
746                 ~")");
747                 foreach (i; 0 .. names.length)
748                 {
749                     ps.values[i] += values[i];
750                 }
751             }
752         }
753     }
754     else
755     {
756         private void addPerCall(string f, uint n, names...)(ulong[]...)
757         {
758         }
759     }
760 }
761 
762 ///
763 @system unittest
764 {
765     import std.experimental.allocator.building_blocks.free_list : FreeList;
766     import std.experimental.allocator.gc_allocator : GCAllocator;
767     alias Allocator = StatsCollector!(GCAllocator, Options.all, Options.all);
768 
769     Allocator alloc;
770     auto b = alloc.allocate(10);
771     alloc.reallocate(b, 20);
772     alloc.deallocate(b);
773 
774     import std.file : deleteme, remove;
775     import std.range : walkLength;
776     import std.stdio : File;
777 
778     auto f = deleteme ~ "-dlang.std.experimental.allocator.stats_collector.txt";
779     scope(exit) remove(f);
780     Allocator.reportPerCallStatistics(File(f, "w"));
781     alloc.reportStatistics(File(f, "a"));
782     assert(File(f).byLine.walkLength == 24);
783 }
784 
785 @system unittest
786 {
787     void test(Allocator)()
788     {
789         import std.range : walkLength;
790         import std.typecons : Ternary;
791 
792         Allocator a;
793         assert((() pure nothrow @safe @nogc => a.empty)() == Ternary.yes);
794         auto b1 = a.allocate(100);
795         assert(a.numAllocate == 1);
796         assert((() nothrow @safe => a.expand(b1, 0))());
797         assert(a.reallocate(b1, b1.length + 1));
798         auto b2 = a.allocate(101);
799         assert(a.numAllocate == 2);
800         assert(a.bytesAllocated == 202);
801         assert(a.bytesUsed == 202);
802         auto b3 = a.allocate(202);
803         assert(a.numAllocate == 3);
804         assert(a.bytesAllocated == 404);
805         assert((() pure nothrow @safe @nogc => a.empty)() == Ternary.no);
806 
807         () nothrow @nogc { a.deallocate(b2); }();
808         assert(a.numDeallocate == 1);
809         () nothrow @nogc { a.deallocate(b1); }();
810         assert(a.numDeallocate == 2);
811         () nothrow @nogc { a.deallocate(b3); }();
812         assert(a.numDeallocate == 3);
813         assert(a.numAllocate == a.numDeallocate);
814         assert(a.bytesUsed == 0);
815      }
816 
817     import std.experimental.allocator.building_blocks.free_list : FreeList;
818     import std.experimental.allocator.gc_allocator : GCAllocator;
819     test!(StatsCollector!(GCAllocator, Options.all, Options.all));
820     test!(StatsCollector!(FreeList!(GCAllocator, 128), Options.all,
821         Options.all));
822 }
823 
824 @system unittest
825 {
826     void test(Allocator)()
827     {
828         import std.range : walkLength;
829         Allocator a;
830         auto b1 = a.allocate(100);
831         assert((() nothrow @safe => a.expand(b1, 0))());
832         assert(a.reallocate(b1, b1.length + 1));
833         auto b2 = a.allocate(101);
834         auto b3 = a.allocate(202);
835 
836         () nothrow @nogc { a.deallocate(b2); }();
837         () nothrow @nogc { a.deallocate(b1); }();
838         () nothrow @nogc { a.deallocate(b3); }();
839     }
840     import std.experimental.allocator.building_blocks.free_list : FreeList;
841     import std.experimental.allocator.gc_allocator : GCAllocator;
842     test!(StatsCollector!(GCAllocator, 0, 0));
843 }
844 
845 @system unittest
846 {
847     import std.experimental.allocator.gc_allocator : GCAllocator;
848     StatsCollector!(GCAllocator, 0, 0) a;
849 
850     // calls std.experimental.allocator.common.goodAllocSize
851     assert((() pure nothrow @safe @nogc => a.goodAllocSize(1))());
852 }
853 
854 @system unittest
855 {
856     import std.experimental.allocator.building_blocks.region : BorrowedRegion;
857 
858     auto a = StatsCollector!(BorrowedRegion!(), Options.all, Options.all)(BorrowedRegion!()(new ubyte[1024 * 64]));
859     auto b = a.allocate(42);
860     assert(b.length == 42);
861     // Test that reallocate infers from parent
862     assert((() nothrow @nogc => a.reallocate(b, 100))());
863     assert(b.length == 100);
864     // Test that deallocateAll infers from parent
865     assert((() nothrow @nogc => a.deallocateAll())());
866 }
867 
868 @system unittest
869 {
870     import std.experimental.allocator.building_blocks.region : BorrowedRegion;
871 
872     auto a = StatsCollector!(BorrowedRegion!(), Options.all)(BorrowedRegion!()(new ubyte[1024 * 64]));
873     auto b = a.alignedAllocate(42, 128);
874     assert(b.length == 42);
875     assert(b.ptr.alignedAt(128));
876     assert(a.numAlignedAllocate == 1);
877     assert(a.numAlignedAllocateOk == 1);
878     assert(a.bytesUsed == 42);
879 
880     b = a.alignedAllocate(23, 256);
881     assert(b.length == 23);
882     assert(b.ptr.alignedAt(256));
883     assert(a.numAlignedAllocate == 2);
884     assert(a.numAlignedAllocateOk == 2);
885     assert(a.bytesUsed == 65);
886 
887     b = a.alignedAllocate(0, 512);
888     assert(b.length == 0);
889     assert(a.numAlignedAllocate == 3);
890     assert(a.numAlignedAllocateOk == 3);
891     assert(a.bytesUsed == 65);
892 
893     b = a.alignedAllocate(1024 * 1024, 512);
894     assert(b is null);
895     assert(a.numAlignedAllocate == 4);
896     assert(a.numAlignedAllocateOk == 3);
897     assert(a.bytesUsed == 65);
898 }