perf(layout): id-keyed fit_image cache entries no longer pin the source (#732)

LayoutContext.fit_image keyed images without a cache_key by id() and held
a strong reference to the source so the id could not be recycled. A
plugin following the documented one-liner -- draw_image(Image.open(path),
box) each frame -- never hit that cache and kept the last 64 sources
alive: ~64MB for 500x500 RGBA team logos (median size under
assets/sports), up to ~600MB for the largest.

The entry now holds a weak reference whose callback drops it when the
source is freed, and a hit re-checks that the referent is the same
image. Sources that cannot be weak-referenced are still pinned. Keyed
entries (the only kind any plugin on ledmatrix-plugins main uses today:
football-scoreboard's logo fit) are unchanged.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Chuck
2026-10-03 12:11:27 -04:00
committed by GitHub
co-authored by Claude Opus 5.5
parent 76ad71d1c5
commit 6bf7c3fa51
2 changed files with 75 additions and 17 deletions
+41 -5
View File
@@ -145,13 +145,49 @@ class TestContextImageCache:
a = ctx.fit_image(img, (20, 20))
assert ctx.fit_image(img, (20, 20)) is a
def test_id_safety_pins_source(self, ctx):
# id()-keyed entries must pin the source image so a recycled id
# can't alias a dead image's cache entry.
def test_id_keyed_entry_does_not_pin_source(self, ctx):
import gc
import weakref
img = _solid(10, 10)
ctx.fit_image(img, (20, 20))
pinned = [entry[1] for entry in ctx._image_cache.values()]
assert img in pinned
assert len(ctx._image_cache) == 1
watch = weakref.ref(img)
del img
gc.collect()
assert watch() is None # the cache did not keep it alive
assert len(ctx._image_cache) == 0 # and its entry went with it
def test_fresh_image_each_frame_holds_nothing(self, ctx):
# draw_image(Image.open(path), box) every frame: the old pinning
# filled all 64 slots with dead-weight sources.
for _ in range(ctx._IMAGE_CACHE_MAX * 2):
ctx.fit_image(_solid(50, 50), (20, 20))
assert len(ctx._image_cache) == 0
def test_recycled_id_does_not_alias(self, ctx):
# A same-size image at a recycled address must not get the dead
# image's fit, even if the entry somehow outlived its source.
red = _solid(10, 10, (255, 0, 0, 255))
first = ctx.fit_image(red, (20, 20))
key = next(iter(ctx._image_cache))
entry = ctx._image_cache[key]
ctx._image_cache[key] = (entry[0], lambda: None) # source "gone"
again = ctx.fit_image(red, (20, 20))
assert again is not first # refit, not a stale hit
assert ctx.fit_image(red, (20, 20)) is again
def test_unweakrefable_source_is_pinned(self, ctx, monkeypatch):
import src.adaptive_layout as layout_mod
def no_weakref(*_args, **_kwargs):
raise TypeError("cannot create weak reference")
monkeypatch.setattr(layout_mod.weakref, "ref", no_weakref)
img = _solid(10, 10)
a = ctx.fit_image(img, (20, 20))
assert ctx.fit_image(img, (20, 20)) is a
assert next(iter(ctx._image_cache.values()))[1]() is img
def test_cache_key_entries_do_not_pin(self, ctx):
img = _solid(10, 10)