Fix event loop blocking from perf endpoint and profile detection

- Change /api/v1/system/performance from async def to def so FastAPI
  runs the blocking psutil + NVML GPU queries in a thread pool instead
  of freezing the event loop (polled every 2s by dashboard)
- Batch profile engine's 3 separate run_in_executor detection calls
  into a single _detect_all_sync() call, reducing event loop wake-ups

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-21 02:06:59 +03:00
parent 55a9662234
commit b14da85f3b
2 changed files with 33 additions and 19 deletions

View File

@@ -140,8 +140,13 @@ async def get_running_processes(_: AuthRequired):
response_model=PerformanceResponse, response_model=PerformanceResponse,
tags=["Config"], tags=["Config"],
) )
async def get_system_performance(_: AuthRequired): def get_system_performance(_: AuthRequired):
"""Get current system performance metrics (CPU, RAM, GPU).""" """Get current system performance metrics (CPU, RAM, GPU).
Uses sync ``def`` so FastAPI runs it in a thread pool — the psutil
and NVML calls are blocking and would stall the event loop if run
in an ``async def`` handler.
"""
mem = psutil.virtual_memory() mem = psutil.virtual_memory()
gpu = None gpu = None

View File

@@ -68,6 +68,23 @@ class ProfileEngine:
async with self._eval_lock: async with self._eval_lock:
await self._evaluate_all_locked() await self._evaluate_all_locked()
def _detect_all_sync(
self, needs_running: bool, needs_topmost: bool, needs_fullscreen: bool,
) -> tuple:
"""Run all platform detection in a single thread call.
Batching the three detection calls into one executor submission reduces
event-loop wake-ups from 3 to 1, minimising asyncio.sleep() jitter in
latency-sensitive processing loops.
"""
running_procs = self._detector._get_running_processes_sync() if needs_running else set()
if needs_topmost:
topmost_proc, topmost_fullscreen = self._detector._get_topmost_process_sync()
else:
topmost_proc, topmost_fullscreen = None, False
fullscreen_procs = self._detector._get_fullscreen_processes_sync() if needs_fullscreen else set()
return running_procs, topmost_proc, topmost_fullscreen, fullscreen_procs
async def _evaluate_all_locked(self) -> None: async def _evaluate_all_locked(self) -> None:
profiles = self._store.get_all_profiles() profiles = self._store.get_all_profiles()
if not profiles: if not profiles:
@@ -84,26 +101,18 @@ class ProfileEngine:
mt = getattr(c, "match_type", "running") mt = getattr(c, "match_type", "running")
match_types_used.add(mt) match_types_used.add(mt)
# WMI process enumeration (~3s) — only needed for "running" match type
needs_running = "running" in match_types_used needs_running = "running" in match_types_used
running_procs = (
await self._detector.get_running_processes()
if needs_running else set()
)
# Foreground window check (<1ms) — needed for "topmost" and "topmost_fullscreen"
needs_topmost = bool(match_types_used & {"topmost", "topmost_fullscreen"}) needs_topmost = bool(match_types_used & {"topmost", "topmost_fullscreen"})
if needs_topmost:
topmost_proc, topmost_fullscreen = await self._detector.get_topmost_process()
else:
topmost_proc = None
topmost_fullscreen = False
# Fullscreen window enumeration (<1ms) — only needed for "fullscreen"
needs_fullscreen = "fullscreen" in match_types_used needs_fullscreen = "fullscreen" in match_types_used
fullscreen_procs = (
await self._detector.get_fullscreen_processes() # Single executor call for all platform detection (avoids 3 separate
if needs_fullscreen else set() # event-loop roundtrips that can jitter processing-loop timing)
loop = asyncio.get_event_loop()
running_procs, topmost_proc, topmost_fullscreen, fullscreen_procs = (
await loop.run_in_executor(
None, self._detect_all_sync,
needs_running, needs_topmost, needs_fullscreen,
)
) )
active_profile_ids = set() active_profile_ids = set()