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 }