1"""Apple Silicon hardware detection and telemetry (macOS, graceful degradation elsewhere)."""23from __future__ import annotations45import asyncio6import platform7import re8import shutil9import subprocess10import time11from dataclasses import asdict, dataclass, field12from pathlib import Path1314import psutil1516GB = 1024**3171819def _run(cmd: list[str], timeout: float = 5.0) -> str:20 try:21 return subprocess.run(cmd, capture_output=True, text=True, timeout=timeout).stdout22 except Exception:23 return ""242526def _sysctl(key: str) -> str:27 return _run(["sysctl", "-n", key]).strip()282930@dataclass31class Hardware:32 chip: str33 memory_gb: float34 memory_bytes: int35 cpu_cores: int36 performance_cores: int | None37 efficiency_cores: int | None38 gpu_cores: int | None39 os: str40 os_version: str41 hostname: str42 apple_silicon: bool43 disk_total_gb: float44 python: str4546 def to_dict(self) -> dict:47 return asdict(self)484950_HW_CACHE: Hardware | None = None515253def detect_hardware(models_dir: Path | None = None) -> Hardware:54 global _HW_CACHE55 if _HW_CACHE is not None:56 return _HW_CACHE57 is_mac = platform.system() == "Darwin"58 chip = _sysctl("machdep.cpu.brand_string") if is_mac else platform.processor() or "unknown"59 mem_bytes = int(_sysctl("hw.memsize") or psutil.virtual_memory().total) if is_mac else psutil.virtual_memory().total60 ncpu = int(_sysctl("hw.ncpu") or psutil.cpu_count() or 0) if is_mac else (psutil.cpu_count() or 0)61 perf = eff = None62 if is_mac:63 p0 = _sysctl("hw.perflevel0.physicalcpu")64 p1 = _sysctl("hw.perflevel1.physicalcpu")65 perf = int(p0) if p0.isdigit() else None66 eff = int(p1) if p1.isdigit() else None67 gpu_cores = None68 if is_mac:69 out = _run(["ioreg", "-r", "-c", "IOAccelerator", "-d", "1"])70 m = re.search(r'"gpu-core-count"\s*=\s*(\d+)', out)71 if m:72 gpu_cores = int(m.group(1))73 disk_path = models_dir if (models_dir and models_dir.exists()) else Path.home()74 du = shutil.disk_usage(disk_path)75 os_version = ""76 if is_mac:77 os_version = _run(["sw_vers", "-productVersion"]).strip()78 _HW_CACHE = Hardware(79 chip=chip or "unknown",80 memory_gb=round(mem_bytes / GB, 1),81 memory_bytes=mem_bytes,82 cpu_cores=ncpu,83 performance_cores=perf,84 efficiency_cores=eff,85 gpu_cores=gpu_cores,86 os="macOS" if is_mac else platform.system(),87 os_version=os_version or platform.release(),88 hostname=platform.node(),89 apple_silicon=is_mac and platform.machine() == "arm64",90 disk_total_gb=round(du.total / GB, 1),91 python=platform.python_version(),92 )93 return _HW_CACHE949596@dataclass97class Telemetry:98 ts: float99 mem_total_gb: float100 mem_used_gb: float101 mem_available_gb: float102 mem_wired_gb: float | None103 mem_compressed_gb: float | None104 mem_pressure_percent: int | None # 0 = none … 100 = critical (derived from free %)105 mem_pressure_level: str # normal | warning | critical106 swap_used_gb: float107 swap_total_gb: float108 cpu_percent: float109 cpu_per_core: list[float] = field(default_factory=list)110 load_avg: list[float] = field(default_factory=list)111 gpu_percent: float | None = None112 gpu_renderer_percent: float | None = None113 gpu_memory_gb: float | None = None114 thermal_state: str = "nominal"115 thermal_cpu_speed_limit: int | None = None116 disk_total_gb: float = 0117 disk_used_gb: float = 0118 disk_free_gb: float = 0119 uptime_seconds: float = 0120 process_rss_gb: float = 0121122 def to_dict(self) -> dict:123 return asdict(self)124125126def _read_gpu() -> tuple[float | None, float | None, float | None]:127 out = _run(["ioreg", "-r", "-c", "IOAccelerator", "-d", "1"], timeout=3)128 if not out:129 return None, None, None130 dev = re.search(r'"Device Utilization %"=(\d+)', out)131 rend = re.search(r'"Renderer Utilization %"=(\d+)', out)132 mem = re.search(r'"In use system memory"=(\d+)', out)133 return (134 float(dev.group(1)) if dev else None,135 float(rend.group(1)) if rend else None,136 round(int(mem.group(1)) / GB, 2) if mem else None,137 )138139140def _read_thermal() -> tuple[str, int | None]:141 out = _run(["pmset", "-g", "therm"], timeout=3)142 if not out:143 return "unknown", None144 m = re.search(r"CPU_Speed_Limit\s*=\s*(\d+)", out)145 limit = int(m.group(1)) if m else None146 if limit is not None and limit < 100:147 state = "throttled" if limit < 80 else "warm"148 elif "No thermal warning" in out or limit == 100:149 state = "nominal"150 else:151 state = "nominal"152 return state, limit153154155def _read_vm_stat() -> dict[str, int]:156 out = _run(["vm_stat"], timeout=3)157 vals: dict[str, int] = {}158 if not out:159 return vals160 m = re.search(r"page size of (\d+) bytes", out)161 page = int(m.group(1)) if m else 16384162 for line in out.splitlines()[1:]:163 if ":" not in line:164 continue165 k, v = line.split(":", 1)166 v = v.strip().rstrip(".")167 if v.isdigit():168 vals[k.strip()] = int(v) * page169 vals["_page"] = page170 return vals171172173def sample_telemetry(models_dir: Path | None = None) -> Telemetry:174 vm = psutil.virtual_memory()175 sw = psutil.swap_memory()176 is_mac = platform.system() == "Darwin"177 wired = compressed = None178 if is_mac:179 vs = _read_vm_stat()180 if vs:181 wired = round(vs.get("Pages wired down", 0) / GB, 2)182 compressed = round(vs.get("Pages occupied by compressor", 0) / GB, 2)183 gpu, gpu_r, gpu_mem = _read_gpu() if is_mac else (None, None, None)184 therm, limit = _read_thermal() if is_mac else ("unknown", None)185 disk_path = models_dir if (models_dir and models_dir.exists()) else Path.home()186 du = shutil.disk_usage(disk_path)187 avail_pct = vm.available / vm.total * 100188 if avail_pct > 20 and sw.used < 1 * GB:189 level = "normal"190 elif avail_pct > 8 and sw.used < 4 * GB:191 level = "warning"192 else:193 level = "critical"194 pressure = int(max(0.0, min(100.0, 100 - avail_pct)))195 proc = psutil.Process()196 try:197 load = list(psutil.getloadavg())198 except Exception:199 load = []200 return Telemetry(201 ts=time.time(),202 mem_total_gb=round(vm.total / GB, 2),203 mem_used_gb=round((vm.total - vm.available) / GB, 2),204 mem_available_gb=round(vm.available / GB, 2),205 mem_wired_gb=wired,206 mem_compressed_gb=compressed,207 mem_pressure_percent=pressure,208 mem_pressure_level=level,209 swap_used_gb=round(sw.used / GB, 2),210 swap_total_gb=round(sw.total / GB, 2),211 cpu_percent=psutil.cpu_percent(interval=None),212 cpu_per_core=psutil.cpu_percent(interval=None, percpu=True),213 load_avg=[round(x, 2) for x in load],214 gpu_percent=gpu,215 gpu_renderer_percent=gpu_r,216 gpu_memory_gb=gpu_mem,217 thermal_state=therm,218 thermal_cpu_speed_limit=limit,219 disk_total_gb=round(du.total / GB, 1),220 disk_used_gb=round(du.used / GB, 1),221 disk_free_gb=round(du.free / GB, 1),222 uptime_seconds=round(time.time() - psutil.boot_time(), 0),223 process_rss_gb=round(proc.memory_info().rss / GB, 3),224 )225226227async def sample_telemetry_async(models_dir: Path | None = None) -> Telemetry:228 return await asyncio.to_thread(sample_telemetry, models_dir)229230231def process_tree_rss_bytes(pid: int) -> int:232 """RSS of a process and its children (worker memory)."""233 try:234 p = psutil.Process(pid)235 total = p.memory_info().rss236 for c in p.children(recursive=True):237 try:238 total += c.memory_info().rss239 except psutil.Error:240 pass241 return total242 except psutil.Error:243 return 0244245246def process_footprint_bytes(pid: int) -> int:247 """macOS 'physical footprint' (closest to Activity Monitor 'Memory'). Falls back to RSS."""248 if platform.system() == "Darwin":249 out = _run(["footprint", "-p", str(pid)], timeout=3) if shutil.which("footprint") else ""250 m = re.search(r"phys_footprint:\s*([\d.]+)\s*([KMG]B)", out)251 if m:252 mult = {"KB": 1024, "MB": 1024**2, "GB": 1024**3}[m.group(2)]253 return int(float(m.group(1)) * mult)254 return process_tree_rss_bytes(pid)255