Render widgets concurrently instead of one at a time
A layout with several network-backed widgets (photos, weather, calendar) paid their fetch latency serially in one /frame/* request, which could exceed the firmware's fixed HTTP timeout and show a false "server failed" status screen even though the server was still working -- most visibly on the hold-triggered "cycle layouts" action, which swaps in a whole new, cold-started widget set. Each widget now renders on its own DB session in a thread pool (a plain Session isn't thread-safe to share, but the per-frame threading.Lock in frame_locked/widget_locked already made this kind of concurrency safe by design -- see app/db.py); regions are still collected in sort_order so overlapping widgets paint in the same z-order as before.
This commit is contained in:
@@ -144,3 +144,50 @@ def test_two_widget_frame_composites_both_and_next_targets_calendar(client, db_s
|
||||
photo_cfg = db_session.get(PhotoWidgetConfig, photo_widget.id)
|
||||
assert cal_cfg.browse_offset == 1
|
||||
assert photo_cfg.current_asset_id == "" # untouched -- NEXT was never bound to it
|
||||
|
||||
|
||||
def test_widgets_render_concurrently(client, db_session, monkeypatch):
|
||||
"""Two independent, slow widgets on one frame should render in
|
||||
roughly the time of the slowest one, not the sum -- the actual fix
|
||||
for the "hold to cycle layouts times out and shows a false server-
|
||||
failed status screen" bug: several network-backed widgets (photos,
|
||||
weather, calendar) rendering one after another could push a single
|
||||
/frame/* response past the firmware's fixed HTTP timeout even though
|
||||
the server was simply still working."""
|
||||
monkeypatch.setattr(widgets.photos, "immich_client_for", lambda frame: object())
|
||||
monkeypatch.setattr(widgets.photos, "list_assets", lambda client, album_id: _ASSETS)
|
||||
|
||||
from PIL import Image
|
||||
source = Image.new("RGB", (100, 80), (10, 20, 30))
|
||||
|
||||
def _slow_fetch(client, mode, asset_id):
|
||||
time.sleep(0.25)
|
||||
return source, None
|
||||
|
||||
monkeypatch.setattr(widgets.photos, "fetch_source_and_faces", _slow_fetch)
|
||||
|
||||
frame = Frame(
|
||||
name="Concurrency Frame", device_id="112233445566", device_token="devtok-3",
|
||||
manage_token="mtok-3", orientation="landscape", created_at=time.time(),
|
||||
)
|
||||
db_session.add(frame)
|
||||
db_session.flush()
|
||||
|
||||
widget_a = Widget(frame_id=frame.id, widget_type="photos", x=0, y=0, w=4, h=5,
|
||||
sort_order=0, created_at=time.time())
|
||||
widget_b = Widget(frame_id=frame.id, widget_type="photos", x=4, y=0, w=4, h=5,
|
||||
sort_order=1, created_at=time.time())
|
||||
db_session.add_all([widget_a, widget_b])
|
||||
db_session.flush()
|
||||
db_session.add(PhotoWidgetConfig(widget_id=widget_a.id, album_id="album-a"))
|
||||
db_session.add(PhotoWidgetConfig(widget_id=widget_b.id, album_id="album-b"))
|
||||
db_session.commit()
|
||||
|
||||
start = time.monotonic()
|
||||
resp = client.get(f"/frame/image?id={frame.device_id}&token={frame.device_token}")
|
||||
elapsed = time.monotonic() - start
|
||||
|
||||
assert resp.status_code == 200
|
||||
assert len(resp.content) == EXPECTED_BYTES
|
||||
# Serial would be ~0.5s (2 x 0.25s); concurrent should land near 0.25s.
|
||||
assert elapsed < 0.45, f"widgets rendered serially, not concurrently ({elapsed:.2f}s)"
|
||||
|
||||
Reference in New Issue
Block a user