Files
s-canvas/harness/perf.py
HYUNJUNGLEE fc963007b7
All checks were successful
CI / Ruff + Test (Py3.11 + Py3.13) (3.11) (push) Successful in 23s
CI / Ruff + Test (Py3.11 + Py3.13) (3.13) (push) Successful in 23s
fix(ci): uv lock 생성 + setup-uv 옵션 정리 + UI 진행률 인디케이터 (#6, #4 부분)
CI uv setup 실패 (#6 후속):
- 원인: astral-sh/setup-uv@v3 의 enable-cache:true 가 **/uv.lock 미발견 시 fail.
  7개 push 모두 ::error::No file ... matched to [**/uv.lock] → 10-20초 만에 abort.
- 해결: uv.lock 생성 (438KB, 89 packages 해결) + cache-dependency-glob 명시.

연쇄 수정 (uv.lock 생성 과정에서 노출):
- pyproject.toml: scipy/pyproj/numpy 핀을 hard-pin == 에서 range > = 로 완화
  (base vs [py313] extras 충돌 해소). requires-python ">=3.9" → ">=3.11"
  (pyproj>=3.7 wheel 가용 환경과 일치). [tool.uv] no-progress = false 제거 (deprecated).
- .gitea/workflows/ci.yml: 별도 Setup Python step 제거 (uv venv가 자동 fetch),
  install step 단순화 (matrix 분기 EXTRAS 변수), 모든 run: 에 shell: bash 명시.

UI 진행률 인디케이터 (#4 부분):
- self.progress_bar (CTkProgressBar mode=indeterminate, MC overlap orange #FF5F00)
  status_bar 우측에 hidden 배치. start_progress(label)/stop_progress() 메서드 추가.
- self.textbox height 120 → 80 (인라인 로그 비중 축소, 백엔드 파일이 주 기록처).

ruff cleanup (harness/perf.py):
- Optional[Callable[...]] → Callable[...] | None (UP045).
- try/except/pass → contextlib.suppress (SIM105).
- 미사용 # noqa: BLE001 제거 (RUF100).

검증: uv lock 성공, ruff check All checks passed, py_compile + AST OK.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 17:13:33 +09:00

65 lines
2.1 KiB
Python

"""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)")