"""Performance metrics and caching for Alexa Media Player. Provides boot time tracking and intelligent data caching. """ from __future__ import annotations from dataclasses import dataclass, field import logging import time from typing import Any from homeassistant.core import HomeAssistant from .const import DOMAIN _LOGGER = logging.getLogger(__name__) @dataclass class BootMetrics: """Track boot performance metrics.""" start_time: float = field(default_factory=time.monotonic) stages: dict[str, float] = field(default_factory=dict) def record_stage(self, stage_name: str) -> None: """Record a boot stage completion.""" elapsed = time.monotonic() - self.start_time self.stages[stage_name] = elapsed _LOGGER.debug( "[BOOT METRICS] %s completed in %.3fs", stage_name, elapsed, ) def get_summary(self) -> dict[str, Any]: """Get boot metrics summary.""" total = time.monotonic() - self.start_time return { "total_time_seconds": round(total, 3), "stages": {k: round(v, 3) for k, v in self.stages.items()}, } class DataCache: """Simple TTL cache for API responses. Reduces redundant API calls during startup and normal operation. """ def __init__(self, ttl_seconds: float = 30.0, max_entries: int = 128) -> None: """Initialize cache with TTL. Args: ttl_seconds: Time-to-live for cached entries max_entries: Maximum number of entries before evicting oldest """ self._cache: dict[str, tuple[Any, float]] = {} self._ttl = ttl_seconds self._max_entries = max_entries self._hits = 0 self._misses = 0 def get(self, key: str) -> Any | None: """Get value from cache if not expired. Args: key: Cache key Returns: Cached value or None if expired/missing """ if key not in self._cache: self._misses += 1 return None value, timestamp = self._cache[key] if time.monotonic() - timestamp > self._ttl: # Expired del self._cache[key] self._misses += 1 return None self._hits += 1 return value def cache_set(self, key: str, value: Any) -> None: """Store value in cache. Note: Stores a direct reference (not a copy) for performance. Callers should treat cached values as read-only unless the caller created the cached object, is solely responsible for all mutations, and intentionally enriches it in-place (e.g., the device-dict refresh in async_update_data). Args: key: Cache key value: Value to cache """ if len(self._cache) >= self._max_entries and key not in self._cache: oldest_key = min(self._cache, key=lambda k: self._cache[k][1]) del self._cache[oldest_key] self._cache[key] = (value, time.monotonic()) def invalidate(self, key: str) -> None: """Remove key from cache.""" self._cache.pop(key, None) def clear(self) -> None: """Clear all cached entries.""" self._cache.clear() self._hits = 0 self._misses = 0 def get_stats(self) -> dict[str, int]: """Get cache statistics.""" total = self._hits + self._misses hit_rate = (self._hits / total * 100) if total > 0 else 0 return { "entries": len(self._cache), "hits": self._hits, "misses": self._misses, "hit_rate_percent": round(hit_rate, 1), } class AlexaMetrics: """Central metrics collector for Alexa Media Player.""" def __init__(self, hass: HomeAssistant) -> None: """Initialize metrics collector.""" self.hass = hass self.boot_metrics: BootMetrics | None = None self.api_cache = DataCache(ttl_seconds=30.0) self._api_calls: dict[str, tuple[int, float]] = {} # count, total_time def start_boot_tracking(self) -> None: """Start tracking boot performance.""" self.boot_metrics = BootMetrics() _LOGGER.debug("[BOOT METRICS] Started tracking") def record_boot_stage(self, stage_name: str) -> None: """Record a boot stage completion.""" if self.boot_metrics: self.boot_metrics.record_stage(stage_name) def record_api_call(self, endpoint: str, duration: float) -> None: """Record API call metrics. Args: endpoint: API endpoint name duration: Call duration in seconds """ if endpoint not in self._api_calls: self._api_calls[endpoint] = (0, 0.0) count, total = self._api_calls[endpoint] self._api_calls[endpoint] = (count + 1, total + duration) def get_api_stats(self) -> dict[str, Any]: """Get API call statistics.""" stats = {} for endpoint, (count, total) in self._api_calls.items(): stats[endpoint] = { "calls": count, "total_time": round(total, 3), "avg_time": round(total / count, 3) if count > 0 else 0, } return stats def get_full_report(self) -> dict[str, Any]: """Get complete metrics report.""" return { "boot": self.boot_metrics.get_summary() if self.boot_metrics else None, "cache": self.api_cache.get_stats(), "api_calls": self.get_api_stats(), } def get_metrics(hass: HomeAssistant) -> AlexaMetrics | None: """Get metrics instance from hass data. Args: hass: Home Assistant instance Returns: AlexaMetrics instance or None if not initialized """ if DOMAIN in hass.data and "metrics" in hass.data[DOMAIN]: return hass.data[DOMAIN]["metrics"] return None