mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-10-10 09:06:36 +00:00
feat(vegas): ESPN fetches give way to the render thread too
The hourly sports refresh froze hdpi's Vegas scroll for 1.9s: about twenty espn-chunk threads fetching and parsing at once, and the render thread (and the stall watchdog) queued behind all of them for the GIL. The prefetch gate only covered the prefetch thread. Vegas now makes its gate the active one (render_gate.set_active), and render_gate.yielding() gives way through it when there is one and does nothing otherwise. espn_dates wraps each chunk fetch in it (behind the same import fallback as json_body, for the copies plugins bundle), and the background data service wraps each worker. The render thread is never gated -- the first thread to swap is exempt, and a plugin pushing a live refresh from its update thread cannot take its place -- and nested blocks keep the outermost frame as the boundary for the lock checks. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -242,6 +242,102 @@ class TestYielding:
|
||||
assert sys.getprofile() is before
|
||||
|
||||
|
||||
class TestWhoIsGated:
|
||||
def _in_thread(self, fn):
|
||||
out = []
|
||||
thread = threading.Thread(target=lambda: out.append(fn()))
|
||||
thread.start()
|
||||
thread.join(5)
|
||||
return out[0]
|
||||
|
||||
def test_the_render_thread_never_gives_way(self):
|
||||
gate = RenderGate()
|
||||
gate.before_swap(1)
|
||||
gate.after_swap(1) # this thread swaps: it is the render thread
|
||||
with gate.yielding():
|
||||
assert sys.getprofile() is not gate._hook
|
||||
|
||||
def background():
|
||||
with gate.yielding():
|
||||
return sys.getprofile() == gate._hook
|
||||
assert self._in_thread(background)
|
||||
|
||||
def test_a_live_refresh_from_another_thread_does_not_take_its_place(self):
|
||||
gate = RenderGate()
|
||||
gate.after_swap(1) # the render loop
|
||||
self._in_thread(lambda: gate.after_swap(1)) # a plugin pushing a frame
|
||||
with gate.yielding():
|
||||
assert sys.getprofile() is not gate._hook
|
||||
|
||||
def test_nested_blocks_keep_the_outer_boundary(self):
|
||||
gate = RenderGate()
|
||||
|
||||
def nested():
|
||||
with gate.yielding():
|
||||
outer = gate._local.base
|
||||
with gate.yielding():
|
||||
inner = gate._local.base
|
||||
after = gate._local.base
|
||||
still_hooked = sys.getprofile() == gate._hook
|
||||
return outer is inner is after and still_hooked, sys.getprofile()
|
||||
kept, final = self._in_thread(nested)
|
||||
assert kept and final is None
|
||||
|
||||
|
||||
class TestActiveGate:
|
||||
@pytest.fixture(autouse=True)
|
||||
def no_gate_left_behind(self):
|
||||
yield
|
||||
render_gate.set_active(None)
|
||||
|
||||
def test_without_a_vegas_run_nothing_is_gated(self):
|
||||
render_gate.set_active(None)
|
||||
with render_gate.yielding():
|
||||
assert sys.getprofile() is None
|
||||
|
||||
def test_with_one_background_work_gives_way(self):
|
||||
gate = RenderGate()
|
||||
render_gate.set_active(gate)
|
||||
with render_gate.yielding():
|
||||
assert sys.getprofile() == gate._hook
|
||||
assert sys.getprofile() is None
|
||||
|
||||
def test_espn_chunk_fetches_give_way(self):
|
||||
from src.common import espn_dates
|
||||
gate = RenderGate()
|
||||
render_gate.set_active(gate)
|
||||
seen = []
|
||||
|
||||
class Response:
|
||||
content = b'{"events": []}'
|
||||
|
||||
def raise_for_status(self):
|
||||
pass
|
||||
|
||||
def json(self):
|
||||
return {"events": []}
|
||||
|
||||
class Session:
|
||||
def get(self, *args, **kwargs):
|
||||
seen.append(sys.getprofile() == gate._hook)
|
||||
return Response()
|
||||
assert espn_dates._fetch_one_chunk(
|
||||
Session(), "http://x", {}, {}, 5, None, "20260924") == {"events": []}
|
||||
assert seen == [True]
|
||||
|
||||
def test_background_data_fetches_give_way(self):
|
||||
from src.background_data_service import BackgroundDataService
|
||||
gate = RenderGate()
|
||||
render_gate.set_active(gate)
|
||||
service = BackgroundDataService.__new__(BackgroundDataService)
|
||||
service._shutdown = True # never started: nothing for __del__ to stop
|
||||
seen = []
|
||||
service._fetch_data = lambda request: seen.append(
|
||||
sys.getprofile() == gate._hook) or "result"
|
||||
assert service._fetch_data_worker(object()) == "result"
|
||||
assert seen == [True]
|
||||
|
||||
|
||||
class TestDisplayManager:
|
||||
@pytest.fixture
|
||||
def dm(self):
|
||||
@@ -332,8 +428,10 @@ class TestVegasWiring:
|
||||
assert c._state_lock in gate._guarded
|
||||
assert c.stream_manager._buffer_lock in gate._guarded
|
||||
assert c.plugin_adapter._cache_lock in gate._guarded
|
||||
assert render_gate.active() is gate # background fetches use it too
|
||||
c._remove_render_gate()
|
||||
assert c.display_manager.render_gate is None
|
||||
assert render_gate.active() is None
|
||||
|
||||
@pytest.mark.parametrize("releases", [False, None])
|
||||
def test_ignored_without_a_binding_that_releases_the_gil(self, monkeypatch, releases):
|
||||
|
||||
Reference in New Issue
Block a user