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 }