Throttle SSE notifications and debounce client refreshes

Server-side:
- Limit SSE notifications to max once per second per route
- Track last notification time per route

Client-side:
- Debounce screenshot refreshes with 2s minimum interval per tile
- Pending refreshes are scheduled if events arrive during debounce window
This commit is contained in:
GitHub Copilot
2026-01-24 12:20:49 +00:00
parent 73c520b0c6
commit de5ea155a0
+29 -3
View File
@@ -147,8 +147,12 @@ def _rewrite_svg_fonts(svg: str) -> str:
class LocalServer: class LocalServer:
def mark_route_activity(self, route_key: str) -> None: def mark_route_activity(self, route_key: str) -> None:
self._route_last_activity[route_key] = asyncio.get_event_loop().time() now = asyncio.get_event_loop().time()
# Notify SSE subscribers of activity self._route_last_activity[route_key] = now
# Throttle SSE notifications - max once per second per route
last_notified = self._route_last_sse_notification.get(route_key, 0.0)
if now - last_notified >= 1.0:
self._route_last_sse_notification[route_key] = now
self._notify_activity(route_key) self._notify_activity(route_key)
def _notify_activity(self, route_key: str) -> None: def _notify_activity(self, route_key: str) -> None:
@@ -222,6 +226,7 @@ class LocalServer:
self._screenshot_cache_etag: dict[str, str] = {} self._screenshot_cache_etag: dict[str, str] = {}
self._screenshot_locks: dict[str, asyncio.Lock] = {} self._screenshot_locks: dict[str, asyncio.Lock] = {}
self._route_last_activity: dict[str, float] = {} self._route_last_activity: dict[str, float] = {}
self._route_last_sse_notification: dict[str, float] = {}
# SSE subscribers for activity notifications # SSE subscribers for activity notifications
self._sse_subscribers: list[asyncio.Queue[str]] = [] self._sse_subscribers: list[asyncio.Queue[str]] = []
@@ -874,12 +879,33 @@ class LocalServer:
// SSE connection for real-time screenshot updates // SSE connection for real-time screenshot updates
let eventSource = null; let eventSource = null;
let sparklineTimer = null; let sparklineTimer = null;
// Debounce tracking per tile
const pendingRefresh = {{}};
const lastRefresh = {{}};
const REFRESH_DEBOUNCE_MS = 2000; // Min 2s between refreshes per tile
function scheduleRefreshTile(slug) {{
const now = Date.now();
const last = lastRefresh[slug] || 0;
// If we refreshed recently, schedule for later
if (now - last < REFRESH_DEBOUNCE_MS) {{
if (!pendingRefresh[slug]) {{
pendingRefresh[slug] = setTimeout(() => {{
pendingRefresh[slug] = null;
refreshTile(slug);
}}, REFRESH_DEBOUNCE_MS - (now - last));
}}
return;
}}
refreshTile(slug);
lastRefresh[slug] = now;
}}
function startSSE() {{ function startSSE() {{
if (eventSource) return; if (eventSource) return;
eventSource = new EventSource('/events'); eventSource = new EventSource('/events');
eventSource.addEventListener('activity', (e) => {{ eventSource.addEventListener('activity', (e) => {{
refreshTile(e.data); scheduleRefreshTile(e.data);
}}); }});
eventSource.onerror = () => {{ eventSource.onerror = () => {{
// Reconnect on error // Reconnect on error