from __future__ import annotations import asyncio import time from collections import deque from typing import TYPE_CHECKING from yuxi.utils.logging_config import logger if TYPE_CHECKING: from yuxi.channels.services.runtime_state import RuntimeState _MAX_RESPONSE_TIME_SAMPLES = 100 class StatsCollector: def __init__(self, state: RuntimeState): self._state = state self._response_times: deque[float] = deque(maxlen=_MAX_RESPONSE_TIME_SAMPLES) self._request_count_local: int = 0 self._error_count_local: int = 0 self._last_collect_at = time.monotonic() def record_request(self) -> None: self._state.request_count += 1 self._request_count_local += 1 def record_error(self) -> None: self._state.error_count += 1 self._error_count_local += 1 def record_response_time(self, ms: float) -> None: self._response_times.append(ms) async def run(self, interval: float = 60) -> None: logger.info("StatsCollector started") while True: await asyncio.sleep(interval) try: self._collect() except asyncio.CancelledError: break except Exception: logger.exception("StatsCollector error") def _collect(self) -> None: now = time.monotonic() elapsed = now - self._last_collect_at self._last_collect_at = now rps = self._request_count_local / elapsed if elapsed > 0 else 0 eps = self._error_count_local / elapsed if elapsed > 0 else 0 avg_rt = sum(self._response_times) / len(self._response_times) if self._response_times else 0 p95_rt = 0.0 if len(self._response_times) >= 20: sorted_times = sorted(self._response_times) p95_idx = int(len(sorted_times) * 0.95) p95_rt = sorted_times[p95_idx] if p95_idx < len(sorted_times) else sorted_times[-1] error_rate = self._error_count_local / self._request_count_local if self._request_count_local else 0 logger.info( f"Stats: channels={self._state.active_channels}, " f"rps={rps:.1f}, eps={eps:.1f}, errors={self._error_count_local}, " f"error_rate={error_rate:.2%}, " f"avg_rt={avg_rt:.0f}ms, p95_rt={p95_rt:.0f}ms" ) self._request_count_local = 0 self._error_count_local = 0 def get_summary(self) -> dict: avg_rt = sum(self._response_times) / len(self._response_times) if self._response_times else 0 return { "active_channels": self._state.active_channels, "total_requests": self._state.request_count, "total_errors": self._state.error_count, "avg_response_time_ms": round(avg_rt, 1), "phase": self._state.phase, }