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:
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user