feat: add time-based throttling to progress broadcasts

Add 300ms minimum interval between progress broadcasts to reduce
WebSocket message volume. Broadcasts are sent immediately for
significant changes (>=1% or forced), otherwise throttled.

- Add MIN_BROADCAST_INTERVAL class constant (0.3s)
- Track last broadcast time per progress_id using time.monotonic()
- Clean up broadcast timestamps when progress completes/fails/cancels
This commit is contained in:
2026-02-17 17:24:32 +01:00
parent 76f02ec822
commit 1c39dd5c6a

View File

@@ -8,6 +8,7 @@ to connected clients.
from __future__ import annotations from __future__ import annotations
import asyncio import asyncio
import time
from dataclasses import dataclass, field from dataclasses import dataclass, field
from datetime import datetime, timezone from datetime import datetime, timezone
from enum import Enum from enum import Enum
@@ -168,6 +169,9 @@ class ProgressService:
- Support for different progress types (download, scan, queue) - Support for different progress types (download, scan, queue)
""" """
# Minimum interval between broadcasts in seconds (300ms)
MIN_BROADCAST_INTERVAL: float = 0.3
def __init__(self): def __init__(self):
"""Initialize the progress service.""" """Initialize the progress service."""
# Active progress operations: id -> ProgressUpdate # Active progress operations: id -> ProgressUpdate
@@ -182,6 +186,9 @@ class ProgressService:
str, List[Callable[[ProgressEvent], None]] str, List[Callable[[ProgressEvent], None]]
] = {} ] = {}
# Track last broadcast time per progress_id for time-based throttling
self._last_broadcast_time: Dict[str, float] = {}
# Lock for thread-safe operations # Lock for thread-safe operations
self._lock = asyncio.Lock() self._lock = asyncio.Lock()
@@ -389,11 +396,21 @@ class ProgressService:
update.status = ProgressStatus.IN_PROGRESS update.status = ProgressStatus.IN_PROGRESS
update.updated_at = datetime.now(timezone.utc) update.updated_at = datetime.now(timezone.utc)
# Only broadcast if significant change or forced # Time-based throttle: broadcast at most every 300ms,
# or immediately for significant changes / forced broadcasts
now = time.monotonic()
last_broadcast = self._last_broadcast_time.get(progress_id, 0.0)
time_since_last = now - last_broadcast
percent_change = abs(update.percent - old_percent) percent_change = abs(update.percent - old_percent)
should_broadcast = force_broadcast or percent_change >= 1.0
should_broadcast = (
force_broadcast
or percent_change >= 1.0
or time_since_last >= self.MIN_BROADCAST_INTERVAL
)
if should_broadcast: if should_broadcast:
self._last_broadcast_time[progress_id] = time.monotonic()
room = _get_room_for_progress_type(update.type) room = _get_room_for_progress_type(update.type)
event = ProgressEvent( event = ProgressEvent(
event_type=f"{update.type.value}_progress", event_type=f"{update.type.value}_progress",
@@ -442,6 +459,7 @@ class ProgressService:
# Move to history # Move to history
del self._active_progress[progress_id] del self._active_progress[progress_id]
self._last_broadcast_time.pop(progress_id, None)
self._add_to_history(update) self._add_to_history(update)
logger.info( logger.info(
@@ -497,6 +515,7 @@ class ProgressService:
# Move to history # Move to history
del self._active_progress[progress_id] del self._active_progress[progress_id]
self._last_broadcast_time.pop(progress_id, None)
self._add_to_history(update) self._add_to_history(update)
logger.error( logger.error(
@@ -548,6 +567,7 @@ class ProgressService:
# Move to history # Move to history
del self._active_progress[progress_id] del self._active_progress[progress_id]
self._last_broadcast_time.pop(progress_id, None)
self._add_to_history(update) self._add_to_history(update)
logger.info( logger.info(