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 }