1 /**
2    Implements a range algorithm that caches element values turns InputRangess to
3    RandomAccessRanges.
4  */
5 
6 module alid.cached;
7 
8 import alid.errornogc : NogcError;
9 import alid.circularblocks : CircularBlocks;
10 import core.memory : pageSize;
11 import std.range : ElementType;
12 
13 /**
14     A _range algorithm that caches the elements of a _range by evaluating them
15     only once.
16 
17     As a natural benefit of caching, the returned _range is a `ForwardRange`
18     even if the source _range is an `InputRange`. Although the returned _range
19     provides `opIndex`, it is a `RandomAccessRange` only if the source _range
20     `hasLength`.
21 
22     The version that takes buffers may never need to allocate memory if the
23     number of elements never exceed the capacity of the underlying
24     `CircularBlocks`. (See `CircularBlocks`.)
25 
26     Params:
27 
28         range = the source _range to cache elements of
29 
30         heapBlockCapacity = the minimum capacity to use for `CircularBlocks`,
31                             which is used as storage for the _cached elements;
32                             the default value attemps to use one page of memory
33 
34         buffers = the _buffers to be used by the `CircularBlocks` member
35 
36     Bugs:
37 
38         Although `opIndex` is provided, `isRandomAccessRange` produces `false`
39         because this implementation is not a `BidirectionalRange` at this time.
40 
41     Todo:
42 
43         Add `opApplyReverse` support and `BidirectionalRange` functions
44 
45 */
46 auto cached(R)(R range, size_t heapBlockCapacity = pageSize / ElementType!R.sizeof)
47 {
48     // Makes a new ElementCache object that will be kept alive collectively with
49     // the slices that it produces, first of which is returned.
50 
51     if (heapBlockCapacity == 0)
52     {
53         heapBlockCapacity = 100;
54     }
55 
56     auto elements = CircularBlocks!(ElementType!R)(heapBlockCapacity);
57     auto theCacheObject = new ElementCache!R(range, elements, heapBlockCapacity);
58 
59     // This first slice starts with offset 0
60     enum elementOffset = 0;
61     return theCacheObject.makeSlice(elementOffset);
62 }
63 
64 /// Ditto
65 auto cached(R, size_t N, size_t M)(R range, ref ubyte[N][M] buffers)
66 {
67     // Makes a new ElementCache object that will be kept alive collectively with
68     // the slices that it produces, first of which is returned.
69 
70     auto elements = CircularBlocks!(ElementType!R)(buffers);
71     enum heapBlockCapacity = N;
72     auto theCacheObject = new ElementCache!R(range, elements, heapBlockCapacity);
73 
74     enum elementOffset = 0;
75     return theCacheObject.makeSlice(elementOffset);
76 }
77 
78 ///
79 unittest
80 {
81     // In the following typical usage, even though `slide(2)` would visit most
82     // elements more than once, `cached` guarantees that the lambda function
83     // executed by `map` will be executed only once per range element.
84 
85     auto r = iota(4)
86              .map!(i => i * i)    // [0, 1, 4, 9]
87              .cached
88              .slide(2)
89              .array;
90 
91     // There are 3 sliding windows of width 2 over 4 elements
92     r.length.shouldBe(3);
93     assert(r[0].equal([0, 1]));
94     assert(r[1].equal([1, 4]));
95     assert(r[2].equal([4, 9]));
96 }
97 
98 ///
99 unittest
100 {
101     // Random access over an InputRange
102 
103     import std.algorithm : splitter;
104     import std.range : popFrontN;
105 
106     auto lines = "monday,tuesday,wednesday,thursday";
107 
108     auto s = lines.splitter(',');
109     auto c = s.cached;
110 
111     // The source range does not provide random access:
112     static assert(!__traits(compiles, s[0]));
113 
114     // The cached range does:
115     assert(c[2] == "wednesday");
116     assert(c[1] == "tuesday");
117 
118     c.popFrontN(3);
119 
120     // Now index 0 is another element
121     assert(c[0] == "thursday");
122 }
123 
124 ///
125 unittest
126 {
127     // This version uses an existing buffer. It may never need to allocate
128     // additional heap memory.
129 
130     ubyte[1024][2] buffers;
131     auto r = iota(4)
132              .map!(i => i * i)
133              .cached(buffers)    // Will use the provided buffers
134              .slide(2)
135              .array;
136 
137     r.length.shouldBe(3);
138 }
139 
140 private mixin NogcError!"cached";
141 
142 /*
143   This is the implementation of the actual cache, elements of which will be
144   shared by potentially multiple CachedRange ranges.
145 */
146 private struct ElementCache(R)
147 {
148     import std.array : empty;
149     import std.range : hasLength, ElementType;
150     import std.traits : isUnsigned;
151 
152     // Useful aliases and values
153     alias ET = ElementType!R;
154     alias invalidSliceOffset = CachedRange!ElementCache.invalidSliceOffset;
155     enum rangeHasLength = hasLength!R;
156 
157     R range;                       // The source range
158     CircularBlocks!ET elements;    // The cached elements
159     size_t[] sliceOffsets;         // The beginning indexes of slices into 'elements'
160 
161     size_t liveSliceCount;         // The number of slices that are still being used
162     size_t dropLeadingAttempts;    // The number of times we considered but did
163                                    // not go with dropping leading elements
164 
165     size_t minElementsToDrop;      // The number of elements we consider large
166                                    // enough to consider for dropping
167 
168     Stats stats_;
169 
170     @disable this(this);
171     @disable this(ref const(typeof(this)));
172 
173     this(R range, ref CircularBlocks!ET elements, size_t heapBlockCapacity)
174     {
175         import std.algorithm : move;
176 
177         this.range = range;
178         this.elements = move(elements);
179 
180         // Make it the same as heap capacity because it is likely that the
181         // underlying circular buffer will keep the elements alive below this
182         // figure anyway.
183         this.minElementsToDrop = heapBlockCapacity;
184     }
185 
186     Stats stats() const @nogc nothrow pure @safe scope
187     {
188         Stats result = stats_;
189         // Unlike other statistics figures, this value is not kept up-do-date by
190         // us
191         result.heapAllocations = elements.heapAllocations;
192         return result;
193     }
194 
195     // Whether the parameter is valid as a slice id
196     bool isValidId_(in size_t id) const @nogc nothrow pure @safe scope
197     {
198         return id < sliceOffsets.length;
199     }
200 
201     // mixin string for throwing an Error
202     enum idError_ = `cachedError("Invalid id", id)`;
203 
204     // Whether the specified slice is empty
205     bool emptyOf(in size_t id) pure scope
206     in (isValidId_(id), mixin (idError_))
207     {
208         if (!range.empty) {
209             expandAsNeeded(id, 1);
210         }
211 
212         return (sliceOffsets[id] == elements.length) && range.empty;
213     }
214 
215     // The front element of the specified slice
216     auto frontOf(in size_t id) scope
217     in (isValidId_(id), mixin (idError_))
218     {
219         expandAsNeeded(id, 1);
220         return elements[sliceOffsets[id]];
221     }
222 
223     // Pop an element from the specified slice
224     void popFrontOf(in size_t id) @nogc nothrow pure @safe scope
225     in (isValidId_(id), mixin (idError_))
226     {
227         // Trivially increment the offset.
228         ++sliceOffsets[id];
229 
230         if (sliceOffsets[id] >= minElementsToDrop) {
231             /*
232               This slice has popped enough elements for us to consider dropping
233               leading elements. But we don't want to rush to it yet because even
234               determining whether there are unused elements or not incur some
235               cost.
236 
237               We will apply the heuristic rule of ensuring this condition has
238               been seen at least as many times as there are live slices. (Even a
239               single slice is sufficient to hold on to the leading elements.)
240             */
241             ++dropLeadingAttempts;
242 
243             if (dropLeadingAttempts >= liveSliceCount) {
244                 // We waited long enough
245                 dropLeadingAttempts = 0;
246                 const minIndex = unreferencedLeadingElements();
247 
248                 if (minIndex) {
249                     // There are leading elements that are not being used
250                     // anymore
251                     dropLeadingElements(minIndex);
252 
253                     // TODO: Commenting out for now because we can't be sure of
254                     //       usage patterns to decide to remove blocks from the
255                     //       underlying storage. (See a related 'version' in one
256                     //       of the unittest blocks.)
257                     version (RUN_COMPACTION)
258                     {
259                     // Since we've dropped elements, let's also consider
260                     // dropping unused blocks from the underlying circular
261                     // buffer.
262                     const occupancy = elements.heapBlockOccupancy;
263 
264                     if (occupancy.occupied &&
265                         (occupancy.total > occupancy.occupied * 4)) {
266                         ++stats_.compactionRuns;
267                         const removedBlocks = elements.compact();
268                         stats_.removedBlocks += removedBlocks;
269                     }
270                     } // version (none)
271                 }
272             }
273         }
274     }
275 
276     // The specified element (index) of the specified slice (id).
277     auto getElementOf(in size_t id, in size_t index) scope
278     in (isValidId_(id), mixin (idError_))
279     {
280         expandAsNeeded(id, index + 1);
281         return elements[sliceOffsets[id] + index];
282     }
283 
284     // Make and return a new range object that has the same beginning index as
285     // the specified slice
286     auto saveOf(in size_t id) nothrow pure scope
287     in (isValidId_(id), mixin (idError_))
288     {
289         return makeSlice(sliceOffsets[id]);
290     }
291 
292     static if (rangeHasLength)
293     {
294         // The length of the specified slice
295         auto lengthOf(in size_t id) @nogc nothrow pure @safe scope
296         in (isValidId_(id), mixin (idError_))
297         {
298             return range.length + elements.length - sliceOffsets[id];
299         }
300     }
301 
302     // Create and return a RefCounted!CachedRange that initially references all
303     // currently cached elements beginning from the specified offset (element
304     // index)
305     auto makeSlice(in size_t offset)
306     {
307         import std.algorithm : find, move;
308         import std.typecons : refCounted;
309 
310         // Find an unused spot in the slice offsets array.
311         auto found = sliceOffsets.find(invalidSliceOffset);
312         const id = sliceOffsets.length - found.length;
313 
314         if (id == sliceOffsets.length)
315         {
316             // There is no unused spot; add one.
317             sliceOffsets ~= offset;
318         }
319         else
320         {
321             sliceOffsets[id] = offset;
322         }
323 
324         ++liveSliceCount;
325 
326         // Return a reference counted object so that its destructor will
327         // "unregister" it by removing it from the list of slices.
328         auto slice = CachedRange!ElementCache(&this, id);
329         return refCounted(move(slice));
330     }
331 
332     // Remove the specified slice; it does not own any element anymore
333     void removeSlice(in size_t id)
334     in (isValidId_(id), mixin (idError_))
335     {
336         sliceOffsets[id] = invalidSliceOffset;
337         --liveSliceCount;
338     }
339 
340     // Determine the number of leading elements that are not being referenced
341     // (used) by any slice
342     size_t unreferencedLeadingElements() @nogc nothrow pure @safe scope
343     {
344         auto minOffset = size_t.max;
345 
346         foreach (offset; sliceOffsets)
347         {
348             // There is no reason to continue because 0 is the absolute minimum
349             // for unsigned types.
350             if (offset == minOffset.min)
351             {
352                 return offset;
353             }
354 
355             if (offset < minOffset)
356             {
357                 minOffset = offset;
358             }
359         }
360 
361         return minOffset;
362     }
363 
364     // Drop specified number of leading elements from the cache
365     void dropLeadingElements(size_t n) @nogc nothrow pure @safe scope
366     {
367         import std.algorithm : each, filter;
368 
369         // Drop the unreferenced elements
370         elements.removeFrontN(n);
371 
372         // Adjust the starting indexes of all slices accordingly.
373         sliceOffsets
374             .filter!((ref i) => i != invalidSliceOffset)
375             .each!((ref i) => i -= n);
376 
377         ++stats_.leadingDropRuns;
378         stats_.droppedElements += n;
379     }
380 
381     // Ensure that the specified slice has elements as needed
382     void expandAsNeeded(in size_t id, in size_t needed) scope
383     in (isValidId_(id), mixin (idError_))
384     in (sliceOffsets[id] <= elements.length,
385         cachedError("Invalid element index", id, sliceOffsets[id], elements.length))
386     {
387         // expandCache may change both elements and sliceOffsets.
388         while ((elements.length - sliceOffsets[id]) < needed)
389         {
390             expandCache();
391         }
392     }
393 
394     // Expand the element cache by adding one more element from the source range
395     void expandCache() scope
396     in (!range.empty)
397     {
398         import std.range : front, popFront;
399 
400         // Transfer one element from the source range to the cache
401         elements.emplaceBack(range.front);
402         range.popFront();
403     }
404 }
405 
406 /**
407    Statistics about the operation of `ElementCache` as well as the underlying
408    `CircularBlocks` that it uses for element storage.
409 */
410 static struct Stats
411 {
412     /// Number of blocks allocated from the heap by the underlying
413     /// `CircularBlocks` storage
414     size_t heapAllocations;
415 
416     /// Number of times the algorithm for dropping leading elements is executed
417     size_t leadingDropRuns;
418 
419     /// Number of leading elements dropped
420     size_t droppedElements;
421 
422     /// Number of times `CircularBlocks.compact` is executed
423     size_t compactionRuns;
424 
425     /// Number of blocks removed due to compaction
426     size_t removedBlocks;
427 
428     void toString(scope void delegate(in char[]) sink) const scope
429     {
430         import std.format : formattedWrite;
431 
432         sink.formattedWrite!"heap blocks allocated           : %s\n"(heapAllocations);
433         sink.formattedWrite!"leading-element-drop executions : %s\n"(leadingDropRuns);
434         sink.formattedWrite!"total elements dropped          : %s\n"(droppedElements);
435         sink.formattedWrite!"block compactions executions    : %s\n"(compactionRuns);
436         sink.formattedWrite!"blocks removed due to compaction: %s\n"(removedBlocks);
437     }
438 }
439 
440 version (unittest)
441 {
442     import alid.test : shouldBe;
443     import std.algorithm : equal, filter, map;
444     import std.array : array;
445     import std.range : iota, slide;
446 }
447 
448 /**
449     Represents a `ForwardRange` (and a `RandomAccessRange` when possible) over
450     the cached elements of a source range. Provides the range algorithm
451     `length()` only if the source range does so.
452 
453     Params:
454 
455         EC = the template instance of the `ElementCache` template that this
456              range provides access to the elements of. The users are expected to
457              call one of the `cached` functions, which sets this parameter
458              automatically.
459 */
460 //  All pre-condition checks are handled by dispatched ElementCache member functions.
461 struct CachedRange(EC)
462 {
463 private:
464 
465     EC * elementCache;
466     size_t id = size_t.max;
467 
468     enum invalidSliceOffset = size_t.max;
469 
470     // We can't allow copying objects of this type because they are intended to
471     // be reference counted. The users must call save() instead.
472     @disable this(this);
473 
474     // Takes the ElementCache object that this is a slice of, and the assigned
475     // id of this slice
476     this(EC * elementCache, in size_t id)
477     {
478         this.elementCache = elementCache;
479         this.id = id;
480     }
481 
482 public:
483 
484     /**
485        Unregisters this slices from the actual `ElementCache` storage.
486      */
487     ~this() @nogc nothrow pure @safe scope
488     {
489         // Prevent running on an .init state, which move() leaves behind
490         if (elementCache !is null)
491         {
492             elementCache.removeSlice(id);
493         }
494     }
495 
496     /**
497        Return statistics about the operation of `ElementCache` as well as the
498        underlying `CircularBlocks` that it uses for element storage
499     */
500     Stats stats() const @nogc nothrow pure @safe scope
501     {
502         return elementCache.stats;
503     }
504 
505     /**
506         Support for `foreach` loops
507      */
508     // This is needed to support foreach iteration because although
509     // RefCounted!CachedRange implicitly converts to CachedRange, which would be
510     // a range, the result gets copied to the foreach loop. Unfortunately, this
511     // type does not allow copying so we have to support foreach iteration
512     // explicitly here.
513     int opApply(int delegate(ref EC.ET) func) scope
514     {
515         while(!empty)
516         {
517             auto f = front;
518             int result = func(f);
519             if (result)
520             {
521                 return result;
522             }
523             popFront();
524         }
525 
526         return 0;
527     }
528 
529     /// Ditto
530     int opApply(int delegate(ref size_t, ref EC.ET) func) scope
531     {
532         size_t counter;
533         while(!empty)
534         {
535             auto f = front;
536             int result = func(counter, f);
537             if (result)
538             {
539                 return result;
540             }
541             popFront();
542             ++counter;
543         }
544 
545         return 0;
546     }
547 
548     // InputRange functions
549 
550     /// Whether the range is empty
551     auto empty() pure scope
552     {
553         return elementCache.emptyOf(id);
554     }
555 
556     /// The front element of the range
557     auto front() scope
558     {
559         return elementCache.frontOf(id);
560     }
561 
562     /// Remove the front element from the range
563     void popFront() @nogc nothrow pure @safe scope
564     {
565         elementCache.popFrontOf(id);
566     }
567 
568     // ForwardRange function
569 
570     /// Make and return a new `ForwardRange` object that is the equivalent of
571     /// this range object
572     auto save() nothrow pure scope
573     {
574         return elementCache.saveOf(id);
575     }
576 
577     // RandomAccessRange function
578 
579     /**
580         Return a reference to an element
581 
582         The algorithmic complexity of this function is amortized *O(1)* because
583         it might need to cache elements between the currently available last
584         element and the element about to be accessed with the specified index.
585 
586         Params:
587 
588             index = the _index of the element to return
589     */
590     auto opIndex(in size_t index) scope
591     {
592         return elementCache.getElementOf(id, index);
593     }
594 
595     // An optional length
596 
597     static if (EC.rangeHasLength)
598     {
599         /**
600             Number of elements of this range
601 
602             This function is available only if the source range provides it.
603         */
604         auto length() @nogc nothrow pure @safe scope
605         {
606             return elementCache.lengthOf(id);
607         }
608     }
609 }
610 
611 unittest
612 {
613     // Should compile and work with D arrays
614 
615     assert([1, 2].cached.equal([1, 2]));
616 }
617 
618 unittest
619 {
620     // 0 as heapBlockCapacity should work
621 
622     auto r = iota(10).cached(0);
623     r[5].shouldBe(5);
624     assert(r.equal(iota(10)));
625 }
626 
627 unittest
628 {
629     // InputRange, ForwardRange, save, and .length() tests
630 
631     import std.range : take;
632 
633     auto r = iota(1, 11).cached;
634     assert(!r.empty);
635     r.front.shouldBe(1);
636     r.length.shouldBe(10);
637 
638     r.popFront();
639     assert(!r.empty);
640     r.front.shouldBe(2);
641     r.length.shouldBe(9);
642 
643     assert(r.take(4).equal(iota(2, 6)));
644     assert(!r.empty);
645     r.front.shouldBe(6);
646     r.length.shouldBe(5);
647 
648     auto r2 = r.save();
649     assert(r2.equal(iota(6, 11)));
650     assert(r2.empty);
651     r2.length.shouldBe(0);
652 
653     assert(r.equal(iota(6, 11)));
654     assert(r.empty);
655     r.length.shouldBe(0);
656 }
657 
658 unittest
659 {
660     // Iteration tests
661 
662     import std.traits : isIterable;
663 
664     enum begin = 1;
665     auto r = iota(begin, 11).cached;
666     static assert (isIterable!(typeof(r)));
667 
668     auto r2 = r.save();
669 
670     foreach (e; r)
671     {
672         if (e == 3)
673         {
674             break;
675         }
676     }
677     r.front.shouldBe(3);
678 
679     // Fully consume for code coverage
680     foreach (e; r)
681     {
682     }
683     assert(r.empty);
684 
685     foreach (i, e; r2)
686     {
687         e.shouldBe(i + begin);
688 
689         if (e == 7)
690         {
691             break;
692         }
693     }
694     r2.front.shouldBe(7);
695 
696     // Fully consume for code coverage
697     foreach (i, e; r2)
698     {
699     }
700     assert(r2.empty);
701 }
702 
703 unittest
704 {
705     // RandomAccessRange tests
706 
707     import std.algorithm : each;
708 
709     auto r = iota(0, 7).cached;
710     iota(5, 0, -1).each!(i => r[i].shouldBe(i));
711 }
712 
713 unittest
714 {
715     // hasLength tests
716 
717     import std.range : hasLength;
718 
719     {
720         auto r = iota(10).cached;
721         static assert (hasLength!(typeof(r)));
722     }
723     {
724         auto r = iota(10).filter!(i => i % 2).cached;
725         static assert (!hasLength!(typeof(r)));
726     }
727 }
728 
729 unittest
730 {
731     // Although it is discouraged to have side-effects in a map lambda, let's test
732     // with such a map to prove that the lambda is executed only once per element.
733 
734     import std.algorithm : find;
735 
736     enum n = 42;
737     enum nonExistingValue = n + 1;
738     size_t counter = 0;
739 
740     // Ensure all operations will be evaluated by searching for nonExistingValue
741     // in all elements of all sliding window ranges.
742     auto found = iota(n)
743                  .map!((i)
744                        {
745                            ++counter;
746                            return i;
747                        })
748                  .cached
749                  .slide(3)
750                  .find!(window => !window.find(nonExistingValue).empty);
751 
752     // Validate the test
753     assert(found.empty);
754 
755     counter.shouldBe(n);
756 }
757 
758 unittest
759 {
760     // Test that map expressions used by zip are cached as filter works on them.
761 
762     import std.array : array;
763     import std.range : zip;
764 
765     enum n = 10;
766     size_t count;
767 
768     auto r = zip(iota(0, n)
769                  .map!(_ => ++count))
770              .cached
771              .filter!(t => t[0] == t[0])  // Normally, multiple execution
772              .array;
773 
774     count.shouldBe(n);
775 }
776 
777 unittest
778 {
779     // Out-of-bounds access should throw
780 
781     import std.exception : assertThrown;
782 
783     assertThrown!Error(
784         {
785             auto r = iota(5).cached;
786             int result = 0;
787 
788             // More access than elements available
789             foreach (i; 0 .. 10)
790             {
791                 result += r.front;
792                 r.popFront();
793             }
794         }());
795 }
796 
797 unittest
798 {
799     // The following is the code that exposed a difference between Rust's
800     // tuple_window and D's slide. tuple_window uses once() in its
801     // implementation so the generator lambda is executed only once per
802     // element. D's slide calls map's front multiple times. .cached is supposed
803     // to prevent that.
804 
805     import std.array : array;
806 
807     static struct Data
808     {
809         int * ptr;
810         size_t capacity;
811     }
812 
813     enum n = 1_000;
814 
815     int[] v;
816 
817     auto r = iota(n)
818              .map!((i)
819                    {
820                        v ~= i;
821                        return Data(v.ptr, v.capacity);
822                    })
823              .cached    // The lambda should be executed once per object
824              .slide(2)  // [0,1], [1,2], [2,3], etc.
825              .filter!((t)
826                       {
827                           const prev = t.front.ptr;
828                           t.popFront();
829                           const curr = t.front.ptr;
830                           return prev != curr;
831                       })
832              .map!(t => t.front.capacity)
833              .array;
834 
835     v.length.shouldBe(n);
836 }
837 
838 unittest
839 {
840     // A single slice should be sufficient to keep all elements alive
841 
842     import std.algorithm : each;
843     import std.conv : to;
844 
845     // Picking a large number of elements along with very small capacity to
846     // cause at least one consideration of dropping the front elements
847     enum n = 10_000;
848     enum heapBlockCapacity = 5;
849 
850     auto r = iota(n).cached(1000);
851 
852     {
853         void consume(A)(ref A a)
854         {
855             while(!a.empty)
856             {
857                 a.popFront();
858             }
859         }
860 
861         // Create saved states of the range
862         auto saveds = iota(4).map!(_ => r.save()).array;
863 
864         // This operation will ensure populating the element cache
865         consume(r);
866 
867         // No leading element should have been dropped
868         assert(saveds[0].length == n);
869 
870         // Consume all saved states but one
871         foreach (ref s; saveds[1..$])
872         {
873             consume(s);
874         }
875 
876         // Still, no leading element should have been dropped
877         assert(saveds[0].length == n);
878 
879         // Finally, there should be dropping of leading elements
880         consume(saveds[0]);
881     }
882 
883     // We expect non-zero figures
884     const stats = r.stats;
885     assert(stats.heapAllocations);
886     assert(stats.leadingDropRuns);
887     assert(stats.droppedElements);
888     version (RUN_COMPACTION)
889     {
890         assert(stats.compactionRuns);
891         assert(stats.removedBlocks);
892     } else {
893         assert(stats.compactionRuns == 0);
894         assert(stats.removedBlocks == 0);
895     }
896 
897     stats.to!string;
898 }