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