132 lines
3.8 KiB
Python
132 lines
3.8 KiB
Python
import logging
|
|
import math
|
|
import os
|
|
import time
|
|
from contextlib import asynccontextmanager
|
|
from dataclasses import dataclass
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
RECENT_PHASE_CAPACITY = 40
|
|
|
|
|
|
@dataclass
|
|
class PhaseSnapshot:
|
|
name: str
|
|
started_at: float
|
|
ended_at: float
|
|
duration_ms: float
|
|
cpu_user_ms: float
|
|
cpu_system_ms: float
|
|
cpu_total_ms: float
|
|
cpu_core_ratio: float
|
|
details: dict[str, str | int | float | bool] | None = None
|
|
|
|
|
|
_active_phase_stack: list[dict] = []
|
|
_recent_phases: list[PhaseSnapshot] = []
|
|
|
|
|
|
def _round_metric(value: float, digits: int = 1) -> float:
|
|
if not math.isfinite(value):
|
|
return 0.0
|
|
factor = 10**digits
|
|
return round(value * factor) / factor
|
|
|
|
|
|
def _push_recent(snapshot: PhaseSnapshot) -> None:
|
|
_recent_phases.append(snapshot)
|
|
if len(_recent_phases) > RECENT_PHASE_CAPACITY:
|
|
del _recent_phases[: len(_recent_phases) - RECENT_PHASE_CAPACITY]
|
|
|
|
|
|
def _record_phase(snapshot: PhaseSnapshot) -> None:
|
|
_push_recent(snapshot)
|
|
try:
|
|
from yuxi.channel.monitoring.diagnostic_stability import record_stability_event
|
|
|
|
record_stability_event(
|
|
"diagnostic.phase.completed",
|
|
name=snapshot.name,
|
|
started_at=snapshot.started_at,
|
|
ended_at=snapshot.ended_at,
|
|
duration_ms=snapshot.duration_ms,
|
|
cpu_user_ms=snapshot.cpu_user_ms,
|
|
cpu_system_ms=snapshot.cpu_system_ms,
|
|
cpu_total_ms=snapshot.cpu_total_ms,
|
|
cpu_core_ratio=snapshot.cpu_core_ratio,
|
|
)
|
|
except Exception:
|
|
pass
|
|
|
|
|
|
def get_current_phase() -> str | None:
|
|
if not _active_phase_stack:
|
|
return None
|
|
return _active_phase_stack[-1]["name"]
|
|
|
|
|
|
def get_recent_phases(limit: int = 8) -> list[PhaseSnapshot]:
|
|
return list(_recent_phases[-max(0, limit) :])
|
|
|
|
|
|
def record_diagnostic_phase(snapshot: PhaseSnapshot) -> None:
|
|
_record_phase(snapshot)
|
|
|
|
|
|
def reset_diagnostic_phases_for_test() -> None:
|
|
_active_phase_stack.clear()
|
|
_recent_phases.clear()
|
|
|
|
|
|
@asynccontextmanager
|
|
async def diagnostic_phase(name: str, details: dict[str, str | int | float | bool] | None = None):
|
|
cpu_started = os.times()
|
|
active: dict = {
|
|
"name": name,
|
|
"started_at": time.time(),
|
|
"started_wall": time.perf_counter(),
|
|
"cpu_started_user": cpu_started.user,
|
|
"cpu_started_system": cpu_started.system,
|
|
"details": details,
|
|
}
|
|
_active_phase_stack.append(active)
|
|
try:
|
|
yield
|
|
finally:
|
|
ended_at = time.time()
|
|
duration_ms = _round_metric((time.perf_counter() - active["started_wall"]) * 1000)
|
|
|
|
cpu_ended = os.times()
|
|
cpu_user_ms = _round_metric((cpu_ended.user - active["cpu_started_user"]) * 1000)
|
|
cpu_system_ms = _round_metric((cpu_ended.system - active["cpu_started_system"]) * 1000)
|
|
cpu_total_ms = _round_metric(cpu_user_ms + cpu_system_ms)
|
|
|
|
cpu_core_ratio = _round_metric(cpu_total_ms / max(1.0, duration_ms), 3)
|
|
|
|
_active_phase_stack[:] = [e for e in _active_phase_stack if e is not active]
|
|
|
|
snapshot = PhaseSnapshot(
|
|
name=name,
|
|
started_at=active["started_at"],
|
|
ended_at=ended_at,
|
|
duration_ms=duration_ms,
|
|
cpu_user_ms=cpu_user_ms,
|
|
cpu_system_ms=cpu_system_ms,
|
|
cpu_total_ms=cpu_total_ms,
|
|
cpu_core_ratio=cpu_core_ratio,
|
|
details=active["details"],
|
|
)
|
|
_record_phase(snapshot)
|
|
|
|
if cpu_core_ratio > 0.9:
|
|
logger.debug(
|
|
"DiagnosticPhase '%s': %sms, cpu_user=%.1fms, cpu_system=%.1fms, cpu_total=%.1fms, ratio=%.2f",
|
|
name,
|
|
duration_ms,
|
|
cpu_user_ms,
|
|
cpu_system_ms,
|
|
cpu_total_ms,
|
|
cpu_core_ratio,
|
|
)
|