From 1c39dd5c6ab940c6fd4b3655dc9d0a2c3cd06080 Mon Sep 17 00:00:00 2001 From: Lukas Date: Tue, 17 Feb 2026 17:24:32 +0100 Subject: [PATCH] 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 --- src/server/services/progress_service.py | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/src/server/services/progress_service.py b/src/server/services/progress_service.py index 06d1beb..31717d8 100644 --- a/src/server/services/progress_service.py +++ b/src/server/services/progress_service.py @@ -8,6 +8,7 @@ to connected clients. from __future__ import annotations import asyncio +import time from dataclasses import dataclass, field from datetime import datetime, timezone from enum import Enum @@ -168,6 +169,9 @@ class ProgressService: - Support for different progress types (download, scan, queue) """ + # Minimum interval between broadcasts in seconds (300ms) + MIN_BROADCAST_INTERVAL: float = 0.3 + def __init__(self): """Initialize the progress service.""" # Active progress operations: id -> ProgressUpdate @@ -182,6 +186,9 @@ class ProgressService: 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 self._lock = asyncio.Lock() @@ -389,11 +396,21 @@ class ProgressService: update.status = ProgressStatus.IN_PROGRESS 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) - 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: + self._last_broadcast_time[progress_id] = time.monotonic() room = _get_room_for_progress_type(update.type) event = ProgressEvent( event_type=f"{update.type.value}_progress", @@ -442,6 +459,7 @@ class ProgressService: # Move to history del self._active_progress[progress_id] + self._last_broadcast_time.pop(progress_id, None) self._add_to_history(update) logger.info( @@ -497,6 +515,7 @@ class ProgressService: # Move to history del self._active_progress[progress_id] + self._last_broadcast_time.pop(progress_id, None) self._add_to_history(update) logger.error( @@ -548,6 +567,7 @@ class ProgressService: # Move to history del self._active_progress[progress_id] + self._last_broadcast_time.pop(progress_id, None) self._add_to_history(update) logger.info(