"""S-CANVAS perf instrumentation — ms 단위 wall/CPU 시간 측정. 피드백 #11: "로딩이 오래 걸리는 부분(위성지도 결합·구조물 빌드 시 등)은 CPU 이용률이 대폭 증가하는 프로세스를 ms 단위로 추적해서 원인을 규명하고 최적화하는 조치 필요" 사용: from harness.perf import perf_block, set_perf_log set_perf_log(app.log) # GUI 로그에 함께 기록 (옵션) with perf_block("XYZ tiles 5x5"): download_tiles(...) 출력: [PERF] XYZ tiles 5x5: wall=2540.3ms cpu=120.1ms (I/O/Net-bound) 판별: cpu/wall > 0.5 → CPU-bound, 그 외 → I/O/Net-bound (GIL 풀린 시간 비율). """ from __future__ import annotations import contextlib import logging import time from collections.abc import Callable from contextlib import contextmanager _log_callable: Callable[[str], None] | None = None _logger = logging.getLogger("scanvas.perf") def set_perf_log(fn: Callable[[str], None] | None) -> None: """app.log 등 외부 sink로 perf 라인 라우팅. None이면 logger 만.""" global _log_callable # noqa: PLW0603 (module-level singleton) _log_callable = fn def _emit(line: str) -> None: _logger.info(line) if _log_callable is not None: # 로그 sink 실패가 측정 흐름을 끊으면 안 됨 — 폭넓게 suppress. with contextlib.suppress(Exception): _log_callable(line) @contextmanager def perf_block(label: str): """블록 단위 wall-clock + CPU 시간을 한 줄로 출력. Args: label: 출력 prefix (예: "TIN densify Phase C", "capture x3"). 측정 단위는 ms. CPU-bound vs I/O/Net-bound를 cpu/wall 비율로 거칠게 분류. """ t_wall = time.perf_counter() t_cpu = time.process_time() try: yield finally: dt_wall = (time.perf_counter() - t_wall) * 1000 dt_cpu = (time.process_time() - t_cpu) * 1000 ratio = dt_cpu / dt_wall if dt_wall > 1e-3 else 0.0 kind = "CPU" if ratio > 0.5 else "I/O/Net" _emit(f"[PERF] {label}: wall={dt_wall:.1f}ms cpu={dt_cpu:.1f}ms ({kind}-bound)")