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

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 

8 

9try: 

10 import pyinstrument 

11 HAS_PYINSTRUMENT = True 

12except ImportError: 

13 HAS_PYINSTRUMENT = False 

14 

15 

16class PerformanceProfiler: 

17 """ 

18 시스템 전반의 성능 측정 및 로깅을 담당하는 클래스. 

19 - 타이머: 주요 함수별 동작 시간을 측정 (start_timer / log_timer) 

20 - 프로파일링: Pyinstrument 기반 병목 구간 분석 (profile / profile_async) 

21 """ 

22 PROFILE_OUTPUT_DIR = "logs/profile" 

23 

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 

28 

29 # ── 타이머 (기존) ── 

30 

31 def start_timer(self) -> float: 

32 """타이머 시작 (현재 시간 반환). 비활성화 시 0.0 반환.""" 

33 return time.time() if self.enabled else 0.0 

34 

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 

43 

44 duration = time.time() - start_time 

45 

46 # 호출 시 지정한 threshold가 있으면 우선 사용, 없으면 기본값 사용 

47 limit = threshold if threshold is not None else self.threshold 

48 

49 if duration < limit: 

50 return 

51 

52 msg = f"[Performance] {name}: {duration:.4f}s" 

53 if extra_info: 

54 msg += f" ({extra_info})" 

55 

56 self.logger.info(msg) 

57 

58 # ── Pyinstrument 프로파일링 ── 

59 

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 

65 

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

70 

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

80 

81 @contextmanager 

82 def profile(self, name: str = "profile", save_html: bool = True): 

83 """ 

84 동기 코드 블록의 병목 구간을 분석하는 컨텍스트 매니저. 

85 

86 사용 예: 

87 with pm.profile("주문_처리"): 

88 execute_order(...) 

89 """ 

90 if not self.enabled or not self._check_pyinstrument(): 

91 yield 

92 return 

93 

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) 

101 

102 @asynccontextmanager 

103 async def profile_async(self, name: str = "profile", save_html: bool = True): 

104 """ 

105 비동기 코드 블록의 병목 구간을 분석하는 컨텍스트 매니저. 

106 

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 

114 

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)