1 /** 2 Implements an expanding circular buffer that places elements on an array of 3 blocks. 4 */ 5 6 module alid.circularblocks; 7 8 import alid.errornogc; 9 10 private mixin NogcError!"circularblocks"; 11 12 /** 13 Represents an expanding circular buffer implemented as blocks of elements of 14 `T`. 15 16 The elements are added to the end, removed from the front, and are never 17 moved around on the buffers. 18 19 If user blocks are provided, those blocks are used for the elements until 20 there is no more room on them, in which case new blocks are allocated from 21 the GC heap. 22 23 Params: 24 25 T = the type of the elements to store 26 */ 27 struct CircularBlocks(T) 28 { 29 private @disable this(); 30 private @disable this(this); 31 32 private: 33 34 import alid.blockreusable : ReusableBlock; 35 36 ReusableBlock!T[] blocks; // Where elements are stored 37 const(T*)[] userBlocks; // The .ptr values of the user-provided blocks, 38 // if any 39 40 size_t heapBlockCapacity; // The desired capacity for heap blocks 41 42 size_t tailBlock = 0; // The block new elements are being added to 43 size_t capacity_ = 0; // Current element capacity 44 size_t length_ = 0; // Total number of elements 45 46 package size_t heapAllocations = 0; // The number of times a block is 47 // allocated from the GC heap 48 49 public: 50 51 /** 52 Construct an object without any user-provided blocks. All blocks will be 53 allocated dynamically. 54 55 Params: 56 57 heapBlockCapacity = the minimum capacity desired for each heap 58 block; the actual capacity of each block may be 59 larger 60 */ 61 this(in size_t heapBlockCapacity) 62 { 63 this.heapBlockCapacity = heapBlockCapacity; 64 } 65 66 /** 67 Construct an object that will use the provided buffer 68 69 This constructor will allocate at least one additional heap block for a 70 sliding window use case. Consider the constructor that takes array of 71 arrays to prevent that allocation. 72 73 Params: 74 75 buffer = the buffer to use for the elements 76 */ 77 this(ubyte[] buffer) 78 { 79 // Make a 1-element slice (without allocating memory) and dispatch to an 80 // alternative constructor. 81 this((&buffer)[0..1]); 82 } 83 84 /** 85 Construct an object that will use the provided buffers 86 87 This constructor will be friendly to the sliding window use case. As 88 long as the window width remains less than or equal to the capacity of 89 the shortest buffer, there should be no heap block allocation. 90 91 This constructor picks the longest buffer's capacity to use for 92 potential heap block allocations. 93 94 Params: 95 96 buffers = the _buffers to use for the elements 97 */ 98 this(ubyte[][] buffers) 99 { 100 this.userBlocks.reserve(buffers.length); 101 102 foreach (buffer; buffers) 103 { 104 addInitialBlock(buffer); 105 } 106 } 107 108 /// Ditto 109 this(size_t N, size_t M)(ref ubyte[N][M] buffers) 110 { 111 this.userBlocks.reserve(M); 112 113 foreach (ref buffer; buffers) 114 { 115 addInitialBlock(buffer); 116 } 117 } 118 119 private void addInitialBlock(ubyte[] buffer) 120 { 121 import std.algorithm : max; 122 import std.array : back; 123 124 addExistingBlock_(buffer); 125 userBlocks ~= blocks.back.ptr; 126 127 this.heapBlockCapacity = max(this.heapBlockCapacity, blocks.back.capacity); 128 } 129 130 /** 131 Clears all blocks in reverse order 132 */ 133 ~this() 134 { 135 import std.algorithm : all, canFind, map; 136 137 foreach_reverse (ref block; blocks) 138 { 139 block.clear(); 140 } 141 142 // Check that we did not lose non-heap blocks 143 assert(userBlocks.all!(pb => canFind(blocks.map!(b => b.ptr), pb))); 144 } 145 146 /// String representation of this object useful mostly for debugging 147 void toString(scope void delegate(in char[]) sink) const 148 { 149 import std.algorithm : canFind, map; 150 import std.format : format, formattedWrite; 151 152 sink.formattedWrite!"cap:%s len:%s heapBlkCap:%s tail:%s added:%s\n"( 153 capacity, length, heapBlockCapacity, tailBlock, heapAllocations); 154 155 sink.formattedWrite!"occ: %s/%s\n"( 156 heapBlockOccupancy.occupied, heapBlockOccupancy.total); 157 158 sink.formattedWrite!"blocks (* -> user block):%-(\n %s%)\n"( 159 blocks.map!(b => format!"%s%s"(canFind(userBlocks, b.ptr) ? "* " : " ", b))); 160 } 161 162 /// Total element _capacity 163 size_t capacity() const 164 { 165 return capacity_; 166 } 167 168 /// Number of elements currently available 169 size_t length() const 170 { 171 return length_; 172 } 173 174 /** 175 Append an _element 176 177 Params: 178 179 element = the _element to add; lvalues are copied, rvalues are moved 180 */ 181 void opOpAssign(string op)(auto ref T element) 182 if (op == "~") 183 { 184 ensureFreeSpace_(); 185 blocks[tailBlock] ~= element; 186 ++length_; 187 } 188 189 /** 190 Move an _element to the end 191 192 Params: 193 194 element = the _element to move 195 */ 196 void moveEmplace(ref T element) 197 { 198 ensureFreeSpace_(); 199 blocks[tailBlock].moveEmplace(element); 200 ++length_; 201 } 202 203 /** 204 Construct a new _element at the end 205 206 Params: 207 208 args = the constructor arguments to use 209 */ 210 void emplaceBack(Args...)(auto ref Args args) 211 { 212 ensureFreeSpace_(); 213 blocks[tailBlock].emplaceBack(args); 214 ++length_; 215 } 216 217 /** 218 Return a reference to an element 219 220 Accessing the elements with the InputRange interface returned from 221 opSlice is more efficient. 222 223 Params: 224 225 index = the _index of the element to return 226 */ 227 ref inout(T) opIndex(size_t index) inout 228 in (index < length, circularblocksError("Index is invalid for length", index, length)) 229 { 230 const blockId = computeBlockAndIndex(blocks, &index); 231 return blocks[blockId][index]; 232 } 233 234 /// Number of elements in the block 235 size_t opDollar() const 236 { 237 return length; 238 } 239 240 /** 241 A range providing access _to the specified elements. 242 243 Although this range provides the RandomAccessRange interface, accessing 244 the elements with opIndex is less efficient than accessing the elements 245 with the InputRange interface. 246 247 Params: 248 249 from = the index of the first element of the range 250 to = the index of the element one beyond the last element of the range 251 */ 252 auto opSlice(in size_t from, in size_t to) const 253 in (from <= to, circularblocksError("Range begin is greater than end", from, to)) 254 in (to - from <= length, circularblocksError("Range is too long", from, to, length)) 255 { 256 static struct Range 257 { 258 const(ReusableBlock!T)[] bks; 259 size_t bId; 260 size_t idx; 261 size_t bIdBack; 262 size_t idxBack; // This is 1 more than the actual value 263 size_t len; 264 265 bool empty() const 266 { 267 return len == 0; 268 } 269 270 ref front() const 271 { 272 return bks[bId][idx]; 273 } 274 275 void popFront() 276 { 277 --len; 278 ++idx; 279 if (idx == bks[bId].length) { 280 ++bId; 281 idx = 0; 282 } 283 } 284 285 size_t length() const 286 { 287 return len; 288 } 289 290 auto save() 291 { 292 return this; 293 } 294 295 ref back() const 296 in (idxBack > 0, circularblocksError("Zero idxBack")) 297 { 298 return bks[bIdBack][idxBack - 1]; 299 } 300 301 void popBack() 302 { 303 --len; 304 --idxBack; 305 if (idxBack == 0) { 306 --bIdBack; 307 idxBack = bks[bIdBack].length; // Yes, 1 more 308 } 309 } 310 311 auto opIndex(size_t index) 312 in (index < len, 313 circularblocksError("Index is invalid for length", index, len)) 314 { 315 index += idx; 316 const blockId = computeBlockAndIndex(bks, &index); 317 return bks[blockId][index]; 318 } 319 } 320 321 size_t index = from; 322 const blockId = computeBlockAndIndex(blocks, &index); 323 324 size_t indexBack = to - 1; 325 const blockIdBack = computeBlockAndIndex(blocks, &indexBack); 326 327 return Range(blocks, blockId, index, blockIdBack, indexBack + 1, to - from); 328 } 329 330 /// A range to all elements; the same as `[0..$]` 331 auto opSlice() const 332 { 333 return this[0..$]; 334 } 335 336 /** 337 Remove elements from the head of the block 338 339 Params: 340 341 n = number of elements to remove 342 */ 343 void removeFrontN(size_t n) 344 in (length >= n, circularblocksError("Not enough elements to remove", n, length)) 345 { 346 import std.algorithm : bringToFront; 347 348 length_ -= n; 349 350 // We don't want to use division because the blocks may have different 351 // lengths; for example, the first block may have few elements currently 352 // at an offset. 353 size_t blockDropCount = 0; 354 foreach (block; blocks) 355 { 356 if (n < block.length) 357 { 358 break; 359 } 360 361 n -= block.length; 362 ++blockDropCount; 363 } 364 365 if (blockDropCount < blocks.length) 366 { 367 // This block will be the head block when others will be moved to 368 // back. The elements here are the oldest, so they must be destroyed 369 // first. 370 blocks[blockDropCount].removeFrontN(n); 371 tailBlock -= blockDropCount; 372 } 373 else 374 { 375 // All blocks will be dropped. This can only happen when the drop 376 // count matched length. 377 import alid.test : shouldBe; 378 379 n.shouldBe(0); 380 tailBlock = 0; 381 } 382 383 // Destroy elements of blocks that will go to the back 384 foreach_reverse (b; 0 .. blockDropCount) 385 { 386 blocks[b].clear(); 387 } 388 389 bringToFront(blocks[0..blockDropCount], blocks[blockDropCount..$]); 390 } 391 392 /** 393 Total number of heap blocks and the number of those that are occupied by 394 at least one element 395 396 Returns: a tuple with two `size_t` members: `.total` and `.occupied` 397 */ 398 auto heapBlockOccupancy() const 399 { 400 import std.algorithm : canFind, count; 401 import std.array : empty; 402 import std.typecons : tuple; 403 404 const occupied = blocks.count!(b => !b.empty && !canFind(userBlocks, b.ptr)); 405 const total = blocks.length - userBlocks.length; 406 return tuple!("total", "occupied")(total, occupied); 407 } 408 409 /** 410 Release all unoccupied heap blocks from the blocks array 411 412 Return: 413 414 number of blocks removed 415 */ 416 size_t compact() 417 { 418 import std.array : empty; 419 import std.algorithm : canFind, map, remove, sum, SwapStrategy; 420 421 const before = blocks.length; 422 423 // Because all empty blocks are at the back, removing with .unstable causes the reordering 424 // of only the empty userBlocks. Blocks that actively carry elements should not be 425 // affected. (We have a unittest block for this case.) 426 blocks = blocks.remove!(b => (b.empty && !canFind(userBlocks, b.ptr)), 427 SwapStrategy.unstable); 428 const after = blocks.length; 429 430 capacity_ = blocks.map!(b => b.capacity).sum; 431 432 assert(before >= after); 433 return before - after; 434 } 435 436 private: 437 438 void ensureFreeSpace_() 439 { 440 import std.array : empty; 441 442 if (blocks.empty) 443 { 444 import alid.test : shouldBe; 445 446 addHeapBlock_(); 447 tailBlock.shouldBe(0); 448 } 449 else if (!blocks[tailBlock].freeCapacity) 450 { 451 ++tailBlock; 452 453 if (tailBlock == blocks.length) 454 { 455 // Need a new block 456 addHeapBlock_(); 457 } 458 459 assert(blocks[tailBlock].empty); 460 } 461 } 462 463 void addHeapBlock_() @trusted 464 { 465 import std.algorithm : max; 466 467 // Using a T slice to guarantee correct alignment of Ts 468 T[] tArr; 469 tArr.reserve(heapBlockCapacity); 470 471 // Use all extra capacity (tArr.capacity can be greater than 472 // heapBlockCapacity) 473 const ubyteCapacity = tArr.capacity * T.sizeof; 474 auto arr = (cast(ubyte*)tArr.ptr)[0 .. ubyteCapacity]; 475 476 addExistingBlock_(arr); 477 ++heapAllocations; 478 } 479 480 void addExistingBlock_(ubyte[] buffer) 481 { 482 import std.array : back; 483 484 blocks ~= ReusableBlock!T(buffer); 485 capacity_ += blocks.back.capacity; 486 } 487 } 488 489 /// 490 unittest 491 { 492 // This example starts with user-provided buffers. (It is possible to 493 // provide a single buffer but that case would need to allocate at least one 494 // heap buffer later e.g. in a sliding window use case.) 495 496 enum size = 42; 497 enum count = 2; 498 ubyte[size][count] buffers; 499 500 // Create a circular buffer of ints using those buffers 501 auto c = CircularBlocks!int(buffers); 502 503 // We can't be certain about total capacity because initial parts of the 504 // buffers may not be used due to alignment requirements. At least make sure 505 // the tests will be valid 506 const initialCapacity = c.capacity; 507 assert(initialCapacity > 0, "Invalid unittest"); 508 509 // Populate with some elements 510 iota(initialCapacity).each!(i => c ~= i.to!int); 511 512 // All capacity should be utilized at this point 513 c.length.shouldBe(c.capacity); 514 515 // As all elements are on provided buffers so far, there should be no heap 516 // allocation yet 517 c.heapBlockOccupancy.total.shouldBe(0); 518 c.heapBlockOccupancy.occupied.shouldBe(0); 519 520 // Adding one more element should allocate one heap block 521 c ~= 42; 522 c.heapBlockOccupancy.total.shouldBe(1); 523 c.heapBlockOccupancy.occupied.shouldBe(1); 524 assert(c.capacity > initialCapacity); 525 526 // Remove all elements 527 c.removeFrontN(c.length); 528 c.length.shouldBe(0); 529 c.heapBlockOccupancy.total.shouldBe(1); 530 c.heapBlockOccupancy.occupied.shouldBe(0); 531 532 // Release the unoccupied heap blocks 533 c.compact(); 534 c.heapBlockOccupancy.total.shouldBe(0); 535 c.heapBlockOccupancy.occupied.shouldBe(0); 536 537 // Because we started with user buffers, the capacity should never be less 538 // than initial capacity 539 c.capacity.shouldBe(initialCapacity); 540 } 541 542 /// 543 nothrow 544 unittest 545 { 546 // This example uses a single user-provided buffer. It is inevitable that 547 // there will be at least one heap block allocation if more elements added 548 // than the capacity of the user-provided buffer. 549 550 ubyte[100] buffer; 551 auto c = CircularBlocks!string(buffer[]); 552 553 iota(c.capacity).each!(_ => c ~= "hi"); 554 c.length.shouldBe(c.capacity); 555 556 // There should be no heap block allocation 557 c.heapBlockOccupancy.total.shouldBe(0); 558 c.heapBlockOccupancy.occupied.shouldBe(0); 559 } 560 561 /// 562 nothrow 563 unittest 564 { 565 // This example does not start with any user-provided buffer 566 const heapBlockCapacity = 100; 567 auto c = CircularBlocks!double(heapBlockCapacity); 568 569 // Due to lazy allocation, no heap block should be allocated yet 570 c.capacity.shouldBe(0); 571 572 // Adding elements should cause heap block allocations 573 c ~= 1; 574 assert(c.capacity != 0); 575 c.heapBlockOccupancy.total.shouldBe(1); 576 c.heapBlockOccupancy.occupied.shouldBe(1); 577 } 578 579 /// 580 unittest 581 { 582 // When user-provided buffers are sufficiently large for a sliding window of 583 // elements, no heap block should be allocated 584 585 ubyte[64][2] buffers; 586 auto c = CircularBlocks!int(buffers); 587 assert(c.capacity != 0); 588 589 // Start with some elements filling half the capacity 590 iota(c.capacity / 2).each!(i => c ~= i.to!int); 591 592 // Use the rest of the capacity as the width of a sliding window 593 const windowWidth = c.capacity - c.length; 594 595 // Add and remove equal number of elements for many number of times 596 foreach (i; 0 .. 117) 597 { 598 iota(windowWidth).each!(i => c ~= i.to!int); 599 // Prove that buffer is completely full after the additions 600 c.length.shouldBe(c.capacity); 601 c.removeFrontN(windowWidth); 602 } 603 604 // No heap block should have been allocated 605 c.heapBlockOccupancy.total.shouldBe(0); 606 } 607 608 // Takes the index of an element and returns the id of the block that hosts the 609 // element. This function also modifies the 'index' parameter to indicate the 610 // index inside the returned block. 611 private size_t computeBlockAndIndex(B)(const(B) blocks, size_t * index) 612 { 613 // We don't want to use division because the blocks may have different 614 // lengths; for example, the first block may have few elements currently 615 // at an offset. 616 foreach (blockId, block; blocks) 617 { 618 if (block.length > *index) 619 { 620 return blockId; 621 } 622 623 *index -= block.length; 624 } 625 626 assert(false, "Failed to return early."); 627 } 628 629 version (unittest) 630 { 631 import alid.test : shouldBe; 632 import std.algorithm : each, map; 633 import std.array : array; 634 import std.conv : to; 635 import std.range : iota; 636 } 637 638 unittest 639 { 640 // Test const and immutable types are usable 641 642 import std.meta : AliasSeq; 643 644 void test(T)(in T value) 645 { 646 auto c = CircularBlocks!T(100); 647 c ~= value; 648 c[0].shouldBe(value); 649 c.removeFrontN(1); 650 c.length.shouldBe(0); 651 } 652 653 struct S 654 { 655 string s; 656 } 657 658 alias Ts = AliasSeq!(const(int), 42, 659 immutable(double), 1.5, 660 const(S), S("hello")); 661 662 static foreach (i; 0 .. Ts.length) 663 { 664 static if (i % 2 == 0) 665 { 666 test!(Ts[i])(Ts[i+1]); 667 } 668 } 669 } 670 671 unittest 672 { 673 // Test full slice 674 675 import std.algorithm : each; 676 677 auto c = CircularBlocks!size_t(1024); 678 679 enum begin = size_t(0); 680 enum end = size_t(1000); 681 iota(begin, end).each!(i => c ~= i); 682 683 enum dropped = size_t(10); 684 c.removeFrontN(dropped); 685 686 assert(end > dropped); 687 688 c[0..$].length.shouldBe(end - dropped); 689 c[].shouldBe(iota(dropped, end)); 690 c[0..$].shouldBe(iota(dropped, end)); 691 } 692 693 unittest 694 { 695 // Test partial slice 696 697 import std.range : isRandomAccessRange, retro; 698 699 auto c = CircularBlocks!size_t(7); 700 701 enum begin = size_t(0); 702 enum end = size_t(100); 703 iota(begin, end).each!(i => c ~= i); 704 705 const from = begin + 5; 706 const to = end - 17; 707 708 auto expectedSlice = iota(from, to); 709 710 // 'save' should preserve iteration state 711 712 auto sl = c[from..to]; 713 auto sl2 = sl.save(); 714 sl.shouldBe(expectedSlice.save()); 715 716 static assert (isRandomAccessRange!(typeof(sl))); 717 718 // Consume the original slice 719 while(!sl.empty) 720 { 721 sl.popFront(); 722 } 723 // The saved state should still have the expected elements 724 sl2.shouldBe(expectedSlice.save()); 725 726 // Test the opIndex of the slice interface 727 iota(to - from).map!(i => sl2[i]).shouldBe(expectedSlice.save()); 728 729 sl2.retro.shouldBe(expectedSlice.save().retro); 730 } 731 732 unittest 733 { 734 // The string representation 735 736 assert(CircularBlocks!string(new ubyte[10]).to!string.length); 737 } 738 739 unittest 740 { 741 // moveEmplace tests 742 743 static struct S 744 { 745 string msg = "initial value"; 746 747 // Needed for destroy() to set to the .init value 748 ~this() {} 749 } 750 751 auto s = S("hello"); 752 s.msg.shouldBe("hello"); 753 754 auto c = CircularBlocks!S(10); 755 c.moveEmplace(s); 756 c.length.shouldBe(1); 757 c[].front.msg.shouldBe("hello"); 758 s.msg.shouldBe("initial value"); 759 } 760 761 unittest 762 { 763 // compact() should not reorder elements 764 765 import std.typecons : Flag, No, Yes; 766 767 enum n = 1024; 768 enum bufferSize = n / 10; 769 enum toRemove = 9 * bufferSize; 770 static assert (toRemove < n); 771 772 void test(Flag!"withUserBuffers" withUserBuffers)() 773 { 774 static if (withUserBuffers) 775 { 776 ubyte[bufferSize][2] buffers; 777 auto c = CircularBlocks!int(buffers); 778 } 779 else 780 { 781 auto c = CircularBlocks!int(bufferSize); 782 } 783 784 // Cause some empty blocks 785 iota(n).each!(i => c ~= i); 786 c.removeFrontN(toRemove); 787 788 // Ensure that we have caused multiple empty blocks 789 assert(c.heapBlockOccupancy.total - c.heapBlockOccupancy.occupied > 1); 790 791 c.compact(); 792 793 auto expected = iota(toRemove, n); 794 c[].shouldBe(expected); 795 } 796 797 test!(Yes.withUserBuffers)(); 798 test!(No.withUserBuffers)(); 799 }