Reference counted garbage collection for the Images cache

#4083 · open · 0 comments

View on GitHub ↗

spydon

### Problem to solve The `Images` cache has no way to free images that are no longer used. Once an image is loaded it stays in memory until someone calls `clear` or `clearCache` by hand, and nothing in Flame tracks whether an image is still referenced. A user reported that loading all of their game's images up front took almost 1 GB of RAM, and they had to write their own collector that culls unused images. The underlying issue is that everything downstream of the cache (`Sprite`, `SpriteSheet`, `SpriteBatch`, `Parallax`, the tiled atlas, and so on) holds a bare `dart:ui` `Image` with no link back to the cache. So the cache cannot tell whether an entry is in use, and disposing a referenced image crashes the next render. Any automatic collection has to solve that first. ### Proposal Add reference counted collection to `Images`, modeled on how Flutter's own `ImageCache` separates "live" images (tracked by listeners, never evicted) from a size bounded LRU of everything else. **1. Bookkeeping in `Images`** - Each entry gets a reference count, an estimated byte size (`width * height * 4`) and a last used timestamp. - Add an `Expando<_ImageAsset>` from `Image` to entry so lookups by image are constant time. This also replaces the linear scan in `findKeyForImage`. - Expose a `sizeBytes` getter so the total can be inspected, and surface it in `flame_devtools`. **2. Retain and release** - Add `images.retain(Image)` and `images.release(Image)`. Both are no-ops for images the cache does not own. - Add a small `Component` mixin that retains the images a component draws in `onMount` and releases them in `onRemove`. - Apply it to the image holding components: `SpriteComponent`, `SpriteAnimationComponent`, `SpriteGroupComponent`, `SpriteAnimationGroupComponent`, `SpriteBatchComponent`, `ParallaxComponent`, `NineTileBoxComponent`, `IsometricTileMapComponent`, and the `flame_tiled` atlas. - The `sprite`, `sprites`, `animation` and `animations` setters swap images after mount, so they must release the old image and retain the new one. This is where most of the work is. **3. Collection policy** - Add `collect()`, which disposes entries whose reference count is zero. - Add an optional `maxSizeBytes` budget. When a load pushes the cache over the budget, `collect()` runs automatically, evicting unreferenced entries in least recently used order until the cache is under budget again. - Unreferenced entries get a grace period before they are eligible, since the common flow is `await images.load()` in `onLoad` followed by the retain in `onMount`, with a gap between them. - Collection only ever touches unreferenced entries, so it is safe by construction for anything that goes through the components above. - In debug mode, assert when a collected image is accessed, so that user code which keeps a raw image from `fromCache` in a field without retaining it fails loudly. Everything should be opt-in and non-breaking: with no `maxSizeBytes` set and no `collect()` call, the cache behaves exactly as it does today. The documentation should also be updated to recommend loading images per level or scene and letting the cache collect between them, since a collector only helps when there is something unreferenced to free. ### More information Flutter's `ImageCache` in `package:flutter/painting` is the reference for the live versus LRU split: https://api.flutter.dev/flutter/painting/ImageCache-class.html ### Other - [x] Are you interested in working on a PR for this?

Comments