diff --git a/CHANGELOG.md b/CHANGELOG.md index 17cf85e..c365d47 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,51 @@ --- +## 2026-05-08 + +### [merge] Gitea s-canvas 원격(raw upload, 184185c)과 로컬 lint+Phase 0 history 통합 + +- **상황**: 원격 `https://gitea.hmac.kr/HYUNJUNGLEE/s-canvas.git` 에 사용자가 site에서 raw upload 한 1회 commit (`184185c`)이 존재. 로컬은 `53d8b53` → `b9342f6` (import + iter1~7 lint cleanup) → `e9cc6bf` (Phase 0 of expert feedback) history. **공통 조상 없음 (unrelated histories)**. +- **분석**: 원격에만 있는 파일 0개 (원격은 로컬의 부분집합). 양쪽 다 있고 내용 다른 파일 29개 (raw upload vs lint-applied 버전). 로컬에만 있는 파일 80+ (Phase 0 산출물 + workspace/`_unused/` 등). +- **전략**: `git merge --allow-unrelated-histories -X ours`. 충돌 시 로컬 우선으로 lint cleanup 보존. +- **결과**: 머지 commit `8c6d7f0`. 자동 머지된 31파일 중 README.md 만 실질 변경 (로컬 0줄 → 원격 404줄 README 흡수 — 빈 파일 vs 내용있음은 `-X ours` 적용 외 단순 합병). 나머지 source/config 28개는 로컬 lint 버전 유지. +- **푸시**: 머지 후 fast-forward push 가능. 원격 history 손실 없이 통합. + +### [feat] #11 perf instrumentation — `harness/perf.py` 신규 + `scanvas_maker.py` 5곳 wire + +- **사용자 피드백 #11**: "로딩이 오래 걸리는 부분(위성지도 결합·구조물 빌드 시 등)은 CPU 이용률이 대폭 증가하는 프로세스를 ms 단위로 추적해서 원인을 규명하고 최적화하는 조치 필요". + +#### 신규 모듈 — `harness/perf.py` (54 LOC) +- `perf_block(label)` — 컨텍스트 매니저. `with perf_block("XYZ tiles"): ...` 형태로 블록 실행 시간(wall + CPU)을 ms 단위로 측정. +- `set_perf_log(callable)` — 외부 sink 등록 (예: `set_perf_log(app.log)` 시 GUI 로그 패널에도 표시). +- 출력 형식: `[PERF] {label}: wall={NNN}ms cpu={NNN}ms ({CPU|I/O/Net}-bound)`. `cpu/wall > 0.5` 면 CPU-bound로 분류. + +#### Setup — `scanvas_maker.py` 2곳 +1. **import 블록 (~line 58)**: `from harness.perf import perf_block, set_perf_log` + ImportError 시 `@contextlib.contextmanager` 노옵 폴백 → `harness/` 모듈 누락 환경에서도 안전. +2. **`SCanvasApp.__init__` (~line 613)**: `set_perf_log(self.log)` 등록 — perf 측정 라인이 GUI 텍스트박스에도 표시됨. + +#### Hotspot wraps — `scanvas_maker.py` 3곳 (PERFORMANCE_BASELINE.md 매핑) +1. **TIN densify Phase C (line ~4430) → H3**: `with perf_block("TIN densify Phase C (10m→1m)")` 로 10단계 점진 격자 루프 감쌈. +2. **위성 타일 다운로드 (line ~5384) → H1**: `with perf_block("위성 타일 다운로드+병합")` 로 `_download_xyz_tiles()` 감쌈 — 사용자 피드백 #11이 명시한 "위성지도 결합" 핫스팟. +3. **제어맵 캡처 파이프라인 (line ~5864) → H12**: `with perf_block("control map capture x3 + composite")` 로 textured + depth + lineart 3-stage 캡처 + composite 감쌈. + +#### 출력 예 (실제 측정 시) +``` +[PERF] 위성 타일 다운로드+병합: wall=12340.5ms cpu=860.3ms (I/O/Net-bound) +[PERF] TIN densify Phase C (10m→1m): wall=2150.7ms cpu=2080.4ms (CPU-bound) +[PERF] control map capture x3 + composite: wall=4520.1ms cpu=3760.8ms (CPU-bound) +``` + +#### 검증 +- `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`). + +#### 다음 라운드 (#11 추가) +- 사용자 실제 도면으로 [PERF] 출력 1회 측정 → PERFORMANCE_BASELINE.md "측정 후 비교 표" 채움. +- 측정 결과 기반 추가 hotspot wrap (H7·H9·H13·H18 등) + 최적화 (numpy 벡터화 / 스레드 풀 / GIL 해제). + +--- + ## 2026-04-28 ### [fix] 화면비 버튼이 텍스트만 떠서 안 보이는 문제 — vtkButtonWidget 로 교체 diff --git a/harness/perf.py b/harness/perf.py new file mode 100644 index 0000000..864d16c --- /dev/null +++ b/harness/perf.py @@ -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)") diff --git a/scanvas_maker.py b/scanvas_maker.py index 7d841db..10e72a7 100644 --- a/scanvas_maker.py +++ b/scanvas_maker.py @@ -55,6 +55,16 @@ try: except ImportError: HARNESS_AVAILABLE = False +# Perf instrumentation (#11) — ms 단위 wall/CPU 측정. import 실패 시 no-op 폴백. +try: + from harness.perf import perf_block, set_perf_log +except ImportError: + @contextlib.contextmanager + def perf_block(label): # type: ignore[no-redef] + yield + def set_perf_log(fn): # type: ignore[no-redef] + pass + # 구조물 상세도면 치수 파서 try: from detail_parser import DetailParser, dimensions_to_structure_params @@ -610,6 +620,10 @@ class SCanvasApp(ctk.CTk): self.log("S-CANVAS Generative Design Engine 구동 완료.") + # Perf 측정 라인을 GUI 로그에도 함께 표시 (#11). harness/perf.py 폴백 import 시 + # set_perf_log는 no-op이라 실패해도 안전. + set_perf_log(self.log) + def create_sidebar_button(self, text, command, row, **kwargs): btn = ctk.CTkButton( self.sidebar_frame, text=text, command=command, height=34, **kwargs) @@ -4427,40 +4441,41 @@ class SCanvasApp(ctk.CTk): from matplotlib.path import Path as _MplPath total_phase_c = 0 steps_log = [] - for _step in (10.0, 9.0, 8.0, 7.0, 6.0, 5.0, 4.0, 3.0, 2.0, 1.0): - try: - hull_c = _ConvexHull(pts[:, :2]) - except Exception: - break - hull_poly_xy = pts[hull_c.vertices, :2] - hull_path_c = _MplPath(hull_poly_xy, closed=True) - gx = np.arange(x0_abs, x1_abs + _step * 0.5, _step) - gy = np.arange(y0_abs, y1_abs + _step * 0.5, _step) - ggx, ggy = np.meshgrid(gx, gy) - grid_xy_c = np.column_stack([ggx.ravel(), ggy.ravel()]) - inside_bbox = ( - (grid_xy_c[:, 0] >= x0_abs - 1e-6) - & (grid_xy_c[:, 0] <= x1_abs + 1e-6) - & (grid_xy_c[:, 1] >= y0_abs - 1e-6) - & (grid_xy_c[:, 1] <= y1_abs + 1e-6) - ) - grid_xy_c = grid_xy_c[inside_bbox] - if len(grid_xy_c) == 0: - continue - inside_hull = hull_path_c.contains_points(grid_xy_c) - outside_hull_xy = grid_xy_c[~inside_hull] - if len(outside_hull_xy) == 0: - continue - # 기존 점과 너무 가까운 격자점(≤ step×0.4) 제외 — 중복 방지 - tree_ex = _cKDTreeC(pts[:, :2]) - d_ex, _ = tree_ex.query(outside_hull_xy, k=1) - new_only_xy = outside_hull_xy[d_ex > _step * 0.4] - if len(new_only_xy) == 0: - continue - new_z_c = _dem_sample_minus_offset(new_only_xy) - pts = np.vstack([pts, np.column_stack([new_only_xy, new_z_c])]) - total_phase_c += len(new_only_xy) - steps_log.append(f"{_step:.0f}m:{len(new_only_xy)}") + with perf_block("TIN densify Phase C (10m→1m)"): + for _step in (10.0, 9.0, 8.0, 7.0, 6.0, 5.0, 4.0, 3.0, 2.0, 1.0): + try: + hull_c = _ConvexHull(pts[:, :2]) + except Exception: + break + hull_poly_xy = pts[hull_c.vertices, :2] + hull_path_c = _MplPath(hull_poly_xy, closed=True) + gx = np.arange(x0_abs, x1_abs + _step * 0.5, _step) + gy = np.arange(y0_abs, y1_abs + _step * 0.5, _step) + ggx, ggy = np.meshgrid(gx, gy) + grid_xy_c = np.column_stack([ggx.ravel(), ggy.ravel()]) + inside_bbox = ( + (grid_xy_c[:, 0] >= x0_abs - 1e-6) + & (grid_xy_c[:, 0] <= x1_abs + 1e-6) + & (grid_xy_c[:, 1] >= y0_abs - 1e-6) + & (grid_xy_c[:, 1] <= y1_abs + 1e-6) + ) + grid_xy_c = grid_xy_c[inside_bbox] + if len(grid_xy_c) == 0: + continue + inside_hull = hull_path_c.contains_points(grid_xy_c) + outside_hull_xy = grid_xy_c[~inside_hull] + if len(outside_hull_xy) == 0: + continue + # 기존 점과 너무 가까운 격자점(≤ step×0.4) 제외 — 중복 방지 + tree_ex = _cKDTreeC(pts[:, :2]) + d_ex, _ = tree_ex.query(outside_hull_xy, k=1) + new_only_xy = outside_hull_xy[d_ex > _step * 0.4] + if len(new_only_xy) == 0: + continue + new_z_c = _dem_sample_minus_offset(new_only_xy) + pts = np.vstack([pts, np.column_stack([new_only_xy, new_z_c])]) + total_phase_c += len(new_only_xy) + steps_log.append(f"{_step:.0f}m:{len(new_only_xy)}") if total_phase_c > 0: self.log( f" [Phase C] hull 바깥 × bbox 내부 점진 densify: " @@ -5380,7 +5395,8 @@ class SCanvasApp(ctk.CTk): if not vk: raise ValueError("Vworld 타일 사용 시 API Key가 필요합니다. 사이드바에 입력해주세요.") tile_url_template = tile_url_template.replace("{vworld_key}", vk) - satellite_img = self._download_xyz_tiles(tile_url_template, min_lat, min_lon, max_lat, max_lon) + with perf_block("위성 타일 다운로드+병합"): + satellite_img = self._download_xyz_tiles(tile_url_template, min_lat, min_lon, max_lat, max_lon) img_path = "satellite_temp.png" satellite_img.save(img_path) @@ -5861,28 +5877,29 @@ class SCanvasApp(ctk.CTk): ar_label = f"비율 {ar[0]}:{ar[1]}" if ar else f"뷰어 창 {self._saved_window_size or '미저장'}" self.log(f" 캡처 해상도: {out_w}x{out_h} ({ar_label} 기반)") - # 1. 위성 텍스처 3D 캡처 - self.capture_image = self._capture_from_camera(out_w, out_h, textured=True) - self.capture_image.save("capture_textured.png") - self.log(f" 캡처 완료: {self.capture_image.size}") + with perf_block("control map capture x3 + composite"): + # 1. 위성 텍스처 3D 캡처 + self.capture_image = self._capture_from_camera(out_w, out_h, textured=True) + self.capture_image.save("capture_textured.png") + self.log(f" 캡처 완료: {self.capture_image.size}") - # 2. Depth Map - self.log(" Depth Map 추출 중...") - self.depth_map = self._capture_depth_from_camera(out_w, out_h) - self.depth_map.save("depth_map.png") - self.log(" Depth Map 완료.") + # 2. Depth Map + self.log(" Depth Map 추출 중...") + self.depth_map = self._capture_depth_from_camera(out_w, out_h) + self.depth_map.save("depth_map.png") + self.log(" Depth Map 완료.") - # 3. Lineart Map - self.log(" Lineart Map 추출 중...") - self.lineart_map = self._capture_lineart_from_camera(out_w, out_h) - self.lineart_map.save("lineart_map.png") - self.log(" Lineart Map 완료.") + # 3. Lineart Map + self.log(" Lineart Map 추출 중...") + self.lineart_map = self._capture_lineart_from_camera(out_w, out_h) + self.lineart_map.save("lineart_map.png") + self.log(" Lineart Map 완료.") - # 4. 가이드 이미지 합성 - self.guide_image = self._compose_guide_image( - self.capture_image, self.depth_map, self.lineart_map - ) - self.guide_image.save("guide_composite.png") + # 4. 가이드 이미지 합성 + self.guide_image = self._compose_guide_image( + self.capture_image, self.depth_map, self.lineart_map + ) + self.guide_image.save("guide_composite.png") self.set_status("제어맵 추출 완료", "#2ECC71") self.btn_step4.configure(fg_color=["#3a7ebf", "#1f538d"])