1 /// Provides an arena for keeping track of and periodically freeing unused resources.
2 module fluid.future.arena;
3 
4 import std.meta;
5 import std.array;
6 import std.algorithm;
7 
8 // mustuse is not available in LDC 1.28
9 static if (__traits(compiles, { import core.attribute : mustuse; }))
10     import core.attribute : mustuse;
11 else
12     private alias mustuse = AliasSeq!();
13 
14 @safe:
15 
16 /// A resource arena keeps track of resources in use, taking note of the time since the resource was last updated.
17 /// Time is tracked in terms of "cycles". Resources are expected to be updated every cycle, or be freed. The time
18 /// it takes to actually free resource can be adjusted.
19 ///
20 /// The simplest way to understand the resource arena is by assuming all resources are freed every cycle:
21 ///
22 /// ---
23 /// const resourceID = arena.load(...);  // Resource loaded
24 /// arena.startCycle();  // Unloaded
25 /// ---
26 ///
27 /// In a practical scenario, resources will likely continue to be used across cycles. The `load` call will be repeated
28 /// next cycle with the same resource. The arena will therefore *not* free resources immediately, but keep them alive
29 /// in the background for long enough they can be reused for another `load`. This means that on the outside
30 /// the resources appear freed, while they stay active until it can be confirmed they will not be reused.
31 ///
32 /// The amount of cycles resources stay alive for can be adjusted by setting `arena.resourceLifetime`. The default is
33 /// to preserve resources for one cycle (1). Forcing immediate frees like in the example above can be achieved by
34 /// setting it to zero (0).
35 ///
36 /// In Fluid, the resource arena is mostly used by I/O systems to manage resources that nodes will allocate.
37 /// A cycle starts when the I/O system begins resizing:
38 ///
39 /// ---
40 /// ResourceArena!Image images;
41 /// override void resizeImpl(Vector2 space) {
42 ///     images.startCycle();
43 ///     super.resizeImpl(space);
44 /// }
45 /// override int load(DrawableImage image) {
46 ///     return images.load(image);
47 /// }
48 /// ---
49 struct ResourceArena(T) {
50 
51     private struct Resource {
52 
53         /// The resource.
54         T value;
55 
56         /// Number of the last cycle when the resource was used.
57         int lastCycle;
58 
59     }
60 
61     public {
62 
63         /// Number of the current cycle. Cycles are used to determine the lifetimes stored in the arena.
64         /// This field is incremented every time a new cycle starts.
65         int cycleNumber;
66 
67         /// Number of cycles a resource stays alive for, beyond the cycle they were created during.
68         ///
69         /// If set to zero (0), resources are freed whenever `startCycle` is called. If set to one (1), the default,
70         /// resources have to remain unused for a cycle to be freed.
71         int resourceLifetime = 1;
72 
73     }
74 
75     private {
76 
77         /// Storage for currently allocated resources.
78         Appender!(Resource[]) _resources;
79 
80     }
81 
82     /// Returns: Number of all resources, active or not.
83     int resourceCount() const nothrow {
84 
85         return cast(int) _resources[].length;
86 
87     }
88 
89     /// List every resource in the arena.
90     /// Returns: A range that lists every resource.
91     auto allResources(this This)() nothrow {
92 
93         return _resources[].map!(a => a.value);
94 
95     }
96 
97     /// List every active resource in the arena. Omits expired resources.
98     /// Returns: A range that lists every active resource.
99     auto activeResources(this This)() nothrow {
100 
101         import std.range;
102 
103         alias R = typeof(This._resources[].front);
104         alias Item = typeof(R.value);
105 
106         struct ActiveResources {
107 
108             private R[] resources;
109             private int cycleNumber;
110 
111             this(R[] resources, int cycleNumber) {
112                 this.resources = resources;
113                 this.cycleNumber = cycleNumber;
114                 this.advance();
115             }
116 
117             bool empty() const {
118                 return resources.empty;
119             }
120 
121             Item front() {
122                 return resources.front.value;
123             }
124 
125             void popFront() {
126                 resources.popFront();
127                 advance();
128             }
129 
130             ActiveResources save() {
131                 return this;
132             }
133 
134             /// Omit expired resources
135             private void advance() {
136                 while (!empty && resources.front.lastCycle < cycleNumber) {
137                     resources.popFront();
138                 }
139             }
140 
141         }
142 
143         return ActiveResources(_resources[], cycleNumber);
144 
145     }
146 
147     /// Only resources that were reloaded are included when using `[]`.
148     @("ResourceArena.activeResources only provides resources that have been reloaded.")
149     unittest {
150 
151         ResourceArena!int arena;
152         arena.load(0);
153         arena.load(1);
154         arena.load(2);
155         assert(arena.activeResources.equal([0, 1, 2]));
156 
157         // Observe the results as we reload the resources during the next cycle
158         arena.startCycle();
159         assert(arena.activeResources.empty);
160         arena.reload(0, 0);
161         assert(arena.activeResources.equal([0]));
162         arena.reload(2, 2);
163         assert(arena.activeResources.equal([0, 2]));
164         arena.reload(1, 1);
165         assert(arena.activeResources.equal([0, 1, 2]));  // Order of insertion
166 
167     }
168 
169     /// Get a resource by its index.
170     ///
171     /// This may return an expired resource, which can be then brought back to life
172     /// with an `update` call.
173     ///
174     /// Params:
175     ///     index = Index of the resource.
176     /// Returns:
177     ///     The resource.
178     ref inout(T) opIndex(int index) inout {
179 
180         return _resources[][index].value;
181 
182     }
183 
184     /// Check if resource at given index is active; A resource is active if it has been loaded during this cycle.
185     /// An active resource can be fetched using `opIndex`.
186     ///
187     /// For the looser variant, which would return true also if the resource is still loaded in arena,
188     /// even if not recently loaded, see `isAlive`.
189     ///
190     /// Params:
191     ///     index = Index to check.
192     /// Returns:
193     ///     True if the resource is active: allocated, alive, ready to use.
194     bool isActive(int index) const {
195 
196         return isAlive(index)
197             && cycleNumber <= _resources[][index].lastCycle;
198 
199     }
200 
201     /// Check if the resource is still loaded in the area.
202     ///
203     /// This will return true as long as the resource hasn't expired yet, even if it hasn't been loaded during
204     /// this cycle. `isAlive` is intended for diagnostics and debugging, rather than practical usage. See `isActive`
205     /// for the stricter variant that will not return `true` for unloaded resources.
206     ///
207     /// Params:
208     ///     index = Index to check.
209     /// Returns:
210     ///     True if the resource is alive, that is, hasn't expired, hasn't been unloaded, but also hasn't been
211     ///     loaded/used during this cycle.
212     bool isAlive(int index) const {
213 
214         // Any resource in bounds is alive
215         return index < _resources[].length;
216 
217     }
218 
219     /// Start a new cycle.
220     ///
221     /// Each time a cycle is started, all expired resources are freed, making space for new resources.
222     /// Resources that are kept alive, are moved to the start of the array, effectively changing their indices.
223     /// Iterate on the return value to safely update your resources to use the new indices.
224     ///
225     /// Params:
226     ///     moved = A function that handles freeing and moving resources.
227     ///         It is called with a pair (index, resource) for every item that was freed or moved.
228     ///         For freed resources, the index is set to `-1`.
229     /// See_Also:
230     ///     `resourceLifetime`
231     auto startCycle(scope void delegate(int index, ref T resource) @safe moved = null) {
232 
233         int newIndex;
234 
235         foreach (oldIndex, ref resource; _resources[]) {
236 
237             // Free the resource if it has expired
238             if (cycleNumber >= resource.lastCycle + resourceLifetime) {
239                 if (moved) {
240                     moved(-1, resource.value);
241                 }
242                 continue;
243             }
244 
245             // Still alive, but moved
246             else if (oldIndex != newIndex) {
247                 _resources[][newIndex] = resource;
248                 if (moved) {
249                     moved(newIndex, _resources[][newIndex].value);
250                 }
251             }
252 
253             // Still alive, keep the index up
254             newIndex++;
255 
256         }
257 
258         // Truncate the array and advance to the next cycle
259         _resources.shrinkTo(newIndex);
260         cycleNumber++;
261 
262     }
263 
264     /// Example: Maintaining a hash map of resource IDs to indices.
265     @("ResourceArena: Maintaining a hash map of resource IDs to indices.")
266     unittest {
267 
268         ResourceArena!string arena;
269         int[string] indices;
270         indices["One"]   = arena.load("One");
271         indices["Two"]   = arena.load("Two");
272         indices["Three"] = arena.load("Three");
273 
274         void nextCycle() {
275 
276             arena.startCycle((newIndex, ref resource) {
277 
278                 // Freed
279                 if (newIndex == -1) {
280                     indices.remove(resource);
281                 }
282                 // Moved
283                 else {
284                     indices[resource] = newIndex;
285                 }
286 
287             });
288 
289         }
290 
291         // Start a cycle and remove "Two" from the arena
292         nextCycle();
293         arena.reload(indices["One"], "One");
294         arena.reload(indices["Three"], "Three");
295         assert(indices["One"]   == 0);
296         assert(indices["Two"]   == 1);
297         assert(indices["Three"] == 2);
298 
299         // Next cycle the resource should be freed
300         nextCycle();
301         assert(indices["One"]   == 0);
302         assert(indices["Three"] == 1);
303 
304     }
305 
306     /// Load a resource into the arena.
307     ///
308     /// The resource must be new; the arena will not compare it against existing
309     /// resources, nor will it attempt reuse. If the resource is already loaded,
310     /// use `reload`.
311     ///
312     /// Params:
313     ///     resource = Resource to load.
314     /// Returns:
315     ///     Index associated with the resource.
316     int load(T resource) {
317 
318         const index = cast(int) _resources[].length;
319         _resources ~= Resource(resource, cycleNumber);
320         return index;
321 
322     }
323 
324     /// Reload a resource.
325     /// Params:
326     ///     index    = Index the resource is stored under.
327     ///     resource = Stored resource.
328     void reload(int index, T resource) {
329 
330         _resources[][index] = Resource(resource, cycleNumber);
331 
332     }
333 
334 }
335 
336 /// Check if a resource has been using during this cycle with `isActive`.
337 @("ResourceArena.isActive and isAlive can be used to inspect activity")
338 unittest {
339 
340     ResourceArena!string arena;
341     auto zero = arena.load("Zero");
342     auto one  = arena.load("One");
343     auto two  = 2;
344     assert( arena.isActive(zero));
345     assert( arena.isActive(one));
346     assert(!arena.isActive(two));
347 
348     // Resources are marked inactive when a new cycle starts
349     arena.startCycle();
350     assert(!arena.isActive(zero));  // The resource isn't active now
351     assert( arena.isAlive(zero));   // but it remains loaded
352 
353     // Loading a resource turns it active
354     arena.reload(zero, "Zero");
355     assert( arena.isActive(zero));
356     assert( arena.isAlive(zero));
357     assert(!arena.isActive(one));
358     assert( arena.isAlive(one));
359     assert(!arena.isActive(two));
360     assert(!arena.isAlive(two));
361 
362     // "Two" hasn't been used on the last cycle, so it will be freed
363     arena.startCycle();
364     assert( arena.isAlive(zero));
365     assert(!arena.isAlive(one));
366     assert(!arena.isAlive(two));
367 
368 }
369 
370 /// Disable keeping objects alive by setting `resourceLifetime` to zero.
371 @("ResourceArena frees everything if `resourceLifetime` is set to zero")
372 unittest {
373 
374     ResourceArena!int arena;
375     arena.resourceLifetime = 0;
376     arena.load(0);
377     assert( arena.isActive(0));
378     assert( arena.isAlive(0));
379 
380     // 0 will be freed on a new cycle
381     arena.startCycle();
382     assert(!arena.isActive(0));
383     assert(!arena.isAlive(0));
384 
385 }