Coverage for core / performance_profiler.py: 98%
69 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-04 15:08 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-04 15:08 +0000
1# core/performance_profiler.py
2import time
3import logging
4import os
5from contextlib import contextmanager, asynccontextmanager
6from typing import Optional
7from core.logger import get_performance_logger
9try:
10 import pyinstrument
11 HAS_PYINSTRUMENT = True
12except ImportError:
13 HAS_PYINSTRUMENT = False
16class PerformanceProfiler:
17 """
18 시스템 전반의 성능 측정 및 로깅을 담당하는 클래스.
19 - 타이머: 주요 함수별 동작 시간을 측정 (start_timer / log_timer)
20 - 프로파일링: Pyinstrument 기반 병목 구간 분석 (profile / profile_async)
21 """
22 PROFILE_OUTPUT_DIR = "logs/profile"
24 def __init__(self, logger: Optional[logging.Logger] = None, enabled: bool = False, threshold: float = 0.0):
25 self.logger = logger if logger else get_performance_logger()
26 self.enabled = enabled
27 self.threshold = threshold
29 # ── 타이머 (기존) ──
31 def start_timer(self) -> float:
32 """타이머 시작 (현재 시간 반환). 비활성화 시 0.0 반환."""
33 return time.time() if self.enabled else 0.0
35 def log_timer(self, name: str, start_time: float, extra_info: str = "", threshold: Optional[float] = None):
36 """
37 시작 시간으로부터 현재까지의 경과 시간을 로깅.
38 start_time이 0.0이면(비활성) 무시.
39 threshold가 제공되면 인스턴스 기본값 대신 사용.
40 """
41 if not self.enabled or start_time == 0.0:
42 return
44 duration = time.time() - start_time
46 # 호출 시 지정한 threshold가 있으면 우선 사용, 없으면 기본값 사용
47 limit = threshold if threshold is not None else self.threshold
49 if duration < limit:
50 return
52 msg = f"[Performance] {name}: {duration:.4f}s"
53 if extra_info:
54 msg += f" ({extra_info})"
56 self.logger.info(msg)
58 # ── Pyinstrument 프로파일링 ──
60 def _check_pyinstrument(self):
61 if not HAS_PYINSTRUMENT:
62 self.logger.warning("[Profile] pyinstrument가 설치되어 있지 않습니다. pip install pyinstrument")
63 return False
64 return True
66 def _save_profile_result(self, profiler: "pyinstrument.Profiler", name: str, save_html: bool):
67 """프로파일 결과를 로그 출력 및 HTML 저장."""
68 text_output = profiler.output_text(unicode=True, color=False)
69 self.logger.info(f"[Profile] {name}\n{text_output}")
71 if save_html:
72 os.makedirs(self.PROFILE_OUTPUT_DIR, exist_ok=True)
73 timestamp = time.strftime("%Y%m%d_%H%M%S")
74 safe_name = name.replace(" ", "_").replace("/", "_")
75 filepath = os.path.join(self.PROFILE_OUTPUT_DIR, f"{safe_name}_{timestamp}.html")
76 html_output = profiler.output_html()
77 with open(filepath, "w", encoding="utf-8") as f:
78 f.write(html_output)
79 self.logger.info(f"[Profile] HTML 저장: {filepath}")
81 @contextmanager
82 def profile(self, name: str = "profile", save_html: bool = True):
83 """
84 동기 코드 블록의 병목 구간을 분석하는 컨텍스트 매니저.
86 사용 예:
87 with pm.profile("주문_처리"):
88 execute_order(...)
89 """
90 if not self.enabled or not self._check_pyinstrument():
91 yield
92 return
94 profiler = pyinstrument.Profiler()
95 profiler.start()
96 try:
97 yield profiler
98 finally:
99 profiler.stop()
100 self._save_profile_result(profiler, name, save_html)
102 @asynccontextmanager
103 async def profile_async(self, name: str = "profile", save_html: bool = True):
104 """
105 비동기 코드 블록의 병목 구간을 분석하는 컨텍스트 매니저.
107 사용 예:
108 async with pm.profile_async("API_호출_분석"):
109 await fetch_stock_data(...)
110 """
111 if not self.enabled or not self._check_pyinstrument():
112 yield
113 return
115 profiler = pyinstrument.Profiler(async_mode="enabled")
116 profiler.start()
117 try:
118 yield profiler
119 finally:
120 profiler.stop()
121 self._save_profile_result(profiler, name, save_html)