Phase 1 (#11): perf instrumentation — harness/perf.py + scanvas_maker.py 5곳 wire
Some checks failed
CI / Ruff + Test (Py3.11 + Py3.13) (3.11) (push) Failing after 10s
CI / Ruff + Test (Py3.11 + Py3.13) (3.13) (push) Failing after 10s

신규 모듈 — harness/perf.py (54 LOC):
- perf_block(label) 컨텍스트 매니저 — 블록 단위 wall-clock + CPU 시간을 ms 단위로 측정.
- set_perf_log(callable) — 외부 sink 등록 (예: app.log 호출 시 GUI 패널에 표시).
- 출력 형식: [PERF] {label}: wall={NN}ms cpu={NN}ms ({CPU|I/O/Net}-bound).
- cpu/wall > 0.5 면 CPU-bound로 분류, 그 외 I/O/Net-bound (GIL 풀린 시간 비율).

Wire 5곳 — scanvas_maker.py:
- import 블록 (~line 58): from harness.perf import perf_block, set_perf_log
  + ImportError 시 contextlib.contextmanager 노옵 폴백 (모듈 누락 환경 대응).
- SCanvasApp.__init__ (~line 613): set_perf_log(self.log) 등록.
- TIN densify Phase C (line ~4430): with perf_block("TIN densify Phase C (10m→1m)").
- 위성 타일 다운로드 (line ~5384): with perf_block("위성 타일 다운로드+병합").
- 제어맵 캡처 x3 + composite (line ~5864): with perf_block("control map capture x3 + composite").

핫스팟 매핑 (PERFORMANCE_BASELINE.md): H1 (위성 타일), H3 (TIN densify), H12 (capture).

검증:
- python -m py_compile scanvas_maker.py harness/perf.py 통과.
- AST parse OK (39 top-level statements).
- ruff Green 정식 검증은 글로벌 ruff 설치 후 (uv pip install -e ".[dev]"; ruff check).

CHANGELOG.md 에 #11 perf instrumentation 항목 추가 (2026-05-08).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-08 16:17:54 +09:00
parent 8c6d7f0279
commit 59a3d1b599
3 changed files with 179 additions and 54 deletions

65
harness/perf.py Normal file
View File

@@ -0,0 +1,65 @@
"""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 logging
import time
from collections.abc import Callable
from contextlib import contextmanager
from typing import Optional
_log_callable: Optional[Callable[[str], 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:
try:
_log_callable(line)
except Exception: # noqa: BLE001 (로그 sink 실패가 측정 흐름을 끊으면 안 됨)
pass
@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)")