mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-10-04 06:15:09 +00:00
- DisplayManager.defer_update()/process_deferred_updates(): one lock around every queue mutation (appends from the update thread were lost to the render thread's filter/slice reassignments); callables run outside it. - FontManager and element_style no longer cache BDF freetype.Face objects process-wide (load_bdf_face caches them per thread); element_style's LRU is locked against get/move_to_end vs eviction races. - limit_refresh_rate_hz default is one constant, DEFAULT_REFRESH_LIMIT_HZ = 100 (the template's), for the library options, refresh_hz, the matrix guard, Vegas and scroll_config. Previously a missing key capped the panel at 90 while pacing assumed 100. - Sync follower: the TCP thread queues the leader's scroll image; the render thread swaps image/array/width in between frames. - update_display() error log rate-limited (traceback first, then once a minute with a count); swallowed DisplayController exceptions log at DEBUG. - Root display_controller.py runs run.py via runpy. - stream_manager: correct the RLock release comments; merge duplicate if. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
94 lines
3.3 KiB
Python
94 lines
3.3 KiB
Python
"""BDF faces must not be shared between threads.
|
|
|
|
src/common/bdf_font.load_bdf_face caches ``freetype.Face`` objects per thread
|
|
because FreeType does not allow two threads to use one face at once
|
|
(``load_char`` rewrites its glyph slot). FontManager.font_cache and
|
|
element_style._font_cache are process-wide, and used to cache the returned
|
|
face for every thread -- so the display thread and a plugin update thread
|
|
drew through the same Face.
|
|
"""
|
|
|
|
import threading
|
|
|
|
import freetype
|
|
import pytest
|
|
|
|
from src import element_style
|
|
from src.font_manager import FontManager
|
|
|
|
|
|
def _on_other_thread(fn):
|
|
box = {}
|
|
|
|
def run():
|
|
box['value'] = fn()
|
|
|
|
t = threading.Thread(target=run)
|
|
t.start()
|
|
t.join(timeout=10)
|
|
assert not t.is_alive()
|
|
return box['value']
|
|
|
|
|
|
def test_font_manager_gives_each_thread_its_own_bdf_face():
|
|
fm = FontManager({})
|
|
here = fm.get_font("five_by_seven", 7)
|
|
there = _on_other_thread(lambda: fm.get_font("five_by_seven", 7))
|
|
assert isinstance(here, freetype.Face) and isinstance(there, freetype.Face)
|
|
assert here is not there
|
|
# Within one thread the face is still reused.
|
|
assert fm.get_font("five_by_seven", 7) is here
|
|
|
|
|
|
def test_font_manager_still_caches_ttf():
|
|
fm = FontManager({})
|
|
here = fm.get_font("press_start", 8)
|
|
assert _on_other_thread(lambda: fm.get_font("press_start", 8)) is here
|
|
|
|
|
|
def test_element_style_gives_each_thread_its_own_bdf_face():
|
|
element_style._font_cache.clear()
|
|
try:
|
|
here = element_style.load_font("5x7.bdf", 7)
|
|
there = _on_other_thread(lambda: element_style.load_font("5x7.bdf", 7))
|
|
assert isinstance(here, freetype.Face) and isinstance(there, freetype.Face)
|
|
assert here is not there
|
|
assert element_style.load_font("5x7.bdf", 7) is here
|
|
finally:
|
|
element_style._font_cache.clear()
|
|
|
|
|
|
def test_element_style_cache_survives_concurrent_eviction(monkeypatch):
|
|
"""get() then move_to_end() on the shared OrderedDict raised KeyError when
|
|
another thread evicted the key in between. Forced deterministically: the
|
|
first get() hands the cache to a second thread that fills it past the
|
|
bound before get() returns."""
|
|
from collections import OrderedDict
|
|
|
|
owner = threading.get_ident()
|
|
state = {'worker': None}
|
|
|
|
class RacingCache(OrderedDict):
|
|
def get(self, key, default=None):
|
|
value = super().get(key, default)
|
|
if state['worker'] is None and threading.get_ident() == owner:
|
|
state['worker'] = threading.Thread(
|
|
target=lambda: [element_style.load_font(
|
|
"PressStart2P-Regular.ttf", s) for s in (20, 21, 22)],
|
|
daemon=True)
|
|
state['worker'].start()
|
|
# Unguarded, the worker evicts `key` now; guarded, it waits
|
|
# for the lock this thread holds.
|
|
state['worker'].join(timeout=0.5)
|
|
return value
|
|
|
|
monkeypatch.setattr(element_style, "_FONT_CACHE_MAX", 2)
|
|
cache = RacingCache()
|
|
monkeypatch.setattr(element_style, "_font_cache", cache)
|
|
element_style.load_font("PressStart2P-Regular.ttf", 8) # populate
|
|
state['worker'] = None
|
|
element_style.load_font("PressStart2P-Regular.ttf", 8) # hit, raced
|
|
state['worker'].join(timeout=10)
|
|
assert not state['worker'].is_alive()
|
|
assert len(cache) <= 2
|