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: 30 31 import alid.blockreusable : ReusableBlock; 32 33 ReusableBlock!T[] blocks; // Where elements are stored 34 const(T*)[] userBlocks; // The .ptr values of the user-provided blocks, 35 // if any 36 37 size_t heapBlockCapacity; // The desired capacity for heap blocks 38 39 size_t tailBlock = 0; // The block new elements are being added to 40 size_t capacity_ = 0; // Current element capacity 41 size_t length_ = 0; // Total number of elements 42 43 package size_t heapAllocations = 0; // The number of times a block is 44 // allocated from the GC heap 45 46 @disable this(); 47 @disable this(this); 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) nothrow pure @safe scope 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) nothrow pure scope 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) nothrow pure @safe scope 99 { 100 this.userBlocks.reserve(userBlocks.length); 101 102 foreach (ref buffer; buffers) 103 { 104 import std.algorithm : max; 105 import std.array : back; 106 107 addExistingBlock_(buffer[]); 108 userBlocks ~= blocks.back.ptr; 109 110 this.heapBlockCapacity = max(this.heapBlockCapacity, blocks.back.capacity); 111 } 112 } 113 114 /// Ditto 115 this(size_t N, size_t M)(ref ubyte[N][M] buffers) { 116 // Dispatch to an alternative constructor with a slice of slices 117 this(buffers[].map!((ref b) => b[]).array); 118 } 119 120 /** 121 Clears all blocks in reverse order 122 */ 123 ~this() scope 124 { 125 import std.algorithm : all, canFind, map; 126 127 foreach_reverse (ref block; blocks) 128 { 129 block.clear(); 130 } 131 132 // Check that we did not lose non-heap blocks 133 assert(userBlocks.all!(pb => canFind(blocks.map!(b => b.ptr), pb))); 134 } 135 136 /// String representation of this object useful mostly for debugging 137 void toString(scope void delegate(in char[]) sink) const scope 138 { 139 import std.algorithm : canFind, map; 140 import std.format : format, formattedWrite; 141 142 sink.formattedWrite!"cap:%s len:%s heapBlkCap:%s tail:%s added:%s\n"( 143 capacity, length, heapBlockCapacity, tailBlock, heapAllocations); 144 145 sink.formattedWrite!"occ: %s/%s\n"( 146 heapBlockOccupancy.occupied, heapBlockOccupancy.total); 147 148 sink.formattedWrite!"blocks (* -> user block):%-(\n %s%)\n"( 149 blocks.map!(b => format!"%s%s"(canFind(userBlocks, b.ptr) ? "* " : " ", b))); 150 } 151 152 /// Total element _capacity 153 size_t capacity() const @nogc nothrow pure @safe scope 154 { 155 return capacity_; 156 } 157 158 /// Number of elements currently available 159 size_t length() const @nogc nothrow pure @safe scope 160 { 161 return length_; 162 } 163 164 /** 165 Append an _element 166 167 Params: 168 169 element = the _element to add; lvalues are copied, rvalues are moved 170 */ 171 void opOpAssign(string op)(auto ref T element) 172 if (op == "~") 173 { 174 ensureFreeSpace_(); 175 blocks[tailBlock] ~= element; 176 ++length_; 177 } 178 179 /** 180 Move an _element to the end 181 182 Params: 183 184 element = the _element to move 185 */ 186 auto moveEmplace(ref T element) nothrow pure scope 187 { 188 ensureFreeSpace_(); 189 blocks[tailBlock].moveEmplace(element); 190 ++length_; 191 } 192 193 /** 194 Construct a new _element at the end 195 196 Params: 197 198 args = the constructor arguments to use 199 */ 200 auto emplaceBack(Args...)(auto ref Args args) 201 { 202 ensureFreeSpace_(); 203 blocks[tailBlock].emplaceBack(args); 204 ++length_; 205 } 206 207 /** 208 Return a reference to an element 209 210 Params: 211 212 index = the _index of the element to return 213 */ 214 ref inout(T) opIndex(size_t index) inout @nogc nothrow pure scope 215 in (index < length, 216 circularblocksError("Index is invalid for length", index, length)) 217 { 218 // We don't want to use division because the blocks may have different 219 // lengths; for example, the first block may have few elements currently 220 // at an offset. 221 foreach (block; blocks) 222 { 223 if (block.length > index) 224 { 225 return block[index]; 226 } 227 228 index -= block.length; 229 } 230 231 assert(false, "If the pre-condition held, we should have returned early."); 232 } 233 234 /// Number of elements in the block 235 size_t opDollar() const @nogc nothrow pure @safe scope 236 { 237 return length; 238 } 239 240 /** 241 A range providing access _to the specified elements 242 243 Params: 244 245 from = the index of the first element of the range 246 to = the index of the element one beyond the last element of the range 247 */ 248 auto opSlice(in size_t from, in size_t to) const nothrow pure @safe scope 249 in (from <= to, circularblocksError("Range begin is greater than end", from, to)) 250 in (to - from <= length, circularblocksError("Range is too long", from, to, length)) 251 { 252 import std.algorithm : map; 253 import std.range : iota; 254 255 return iota(from, to).map!(i => this[i]); 256 } 257 258 /// A range to all elements; the same as `[0..$]` 259 auto opSlice() const nothrow pure @safe scope 260 { 261 return this[0..$]; 262 } 263 264 /** 265 Remove elements from the head of the block 266 267 Params: 268 269 n = number of elements to remove 270 */ 271 void removeFrontN(size_t n) scope 272 in (length >= n, circularblocksError("Not enough elements to remove", n, length)) 273 { 274 import std.algorithm : bringToFront; 275 276 length_ -= n; 277 278 // We don't want to use division because the blocks may have different 279 // lengths; for example, the first block may have few elements currently 280 // at an offset. 281 size_t blockDropCount = 0; 282 foreach (block; blocks) 283 { 284 if (n < block.length) 285 { 286 break; 287 } 288 289 n -= block.length; 290 ++blockDropCount; 291 } 292 293 if (blockDropCount < blocks.length) 294 { 295 // This block will be the head block when others will be moved to 296 // back. The elements here are the oldest, so they must be destroyed 297 // first. 298 blocks[blockDropCount].removeFrontN(n); 299 tailBlock -= blockDropCount; 300 } 301 else 302 { 303 // All blocks will be dropped. This can only happen when the drop 304 // count matched length. 305 import alid.test : shouldBe; 306 307 n.shouldBe(0); 308 tailBlock = 0; 309 } 310 311 // Destroy elements of blocks that will go to the back 312 foreach_reverse (b; 0 .. blockDropCount) 313 { 314 blocks[b].clear(); 315 } 316 317 bringToFront(blocks[0..blockDropCount], blocks[blockDropCount..$]); 318 } 319 320 /** 321 Total number of heap blocks and the number of those that are occupied by 322 at least one element 323 324 Returns: a tuple with two `size_t` members: `.total` and `.occupied` 325 */ 326 auto heapBlockOccupancy() const @nogc nothrow pure @safe scope 327 { 328 import std.algorithm : canFind, count; 329 import std.array : empty; 330 import std.typecons : tuple; 331 332 const occupied = blocks.count!(b => !b.empty && !canFind(userBlocks, b.ptr)); 333 const total = blocks.length - userBlocks.length; 334 return tuple!("total", "occupied")(total, occupied); 335 } 336 337 /** 338 Release all unoccupied heap blocks from the blocks array 339 340 Return: 341 342 number of blocks removed 343 */ 344 size_t compact() @nogc nothrow pure @safe scope 345 { 346 import std.array : empty; 347 import std.algorithm : canFind, map, remove, sum, SwapStrategy; 348 349 const before = blocks.length; 350 blocks = blocks.remove!(b => (b.empty && !canFind(userBlocks, b.ptr)), 351 SwapStrategy.unstable); 352 const after = blocks.length; 353 354 capacity_ = blocks.map!(b => b.capacity).sum; 355 356 assert(before >= after); 357 return before - after; 358 } 359 360 private: 361 362 void ensureFreeSpace_() nothrow pure @safe scope 363 { 364 import std.array : empty; 365 366 if (blocks.empty) 367 { 368 import alid.test : shouldBe; 369 370 addHeapBlock_(); 371 tailBlock.shouldBe(0); 372 } 373 else if (!blocks[tailBlock].freeCapacity) 374 { 375 ++tailBlock; 376 377 if (tailBlock == blocks.length) 378 { 379 // Need a new block 380 addHeapBlock_(); 381 } 382 383 assert(blocks[tailBlock].empty); 384 } 385 } 386 387 void addHeapBlock_() nothrow pure @trusted scope 388 { 389 import std.algorithm : max; 390 391 // Using a T slice to guarantee correct alignment of Ts 392 T[] tArr; 393 tArr.reserve(heapBlockCapacity); 394 395 // Use all extra capacity (tArr.capacity can be greater than 396 // heapBlockCapacity) 397 const ubyteCapacity = tArr.capacity * T.sizeof; 398 auto arr = (cast(ubyte*)tArr.ptr)[0 .. ubyteCapacity]; 399 400 addExistingBlock_(arr); 401 ++heapAllocations; 402 } 403 404 void addExistingBlock_(ubyte[] buffer) nothrow pure @safe scope 405 { 406 import std.array : back; 407 408 blocks ~= ReusableBlock!T(buffer); 409 capacity_ += blocks.back.capacity; 410 } 411 } 412 413 /// 414 unittest 415 { 416 // This example starts with user-provided buffers. (It is possible to 417 // provide a single buffer but that case would need to allocate at least one 418 // heap buffer later e.g. in a sliding window use case.) 419 420 enum size = 42; 421 enum count = 2; 422 ubyte[size][count] buffers; 423 424 // Create a circular buffer of ints using those buffers 425 auto c = CircularBlocks!int(buffers); 426 427 // We can't be certain about total capacity because initial parts of the 428 // buffers may not be used due to alignment requirements. At least make sure 429 // the tests will be valid 430 const initialCapacity = c.capacity; 431 assert(initialCapacity > 0, "Invalid unittest"); 432 433 // Populate with some elements 434 iota(initialCapacity).each!(i => c ~= i.to!int); 435 436 // All capacity should be utilized at this point 437 c.length.shouldBe(c.capacity); 438 439 // As all elements are on provided buffers so far, there should be no heap 440 // allocation yet 441 c.heapBlockOccupancy.total.shouldBe(0); 442 c.heapBlockOccupancy.occupied.shouldBe(0); 443 444 // Adding one more element should allocate one heap block 445 c ~= 42; 446 c.heapBlockOccupancy.total.shouldBe(1); 447 c.heapBlockOccupancy.occupied.shouldBe(1); 448 assert(c.capacity > initialCapacity); 449 450 // Remove all elements 451 c.removeFrontN(c.length); 452 c.length.shouldBe(0); 453 c.heapBlockOccupancy.total.shouldBe(1); 454 c.heapBlockOccupancy.occupied.shouldBe(0); 455 456 // Release the unoccupied heap blocks 457 c.compact(); 458 c.heapBlockOccupancy.total.shouldBe(0); 459 c.heapBlockOccupancy.occupied.shouldBe(0); 460 461 // Because we started with user buffers, the capacity should never be less 462 // than initial capacity 463 c.capacity.shouldBe(initialCapacity); 464 } 465 466 /// 467 unittest 468 { 469 // This example uses a single user-provided buffer. It is inevitable that 470 // there will be at least one heap block allocation if more elements added 471 // than the capacity of the user-provided buffer. 472 473 ubyte[100] buffer; 474 auto c = CircularBlocks!string(buffer[]); 475 476 iota(c.capacity).each!(_ => c ~= "hi"); 477 c.length.shouldBe(c.capacity); 478 479 // There should be no heap block allocation 480 c.heapBlockOccupancy.total.shouldBe(0); 481 c.heapBlockOccupancy.occupied.shouldBe(0); 482 } 483 484 /// 485 unittest 486 { 487 // This example does not start with any user-provided buffer 488 const heapBlockCapacity = 100; 489 auto c = CircularBlocks!double(heapBlockCapacity); 490 491 // Due to lazy allocation, no heap block should be allocated yet 492 c.capacity.shouldBe(0); 493 494 // Adding elements should cause heap block allocations 495 c ~= 1; 496 assert(c.capacity != 0); 497 c.heapBlockOccupancy.total.shouldBe(1); 498 c.heapBlockOccupancy.occupied.shouldBe(1); 499 } 500 501 /// 502 unittest 503 { 504 // When user-provided buffers are sufficiently large for a sliding window of 505 // elements, no heap block should be allocated 506 507 ubyte[64][2] buffers; 508 auto c = CircularBlocks!int(buffers); 509 assert(c.capacity != 0); 510 511 // Start with some elements filling half the capacity 512 iota(c.capacity / 2).each!(i => c ~= i.to!int); 513 514 // Use the rest of the capacity as the width of a sliding window 515 const windowWidth = c.capacity - c.length; 516 517 // Add and remove equal number of elements for many number of times 518 foreach (i; 0 .. 117) 519 { 520 iota(windowWidth).each!(i => c ~= i.to!int); 521 // Prove that buffer is completely full after the additions 522 c.length.shouldBe(c.capacity); 523 c.removeFrontN(windowWidth); 524 } 525 526 // No heap block should have been allocated 527 c.heapBlockOccupancy.total.shouldBe(0); 528 } 529 530 531 version (unittest) 532 { 533 import alid.test : shouldBe; 534 import std.algorithm : each, map; 535 import std.array : array; 536 import std.conv : to; 537 import std.range : iota; 538 } 539 540 unittest 541 { 542 // Test const and immutable types are usable 543 544 import std.meta : AliasSeq; 545 546 void test(T)(in T value) 547 { 548 auto c = CircularBlocks!T(100); 549 c ~= value; 550 c[0].shouldBe(value); 551 c.removeFrontN(1); 552 c.length.shouldBe(0); 553 } 554 555 struct S 556 { 557 string s; 558 } 559 560 alias Ts = AliasSeq!(const(int), 42, 561 immutable(double), 1.5, 562 const(S), S("hello")); 563 564 static foreach (i; 0 .. Ts.length) 565 { 566 static if (i % 2 == 0) 567 { 568 test!(Ts[i])(Ts[i+1]); 569 } 570 } 571 } 572 573 unittest 574 { 575 // Test the range interface 576 577 import std.algorithm : each, equal; 578 579 auto c = CircularBlocks!size_t(1024); 580 581 enum begin = size_t(0); 582 enum end = size_t(1000); 583 iota(begin, end).each!(i => c ~= i); 584 585 enum dropped = size_t(10); 586 c.removeFrontN(dropped); 587 588 assert(end > dropped); 589 590 c[0..$].length.shouldBe(end - dropped); 591 assert(c[].equal(iota(dropped, end))); 592 assert(c[0..$].equal(iota(dropped, end))); 593 } 594 595 unittest 596 { 597 // The string representation 598 599 auto c = CircularBlocks!string(new ubyte[10]).to!string; 600 } 601 602 unittest 603 { 604 // moveEmplace tests 605 606 static struct S 607 { 608 string msg = "initial value"; 609 610 // Needed for destroy() to set to the .init value 611 ~this() {} 612 } 613 614 auto s = S("hello"); 615 assert(s.msg); 616 617 auto c = CircularBlocks!S(10); 618 c.moveEmplace(s); 619 c.length.shouldBe(1); 620 c[].front.msg.shouldBe("hello"); 621 s.msg.shouldBe("initial value"); 622 }