import logging import time from collections.abc import Iterator from contextlib import contextmanager from dataclasses import dataclass, field from resonance_engine.dsp.enums import DSPClientName logger = logging.getLogger(__name__) def _avg(vals: list[int]) -> int: return round(sum(vals) / len(vals)) if vals else 0 def _p95(vals: list[int]) -> int: if not vals: return 0 return sorted(vals)[int(len(vals) * 0.95)] @dataclass class CollectTimings: token_ms: list[int] = field(default_factory=list) profile_ms: list[int] = field(default_factory=list) top_artists_ms: list[int] = field(default_factory=list) top_tracks_ms: list[int] = field(default_factory=list) recently_played_ms: list[int] = field(default_factory=list) playlists_ms: list[int] = field(default_factory=list) saved_albums_ms: list[int] = field(default_factory=list) saved_tracks_ms: list[int] = field(default_factory=list) followed_artists_ms: list[int] = field(default_factory=list) @contextmanager def measure(self, target: list[int]) -> Iterator[None]: """Time the block; on normal exit append elapsed ms to `target`.""" t0 = time.monotonic() yield target.append(round((time.monotonic() - t0) * 1000)) def log_summary( self, processed: int, errors: int, *, dsp_client_name: DSPClientName | None = None, ) -> None: extra: dict[str, object] = {"processed": processed, "errors": errors} if dsp_client_name is not None: extra["dsp_client_name"] = dsp_client_name logger.info( "Collect timing: %d ok, %d err | " "token avg=%dms p95=%dms (n=%d) | " "profile avg=%dms p95=%dms (n=%d) | " "top_artists avg=%dms p95=%dms (n=%d) | " "top_tracks avg=%dms p95=%dms (n=%d) | " "recently_played avg=%dms p95=%dms (n=%d) | " "playlists avg=%dms p95=%dms (n=%d) | " "saved_albums avg=%dms p95=%dms (n=%d) | " "saved_tracks avg=%dms p95=%dms (n=%d) | " "followed_artists avg=%dms p95=%dms (n=%d)", processed, errors, _avg(self.token_ms), _p95(self.token_ms), len(self.token_ms), _avg(self.profile_ms), _p95(self.profile_ms), len(self.profile_ms), _avg(self.top_artists_ms), _p95(self.top_artists_ms), len(self.top_artists_ms), _avg(self.top_tracks_ms), _p95(self.top_tracks_ms), len(self.top_tracks_ms), _avg(self.recently_played_ms), _p95(self.recently_played_ms), len(self.recently_played_ms), _avg(self.playlists_ms), _p95(self.playlists_ms), len(self.playlists_ms), _avg(self.saved_albums_ms), _p95(self.saved_albums_ms), len(self.saved_albums_ms), _avg(self.saved_tracks_ms), _p95(self.saved_tracks_ms), len(self.saved_tracks_ms), _avg(self.followed_artists_ms), _p95(self.followed_artists_ms), len(self.followed_artists_ms), extra=extra, )