""" 성능 모니터링 유틸리티 메모리 사용량, CPU 사용률, 실행 시간 등을 모니터링합니다. """ import time import psutil import threading import logging from typing import Dict, List, Optional, Callable from contextlib import contextmanager from dataclasses import dataclass from datetime import datetime @dataclass class PerformanceMetrics: """성능 메트릭 데이터 클래스""" timestamp: datetime cpu_percent: float memory_used_mb: float memory_percent: float disk_io_read_mb: float disk_io_write_mb: float network_io_sent_mb: float network_io_recv_mb: float class PerformanceMonitor: """성능 모니터링을 담당하는 클래스""" def __init__(self, monitoring_interval: float = 1.0, log_threshold_mb: float = 100.0): """ 성능 모니터 초기화 Args: monitoring_interval (float): 모니터링 간격 (초) log_threshold_mb (float): 로그 임계값 (MB) """ self.monitoring_interval = monitoring_interval self.log_threshold_mb = log_threshold_mb self.is_monitoring = False self.monitoring_thread = None self.metrics_history: List[PerformanceMetrics] = [] self.callbacks: List[Callable[[PerformanceMetrics], None]] = [] # 초기 시스템 상태 self.initial_disk_io = psutil.disk_io_counters() self.initial_network_io = psutil.net_io_counters() # 로거 설정 self.logger = logging.getLogger('performance_monitor') def start_monitoring(self): """성능 모니터링을 시작합니다.""" if self.is_monitoring: return self.is_monitoring = True self.monitoring_thread = threading.Thread(target=self._monitoring_loop, daemon=True) self.monitoring_thread.start() self.logger.info("성능 모니터링이 시작되었습니다.") def stop_monitoring(self): """성능 모니터링을 중지합니다.""" if not self.is_monitoring: return self.is_monitoring = False if self.monitoring_thread: self.monitoring_thread.join(timeout=2.0) self.logger.info("성능 모니터링이 중지되었습니다.") def _monitoring_loop(self): """모니터링 루프""" while self.is_monitoring: try: metrics = self._collect_metrics() self.metrics_history.append(metrics) # 콜백 실행 for callback in self.callbacks: try: callback(metrics) except Exception as e: self.logger.error(f"콜백 실행 중 오류: {e}") # 메모리 사용량이 임계값을 초과하면 경고 if metrics.memory_used_mb > self.log_threshold_mb: self.logger.warning( f"높은 메모리 사용량 감지: {metrics.memory_used_mb:.2f} MB " f"({metrics.memory_percent:.1f}%)" ) time.sleep(self.monitoring_interval) except Exception as e: self.logger.error(f"모니터링 중 오류: {e}") time.sleep(self.monitoring_interval) def _collect_metrics(self) -> PerformanceMetrics: """현재 시스템 메트릭을 수집합니다.""" # CPU 사용률 cpu_percent = psutil.cpu_percent() # 메모리 사용량 memory = psutil.virtual_memory() memory_used_mb = memory.used / (1024**2) memory_percent = memory.percent # 디스크 I/O disk_io = psutil.disk_io_counters() disk_io_read_mb = (disk_io.read_bytes - self.initial_disk_io.read_bytes) / (1024**2) disk_io_write_mb = (disk_io.write_bytes - self.initial_disk_io.write_bytes) / (1024**2) # 네트워크 I/O network_io = psutil.net_io_counters() network_io_sent_mb = (network_io.bytes_sent - self.initial_network_io.bytes_sent) / (1024**2) network_io_recv_mb = (network_io.bytes_recv - self.initial_network_io.bytes_recv) / (1024**2) return PerformanceMetrics( timestamp=datetime.now(), cpu_percent=cpu_percent, memory_used_mb=memory_used_mb, memory_percent=memory_percent, disk_io_read_mb=disk_io_read_mb, disk_io_write_mb=disk_io_write_mb, network_io_sent_mb=network_io_sent_mb, network_io_recv_mb=network_io_recv_mb ) def add_callback(self, callback: Callable[[PerformanceMetrics], None]): """성능 메트릭 콜백을 추가합니다.""" self.callbacks.append(callback) def remove_callback(self, callback: Callable[[PerformanceMetrics], None]): """성능 메트릭 콜백을 제거합니다.""" if callback in self.callbacks: self.callbacks.remove(callback) def get_current_metrics(self) -> Optional[PerformanceMetrics]: """현재 메트릭을 반환합니다.""" if not self.metrics_history: return None return self.metrics_history[-1] def get_metrics_summary(self) -> Dict: """메트릭 요약을 반환합니다.""" if not self.metrics_history: return {} cpu_values = [m.cpu_percent for m in self.metrics_history] memory_values = [m.memory_used_mb for m in self.metrics_history] return { 'total_samples': len(self.metrics_history), 'monitoring_duration': ( self.metrics_history[-1].timestamp - self.metrics_history[0].timestamp ).total_seconds(), 'cpu': { 'min': min(cpu_values), 'max': max(cpu_values), 'avg': sum(cpu_values) / len(cpu_values) }, 'memory': { 'min_mb': min(memory_values), 'max_mb': max(memory_values), 'avg_mb': sum(memory_values) / len(memory_values) }, 'peak_memory_mb': max(memory_values), 'total_disk_read_mb': self.metrics_history[-1].disk_io_read_mb, 'total_disk_write_mb': self.metrics_history[-1].disk_io_write_mb, 'total_network_sent_mb': self.metrics_history[-1].network_io_sent_mb, 'total_network_recv_mb': self.metrics_history[-1].network_io_recv_mb } def clear_history(self): """메트릭 히스토리를 초기화합니다.""" self.metrics_history.clear() self.logger.info("성능 메트릭 히스토리가 초기화되었습니다.") class MemoryProfiler: """메모리 프로파일링을 담당하는 클래스""" def __init__(self): """메모리 프로파일러 초기화""" self.snapshots: List[Dict] = [] self.logger = logging.getLogger('memory_profiler') def take_snapshot(self, label: str = None): """메모리 스냅샷을 생성합니다.""" process = psutil.Process() memory_info = process.memory_info() memory_percent = process.memory_percent() snapshot = { 'timestamp': datetime.now(), 'label': label, 'rss_mb': memory_info.rss / (1024**2), # 실제 메모리 사용량 'vms_mb': memory_info.vms / (1024**2), # 가상 메모리 사용량 'percent': memory_percent, 'num_threads': process.num_threads(), 'num_fds': process.num_fds() if hasattr(process, 'num_fds') else 0 } self.snapshots.append(snapshot) if label: self.logger.info(f"메모리 스냅샷 [{label}]: {snapshot['rss_mb']:.2f} MB") return snapshot def get_memory_diff(self, start_label: str, end_label: str) -> Dict: """두 스냅샷 간의 메모리 차이를 계산합니다.""" start_snapshot = None end_snapshot = None for snapshot in self.snapshots: if snapshot['label'] == start_label: start_snapshot = snapshot elif snapshot['label'] == end_label: end_snapshot = snapshot if not start_snapshot or not end_snapshot: return {} return { 'start_label': start_label, 'end_label': end_label, 'rss_diff_mb': end_snapshot['rss_mb'] - start_snapshot['rss_mb'], 'vms_diff_mb': end_snapshot['vms_mb'] - start_snapshot['vms_mb'], 'percent_diff': end_snapshot['percent'] - start_snapshot['percent'], 'time_diff_seconds': ( end_snapshot['timestamp'] - start_snapshot['timestamp'] ).total_seconds() } def clear_snapshots(self): """스냅샷을 초기화합니다.""" self.snapshots.clear() self.logger.info("메모리 스냅샷이 초기화되었습니다.") @contextmanager def monitor_performance(monitor: PerformanceMonitor, label: str = None): """성능 모니터링 컨텍스트 매니저""" if label: monitor.take_snapshot(f"{label}_start") monitor.start_monitoring() try: yield monitor finally: monitor.stop_monitoring() if label: monitor.take_snapshot(f"{label}_end") @contextmanager def memory_profiling(profiler: MemoryProfiler, label: str = None): """메모리 프로파일링 컨텍스트 매니저""" start_snapshot = profiler.take_snapshot(f"{label}_start" if label else "start") try: yield profiler finally: end_snapshot = profiler.take_snapshot(f"{label}_end" if label else "end") if label: diff = profiler.get_memory_diff(f"{label}_start", f"{label}_end") if diff: profiler.logger.info( f"메모리 프로파일링 [{label}]: " f"RSS 차이: {diff['rss_diff_mb']:.2f} MB, " f"실행 시간: {diff['time_diff_seconds']:.2f}초" ) # 전역 인스턴스 _global_performance_monitor = None _global_memory_profiler = None def get_performance_monitor() -> PerformanceMonitor: """전역 성능 모니터 인스턴스를 반환합니다.""" global _global_performance_monitor if _global_performance_monitor is None: _global_performance_monitor = PerformanceMonitor() return _global_performance_monitor def get_memory_profiler() -> MemoryProfiler: """전역 메모리 프로파일러 인스턴스를 반환합니다.""" global _global_memory_profiler if _global_memory_profiler is None: _global_memory_profiler = MemoryProfiler() return _global_memory_profiler