Files
status-app/test/e2e/scripts/utils/process_metrics.py

131 lines
4.2 KiB
Python

import logging
import os
import threading
from dataclasses import dataclass
import psutil
from scripts.utils import local_system
LOG = logging.getLogger(__name__)
@dataclass
class ProcessSampleStats:
avg_cpu_percent: float
avg_ram_mb: float
max_cpu_percent: float
max_ram_mb: float
sample_count: int
def resolve_monitored_pid(pid: int, app_path=None, app_data=None) -> int:
"""Use the real AUT process when pid points at an idle startaut wrapper."""
if app_path is None:
return pid
exe_name = os.path.basename(str(app_path))
try:
root = psutil.Process(pid)
if root.is_running():
if root.children(recursive=True):
return pid
if root.name() == exe_name:
return pid
except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
pass
if app_data is not None:
matched = local_system.get_pid_by_process_name(exe_name, str(app_data))
if matched:
return matched[-1]
candidate_pids = local_system.get_pid_by_process_name(exe_name) or []
if candidate_pids:
return candidate_pids[-1]
return pid
class ProcessMonitor:
"""Sample CPU % and RSS (MB) for an AUT process tree while a benchmark action runs."""
def __init__(self, pid: int, interval_sec: float = 0.1):
self._pid = pid
self._interval_sec = interval_sec
self._stop_event = threading.Event()
self._thread: threading.Thread | None = None
self._cpu_samples: list[float] = []
self._ram_samples: list[float] = []
def __enter__(self) -> 'ProcessMonitor':
self._stop_event.clear()
self._thread = threading.Thread(target=self._sample_loop, daemon=True)
self._thread.start()
return self
def __exit__(self, exc_type, exc_val, exc_tb):
self._stop_event.set()
if self._thread is not None:
self._thread.join(timeout=2)
def stats(self) -> ProcessSampleStats:
if not self._cpu_samples:
raise RuntimeError(
f'No valid CPU/RAM samples collected for AUT process tree pid={self._pid}'
)
return ProcessSampleStats(
avg_cpu_percent=sum(self._cpu_samples) / len(self._cpu_samples),
avg_ram_mb=sum(self._ram_samples) / len(self._ram_samples),
max_cpu_percent=max(self._cpu_samples),
max_ram_mb=max(self._ram_samples),
sample_count=len(self._cpu_samples),
)
def _iter_processes(self) -> list[psutil.Process]:
processes: list[psutil.Process] = []
try:
root = psutil.Process(self._pid)
processes = [root, *root.children(recursive=True)]
except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
LOG.debug('AUT process tree unavailable for pid=%s', self._pid)
return processes
def _sample_once(self) -> tuple[float, float] | None:
processes = self._iter_processes()
if not processes:
return None
for proc in processes:
try:
proc.cpu_percent(None)
except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
continue
if self._stop_event.wait(self._interval_sec):
return None
cpu_percent = 0.0
ram_total_bytes = 0
sampled_processes = 0
for proc in processes:
try:
cpu_percent += proc.cpu_percent(None)
ram_total_bytes += proc.memory_info().rss
sampled_processes += 1
except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
continue
if sampled_processes == 0 or ram_total_bytes == 0:
return None
cpu_count = psutil.cpu_count(logical=True) or 1
return min(cpu_percent / cpu_count, 100.0), ram_total_bytes / (1024 * 1024)
def _sample_loop(self) -> None:
while not self._stop_event.is_set():
sample = self._sample_once()
if sample is None:
self._stop_event.wait(self._interval_sec)
continue
cpu, ram_mb = sample
self._cpu_samples.append(cpu)
self._ram_samples.append(ram_mb)