From 4da04929c4de1f5fdf635c850fc731e27c55c4ad Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 7 Sep 2026 03:20:39 +0900 Subject: [PATCH] =?UTF-8?q?perf(=EA=B3=84=EC=B8=A1):=20=EC=9E=AC=ED=99=95?= =?UTF-8?q?=EC=A0=95=20=EC=B2=B4=EC=9D=B8=20=EB=B3=91=EB=AA=A9=20=EC=B6=94?= =?UTF-8?q?=EC=A0=81=EC=9A=A9=20=EB=8B=A8=EA=B3=84=20=EB=A7=88=ED=81=AC=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PLAN 0-10. 8000 에서 1회 실측 결과 재확정 체인 292.9s 중 배수유역 분석이 285.0s(97%)로 나와, 그 안쪽을 가르기 위한 마크만 추가. 로직 불변. - B06 확정: 조회 / 기본설계 계산 / 확정 저장 / 서버 재계산 / 측구 역반영 - 서버 재계산 내부: 상세+DB 병렬 조회 / 포장·세월교 보정 / Node 번들 / 정본 저장 - 배수유역 분석 내부: _prepare(도엽 읽기·좌표변환) / preview_stages / 응답 만들기·저장 - preview_stages: 1차 영역 / 격자 확장·흐름 판정 / 흐름 강도 / 외곽선·기본 관·화살표 `_log_steps` 는 체인 모듈 것을 그대로 씀(지연 임포트로 순환 회피). Co-Authored-By: Claude Opus 5 (1M context) --- .../B04_PreProcess_Engine_Watershed_Analyze.py | 8 ++++++++ B04_PreProcess/B04_PreProcess_Router_Watershed.py | 8 ++++++++ B06_Section/B06_Section_Router_Confirm.py | 11 +++++++++++ B06_Section/B06_Section_Server_Calc_Prebuild.py | 9 +++++++++ 4 files changed, 36 insertions(+) diff --git a/B04_PreProcess/B04_PreProcess_Engine_Watershed_Analyze.py b/B04_PreProcess/B04_PreProcess_Engine_Watershed_Analyze.py index 6534d92d..2fadf6dd 100644 --- a/B04_PreProcess/B04_PreProcess_Engine_Watershed_Analyze.py +++ b/B04_PreProcess/B04_PreProcess_Engine_Watershed_Analyze.py @@ -166,8 +166,12 @@ def preview_stages( """ if len(vertices) < 2: return None + from B03_FileInput.B03_FileInput_Service_Chain import _log_steps + + marks = [("시작", time.perf_counter())] route_line = LineString([(vertex.x, vertex.y) for vertex in vertices]) region = resolve_primary_region(vertices, route_line, contour_features, stream_features) + marks.append(("1차 영역(resolve_primary_region)", time.perf_counter())) if region is None: return None @@ -185,6 +189,7 @@ def preview_stages( logger.warning("배수유역: 등고선 하강 방향을 세우지 못해 흐름 판정을 건너뜁니다.") return StagePreview(region=region) + marks.append(("격자 확장·흐름 판정(expand_by_red_boundary)", time.perf_counter())) analysis = expansion.analysis spec = analysis.spec red = analysis.flow.reaches_road & analysis.flow.analyzed @@ -194,6 +199,7 @@ def preview_stages( # 최종적으로 어느 도로 셀로 들어가는지를 봐야 하므로 도로만 흡수점으로 두고 다시 따라간다. routing = trace_flow(analysis.terrain, analysis.road) if analysis.road.count else None strength_curve = _preview_strength(analysis, routing, red, route_line.length) + marks.append(("흐름 강도(trace_flow)", time.perf_counter())) # ⑦ 2차 전체 배수유역 외곽선 = 적색 셀 전체의 외곽. boundary = outer_boundary(spec, red.reshape(spec.n_rows, spec.n_cols)) @@ -209,6 +215,8 @@ def preview_stages( # B05에 얹을 평균 흐름 화살표 — 셀 화살표는 도면 배율에서 안 보인다. flow_arrows = build_flow_arrows(analysis, analysis.flow) + marks.append(("외곽선·기본 관·화살표", time.perf_counter())) + _log_steps("배수유역 preview_stages", marks) logger.info( "배수유역: 단계 분석 %.1fs (확장 %d회, 최종 셀 %d개) — " diff --git a/B04_PreProcess/B04_PreProcess_Router_Watershed.py b/B04_PreProcess/B04_PreProcess_Router_Watershed.py index aec83a12..9beacc38 100644 --- a/B04_PreProcess/B04_PreProcess_Router_Watershed.py +++ b/B04_PreProcess/B04_PreProcess_Router_Watershed.py @@ -11,6 +11,7 @@ import asyncio import json import logging import math +import time from pathlib import Path from typing import Any from uuid import UUID @@ -389,9 +390,13 @@ async def get_primary_region( logger.info("배수유역: 저장된 분석 결과를 그대로 돌려줍니다 (%s).", stored_path) return {**saved, "from_cache": True} + from B03_FileInput.B03_FileInput_Service_Chain import _log_steps + + marks = [("시작", time.perf_counter())] prepared = await _prepare(project_id) if isinstance(prepared, JSONResponse): return prepared + marks.append(("_prepare(도엽 읽기·좌표변환)", time.perf_counter())) preview = await asyncio.to_thread( preview_stages, prepared["vertices"], @@ -403,6 +408,7 @@ async def get_primary_region( status_code=400, content={"status": "error", "message": "1차 배수유역을 정할 등고선이 없습니다."}, ) + marks.append(("preview_stages(유역 산정)", time.perf_counter())) region = preview.region to_lonlat = prepared["to_lonlat"] # 확장을 거치면 격자·해석 영역이 1차 영역보다 커진다. 최종본을 써야 화면과 어긋나지 않는다. @@ -516,5 +522,7 @@ async def get_primary_region( _write_stage_arrays(prepared["stored_path"], preview, domain, spec) _write_road_routing(prepared["stored_path"], preview, spec, prepared["route_line"], to_lonlat) # 응답 자체를 캐시로 남긴다 — 다음 조회는 배열을 재조립하지 않고 이 파일을 그대로 준다. + marks.append(("응답 만들기·저장", time.perf_counter())) + _log_steps("배수유역 분석 내부", marks) _save_response(prepared["stored_path"], payload) return payload diff --git a/B06_Section/B06_Section_Router_Confirm.py b/B06_Section/B06_Section_Router_Confirm.py index c88978d8..8b5455f6 100644 --- a/B06_Section/B06_Section_Router_Confirm.py +++ b/B06_Section/B06_Section_Router_Confirm.py @@ -11,6 +11,7 @@ import asyncio import json import logging +import time from pathlib import Path from typing import Any from uuid import UUID @@ -20,6 +21,7 @@ from fastapi import APIRouter, Body from fastapi.responses import JSONResponse from B03_FileInput.B03_FileInput_Repository import get_project_storage_relative_path +from B03_FileInput.B03_FileInput_Service_Chain import _log_steps from B05_Profile.B05_Profile_Repository import confirm_route as confirm_route_status from B05_Profile.B05_Profile_Router_Confirm import _merge_uphill_overrides_into_longitudinal from B06_Section.B06_Section_Repository import ( @@ -212,6 +214,7 @@ async def save_sections( 통째로 버려져 조작값이 사라진다. 그래서 여기서 정본 행을 만든다. """ pool = get_db_pool() + marks = [("시작", time.perf_counter())] try: async with pool.acquire() as connection: existing = await get_longitudinal_section(connection, project_id, route_id) @@ -240,6 +243,8 @@ async def save_sections( request.standard_cross_section if request else None, ) + marks.append(("기본설계 계산(미지정 측점)", time.perf_counter())) + async with pool.acquire() as connection: await connection.begin() try: @@ -294,6 +299,7 @@ async def confirm_sections( missing = await get_cross_sections_missing_design_chainages(connection, route_id) known = await get_cross_section_chainages(connection, route_id) + marks.append(("조회(종단·경로·미지정 측점)", time.perf_counter())) project_root = Path(resolve_stored_project_path(stored_path)) # 행 자체가 없는 측점(구조물 등 비정규)도 확정 대상에 넣는다 — 정본이 없으면 # 조회 때마다 프리뷰가 다시 계산돼 3D·수량이 확정 결과가 아니게 된다(2026-08-24). @@ -336,8 +342,11 @@ async def confirm_sections( await connection.rollback() raise + marks.append(("확정 저장(트랜잭션)", time.perf_counter())) + # 편집이 들어간 **뒤** 서버가 정본을 다시 낸다(임시저장과 같은 자리). await _recompute_stored_designs(project_id, route_id) + marks.append(("서버 재계산", time.perf_counter())) # 측구 방향(design.ditch_side) 변경을 B05 종단 정본 stations.uphill_side에 역반영한다(E-7). # 파일 기반·비치명적: 실패해도 확정은 유지한다. @@ -363,6 +372,8 @@ async def confirm_sections( project_id, route_id, ) + marks.append(("측구 방향 B05 역반영", time.perf_counter())) + _log_steps("B06 확정", marks) return SectionConfirmResponse(project_id=str(project_id), route_id=route_id) except Exception: logger.exception("B06 종횡단 확정 실패: project_id=%s", project_id) diff --git a/B06_Section/B06_Section_Server_Calc_Prebuild.py b/B06_Section/B06_Section_Server_Calc_Prebuild.py index 78645c13..fb9d7fb4 100644 --- a/B06_Section/B06_Section_Server_Calc_Prebuild.py +++ b/B06_Section/B06_Section_Server_Calc_Prebuild.py @@ -25,11 +25,13 @@ from __future__ import annotations import asyncio import json import logging +import time from pathlib import Path from typing import Any from uuid import UUID from B03_FileInput.B03_FileInput_Repository import get_project_storage_relative_path +from B03_FileInput.B03_FileInput_Service_Chain import _log_steps from B06_Section.B06_Section_Repository import ( get_longitudinal_section, merge_longitudinal_section_data, @@ -99,6 +101,7 @@ async def _recompute(project_id: UUID | str, route_id: int) -> int: from B06_Section.B06_Section_Router import get_section_detail project_uuid = UUID(str(project_id)) + marks = [("시작", time.perf_counter())] # 상세 만들기(파일 읽기 위주)와 DB 두 건은 서로 기다릴 이유가 없다 — 같이 보낸다. # 원격 DB 라 순차로 내면 왕복이 그대로 더해진다(질의 하나 약 12ms, 2026-09-06 실측). pool = get_db_pool() @@ -107,6 +110,7 @@ async def _recompute(project_id: UUID | str, route_id: int) -> int: run_with_connection(get_project_storage_relative_path, project_uuid), run_with_connection(get_longitudinal_section, project_uuid, route_id), ) + marks.append(("종횡단 상세+DB 조회(병렬)", time.perf_counter())) payload = getattr(response, "model_dump", None) if payload is None: # JSONResponse = 실패 logger.warning("서버 재계산: 종횡단 상세를 못 받음 (route_id=%s)", route_id) @@ -128,12 +132,15 @@ async def _recompute(project_id: UUID | str, route_id: int) -> int: if json.dumps(item.get("design"), sort_keys=True, default=str) != before[index] ] + marks.append(("포장·세월교 보정", time.perf_counter())) + output = await asyncio.to_thread( run_bundle_json, BUNDLE, _NPM_SCRIPT, {"detail": detail, "context": _mass_haul_context()}, ) + marks.append(("Node 번들(면적·유토곡선)", time.perf_counter())) if not isinstance(output, dict): output = {} rows = output.get("areas") @@ -182,6 +189,8 @@ async def _recompute(project_id: UUID | str, route_id: int) -> int: except Exception: await connection.rollback() raise + marks.append(("정본 저장", time.perf_counter())) + _log_steps("서버 재계산 내부", marks) logger.info( "서버 재계산: route_id=%s 설계 보정 %s곳, 구조물 면적 %s곳, 유토곡선 %s", route_id,