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 }