""" Расширенный тест с мониторингом использования ресурсов. Отслеживает CPU, RAM, диск и сеть во время выполнения. """ import asyncio import sys import random import time import json from pathlib import Path from datetime import datetime from typing import Dict, List, Optional from dataclasses import dataclass, field from contextlib import asynccontextmanager sys.path.insert(0, str(Path(__file__).parent)) import psutil import matplotlib.pyplot as plt import matplotlib.dates as mdates from config.settings import settings from core.logger import setup_logger from managers.proxy_manager import ProxyManager from services.browser_service import BrowserService logger = setup_logger( name="test_resources", log_level="DEBUG", log_file="logs/test_resources.log" ) @dataclass class ResourceSnapshot: """Снимок состояния ресурсов в момент времени.""" timestamp: float cpu_percent: float memory_percent: float memory_used_mb: float memory_available_mb: float process_memory_mb: float process_cpu_percent: float disk_read_mb: float disk_write_mb: float net_sent_mb: float net_recv_mb: float open_files: int threads: int @dataclass class TestPhase: """Фаза теста с метриками.""" name: str start_time: float end_time: float = 0 snapshots: List[ResourceSnapshot] = field(default_factory=list) @property def duration(self) -> float: return self.end_time - self.start_time if self.end_time else 0 @property def avg_cpu(self) -> float: if not self.snapshots: return 0 return sum(s.cpu_percent for s in self.snapshots) / len(self.snapshots) @property def avg_memory_mb(self) -> float: if not self.snapshots: return 0 return sum(s.process_memory_mb for s in self.snapshots) / len(self.snapshots) @property def peak_memory_mb(self) -> float: if not self.snapshots: return 0 return max(s.process_memory_mb for s in self.snapshots) class ResourceMonitor: """ Монитор ресурсов системы. Собирает метрики CPU, RAM, диска и сети. """ def __init__(self, interval: float = 0.5): """ Args: interval: Интервал сбора метрик в секундах """ self.interval = interval self.process = psutil.Process() self._monitoring = False self._task: Optional[asyncio.Task] = None self._snapshots: List[ResourceSnapshot] = [] self._last_disk = psutil.disk_io_counters() self._last_net = psutil.net_io_counters() self._start_time = time.time() # Получаем начальные значения для расчета дельты self._baseline_memory = self.process.memory_info().rss / (1024 * 1024) logger.info(f"📊 Базовая память процесса: {self._baseline_memory:.1f} MB") async def start(self): """Запуск мониторинга.""" self._monitoring = True self._task = asyncio.create_task(self._monitor_loop()) logger.info("📊 Мониторинг ресурсов запущен") async def stop(self) -> List[ResourceSnapshot]: """Остановка мониторинга.""" self._monitoring = False if self._task: self._task.cancel() try: await self._task except asyncio.CancelledError: pass logger.info(f"📊 Мониторинг остановлен. Собрано снимков: {len(self._snapshots)}") return self._snapshots async def _monitor_loop(self): """Цикл сбора метрик.""" while self._monitoring: try: snapshot = await self._collect_snapshot() self._snapshots.append(snapshot) await asyncio.sleep(self.interval) except asyncio.CancelledError: break except Exception as e: logger.error(f"Ошибка сбора метрик: {e}") await asyncio.sleep(1) async def _collect_snapshot(self) -> ResourceSnapshot: """Сбор одного снимка метрик.""" # CPU cpu_percent = psutil.cpu_percent(interval=None) process_cpu = self.process.cpu_percent(interval=None) # Memory memory = psutil.virtual_memory() process_mem = self.process.memory_info() # Disk (delta) current_disk = psutil.disk_io_counters() disk_read = (current_disk.read_bytes - self._last_disk.read_bytes) / (1024 * 1024) disk_write = (current_disk.write_bytes - self._last_disk.write_bytes) / (1024 * 1024) self._last_disk = current_disk # Network (delta) current_net = psutil.net_io_counters() net_sent = (current_net.bytes_sent - self._last_net.bytes_sent) / (1024 * 1024) net_recv = (current_net.bytes_recv - self._last_net.bytes_recv) / (1024 * 1024) self._last_net = current_net return ResourceSnapshot( timestamp=time.time() - self._start_time, cpu_percent=cpu_percent, memory_percent=memory.percent, memory_used_mb=memory.used / (1024 * 1024), memory_available_mb=memory.available / (1024 * 1024), process_memory_mb=process_mem.rss / (1024 * 1024), process_cpu_percent=process_cpu, disk_read_mb=disk_read, disk_write_mb=disk_write, net_sent_mb=net_sent, net_recv_mb=net_recv, open_files=len(self.process.open_files()), threads=self.process.num_threads() ) class ResourceTestRunner: """ Тест с отслеживанием ресурсов. """ def __init__(self): self.monitor = ResourceMonitor(interval=0.3) # Каждые 300мс self.proxy_manager: Optional[ProxyManager] = None self.browser_service: Optional[BrowserService] = None self.phases: List[TestPhase] = [] async def setup(self): """Инициализация.""" self.proxy_manager = ProxyManager() self.browser_service = BrowserService(self.proxy_manager) # Вывод информации о системе self._print_system_info() def _print_system_info(self): """Вывод информации о системе.""" logger.info("="*60) logger.info("💻 ИНФОРМАЦИЯ О СИСТЕМЕ") logger.info("="*60) cpu_count = psutil.cpu_count() memory = psutil.virtual_memory() disk = psutil.disk_usage('/') logger.info(f"🖥️ CPU:") logger.info(f" • Ядер: {cpu_count}") logger.info(f" • Частота: {psutil.cpu_freq().current:.0f} MHz") logger.info(f"🧠 Память:") logger.info(f" • Всего: {memory.total / (1024**3):.1f} GB") logger.info(f" • Доступно: {memory.available / (1024**3):.1f} GB") logger.info(f"💾 Диск:") logger.info(f" • Всего: {disk.total / (1024**3):.1f} GB") logger.info(f" • Свободно: {disk.free / (1024**3):.1f} GB") @asynccontextmanager async def _measure_phase(self, phase_name: str): """Контекстный менеджер для измерения фазы.""" phase = TestPhase(name=phase_name, start_time=time.time()) # Запускаем мониторинг если не запущен was_monitoring = self.monitor._monitoring if not was_monitoring: await self.monitor.start() try: yield phase finally: if not was_monitoring: phase.snapshots = await self.monitor.stop() phase.end_time = time.time() # Логируем статистику фазы logger.info(f"\n📊 Фаза: {phase_name}") logger.info(f" • Длительность: {phase.duration:.1f}s") logger.info(f" • Средний CPU: {phase.avg_cpu:.1f}%") logger.info(f" • Средняя память: {phase.avg_memory_mb:.1f} MB") logger.info(f" • Пиковая память: {phase.peak_memory_mb:.1f} MB") self.phases.append(phase) async def test_initialization(self): """Тест инициализации.""" async with self._measure_phase("Инициализация") as phase: logger.info("🔧 Тест инициализации компонентов...") # Создаем браузер browser = None try: from camoufox.async_api import AsyncCamoufox browser = await AsyncCamoufox(headless=True).__aenter__() await asyncio.sleep(1) # Ждем полной инициализации except Exception as e: logger.error(f"Ошибка: {e}") finally: if browser: await browser.__aexit__(None, None, None) await asyncio.sleep(2) # Ждем стабилизации метрик async def test_single_visit(self, url: str, reading_time: int): """Тест одного посещения.""" async with self._measure_phase(f"Посещение {url}") as phase: logger.info(f"🌐 Посещение: {url}") logger.info(f" Время чтения: {reading_time}s") result = await self.browser_service.visit_page(url, reading_time) if result.success: logger.info(f" ✅ Успешно") else: logger.error(f" ❌ Ошибка: {result.error}") await asyncio.sleep(2) # Ждем освобождения ресурсов async def test_sequential_visits(self, count: int): """Тест последовательных посещений.""" async with self._measure_phase(f"{count} последовательных посещений") as phase: test_urls = [ "https://httpbin.org/headers", "https://httpbin.org/ip", "https://httpbin.org/user-agent", "https://www.example.com", ] for i in range(count): url = test_urls[i % len(test_urls)] reading = random.randint(5, 15) logger.info(f"📍 Посещение {i+1}/{count}: {url}") try: result = await self.browser_service.visit_page(url, reading) status = "✅" if result.success else "❌" logger.info(f" {status} Память: {self._get_current_memory():.1f} MB") except Exception as e: logger.error(f" ❌ Ошибка: {e}") # Пауза между посещениями if i < count - 1: delay = random.randint(3, 8) logger.info(f" ⏳ Пауза {delay}s") await asyncio.sleep(delay) async def test_concurrent_browsers(self, count: int): """Тест параллельных браузеров.""" async with self._measure_phase(f"{count} параллельных браузеров") as phase: logger.info(f"🌐 Запуск {count} браузеров одновременно...") async def open_browser(idx): try: url = f"https://httpbin.org/headers" result = await self.browser_service.visit_page(url, 10) logger.info(f" Браузер {idx}: {'✅' if result.success else '❌'} " f"(память: {self._get_current_memory():.1f} MB)") except Exception as e: logger.error(f" Браузер {idx}: ❌ {e}") # Запускаем параллельно tasks = [open_browser(i) for i in range(count)] await asyncio.gather(*tasks) await asyncio.sleep(3) # Ждем освобождения def _get_current_memory(self) -> float: """Текущее использование памяти процессом в MB.""" return psutil.Process().memory_info().rss / (1024 * 1024) async def run_comprehensive_test(self, target_url: str = None): """Запуск комплексного теста.""" logger.info("\n" + "🚀"*30) logger.info("КОМПЛЕКСНЫЙ ТЕСТ С МОНИТОРИНГОМ РЕСУРСОВ") logger.info("🚀"*30 + "\n") await self.setup() # Фаза 1: Инициализация await self.test_initialization() # Фаза 2: Одиночное посещение url = target_url or "https://httpbin.org/headers" await self.test_single_visit(url, 10) # Фаза 3: Последовательные посещения await self.test_sequential_visits(5) # Фаза 4: Параллельные браузеры (осторожно!) logger.warning("⚠️ Тест параллельных браузеров (может использовать много памяти)") await self.test_concurrent_browsers(3) # Вывод результатов self._print_results() self._generate_charts() self._save_results_json() def _print_results(self): """Вывод результатов теста.""" logger.info("\n" + "="*60) logger.info("📊 ИТОГОВЫЕ РЕЗУЛЬТАТЫ") logger.info("="*60) for phase in self.phases: logger.info(f"\n📈 {phase.name}:") logger.info(f" • Длительность: {phase.duration:.1f}s") logger.info(f" • Средний CPU: {phase.avg_cpu:.1f}%") logger.info(f" • Средняя RAM: {phase.avg_memory_mb:.1f} MB") logger.info(f" • Пиковая RAM: {phase.peak_memory_mb:.1f} MB") logger.info(f" • Снимков: {len(phase.snapshots)}") def _generate_charts(self): """Генерация графиков использования ресурсов.""" logger.info("\n📊 Генерация графиков...") try: # Собираем все снимки all_snapshots = [] phase_boundaries = [] current_time = 0 for phase in self.phases: for snapshot in phase.snapshots: snapshot.timestamp += current_time all_snapshots.append(snapshot) if phase.snapshots: current_time = phase.snapshots[-1].timestamp phase_boundaries.append(current_time) if not all_snapshots: logger.warning("Нет данных для графиков") return # Создаем графики fig, axes = plt.subplots(3, 2, figsize=(15, 12)) fig.suptitle('Использование ресурсов во время теста', fontsize=14, fontweight='bold') timestamps = [s.timestamp for s in all_snapshots] # CPU ax = axes[0, 0] ax.plot(timestamps, [s.cpu_percent for s in all_snapshots], color='blue', alpha=0.7, linewidth=1) ax.fill_between(timestamps, [s.cpu_percent for s in all_snapshots], alpha=0.2, color='blue') ax.set_title('CPU Usage (%)') ax.set_ylabel('%') ax.grid(True, alpha=0.3) self._add_phase_lines(ax, phase_boundaries) # Process CPU ax = axes[0, 1] ax.plot(timestamps, [s.process_cpu_percent for s in all_snapshots], color='orange', alpha=0.7, linewidth=1) ax.set_title('Process CPU Usage (%)') ax.set_ylabel('%') ax.grid(True, alpha=0.3) self._add_phase_lines(ax, phase_boundaries) # Memory ax = axes[1, 0] ax.plot(timestamps, [s.process_memory_mb for s in all_snapshots], color='green', alpha=0.7, linewidth=1) ax.fill_between(timestamps, [s.process_memory_mb for s in all_snapshots], alpha=0.2, color='green') ax.set_title('Process Memory (MB)') ax.set_ylabel('MB') ax.grid(True, alpha=0.3) self._add_phase_lines(ax, phase_boundaries) # Peak memory annotation peak_idx = max(range(len(all_snapshots)), key=lambda i: all_snapshots[i].process_memory_mb) peak_mem = all_snapshots[peak_idx].process_memory_mb ax.annotate(f'Peak: {peak_mem:.1f} MB', xy=(timestamps[peak_idx], peak_mem), xytext=(10, 10), textcoords='offset points', bbox=dict(boxstyle='round,pad=0.5', fc='yellow', alpha=0.7), arrowprops=dict(arrowstyle='->')) # System Memory ax = axes[1, 1] ax.plot(timestamps, [s.memory_percent for s in all_snapshots], color='red', alpha=0.7, linewidth=1) ax.set_title('System Memory Usage (%)') ax.set_ylabel('%') ax.grid(True, alpha=0.3) self._add_phase_lines(ax, phase_boundaries) # Network ax = axes[2, 0] ax.plot(timestamps, [s.net_recv_mb for s in all_snapshots], color='purple', alpha=0.7, linewidth=1, label='Received') ax.plot(timestamps, [s.net_sent_mb for s in all_snapshots], color='pink', alpha=0.7, linewidth=1, label='Sent') ax.set_title('Network Traffic (MB/s)') ax.set_ylabel('MB/s') ax.legend() ax.grid(True, alpha=0.3) self._add_phase_lines(ax, phase_boundaries) # Threads & Files ax = axes[2, 1] ax2 = ax.twinx() ax.plot(timestamps, [s.threads for s in all_snapshots], color='brown', alpha=0.7, linewidth=1, label='Threads') ax2.plot(timestamps, [s.open_files for s in all_snapshots], color='gray', alpha=0.7, linewidth=1, label='Open Files') ax.set_title('Threads & Open Files') ax.set_ylabel('Threads', color='brown') ax2.set_ylabel('Open Files', color='gray') ax.grid(True, alpha=0.3) lines1, labels1 = ax.get_legend_handles_labels() lines2, labels2 = ax2.get_legend_handles_labels() ax.legend(lines1 + lines2, labels1 + labels2) self._add_phase_lines(ax, phase_boundaries) plt.tight_layout() # Сохраняем chart_file = f"test_results_{datetime.now().strftime('%Y%m%d_%H%M%S')}.png" plt.savefig(chart_file, dpi=150, bbox_inches='tight') plt.close() logger.info(f"✅ Графики сохранены: {chart_file}") except Exception as e: logger.error(f"❌ Ошибка создания графиков: {e}") def _add_phase_lines(self, ax, boundaries): """Добавление линий фаз на график.""" for boundary in boundaries: ax.axvline(x=boundary, color='black', linestyle='--', alpha=0.3) def _save_results_json(self): """Сохранение результатов в JSON.""" try: results = { "test_time": datetime.now().isoformat(), "system_info": { "cpu_count": psutil.cpu_count(), "total_memory_gb": psutil.virtual_memory().total / (1024**3), }, "phases": [] } for phase in self.phases: phase_data = { "name": phase.name, "duration_seconds": phase.duration, "avg_cpu_percent": phase.avg_cpu, "avg_memory_mb": phase.avg_memory_mb, "peak_memory_mb": phase.peak_memory_mb, "snapshots_count": len(phase.snapshots), "memory_timeline": [ { "time": s.timestamp, "cpu": s.cpu_percent, "memory_mb": s.process_memory_mb } for s in phase.snapshots[::5] # Каждый 5-й для компактности ] } results["phases"].append(phase_data) json_file = f"test_results_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json" with open(json_file, 'w', encoding='utf-8') as f: json.dump(results, f, indent=2, ensure_ascii=False) logger.info(f"✅ JSON результаты сохранены: {json_file}") except Exception as e: logger.error(f"❌ Ошибка сохранения JSON: {e}") async def run_memory_test(): """ Специальный тест для измерения потребления памяти. Показывает сколько памяти используется в каждый момент. """ runner = ResourceTestRunner() await runner.setup() print("\n" + "="*60) print("🧠 ТЕСТ ПОТРЕБЛЕНИЯ ПАМЯТИ") print("="*60) # Тест 1: Базовое потребление print("\n1️⃣ Базовое потребление (без браузеров):") for i in range(5): mem = runner._get_current_memory() print(f" {i+1}. {mem:.1f} MB") await asyncio.sleep(1) # Тест 2: С одним браузером print("\n2️⃣ Запуск браузера и посещение:") for i in range(3): mem_before = runner._get_current_memory() print(f" До посещения: {mem_before:.1f} MB") result = await runner.browser_service.visit_page( "https://httpbin.org/headers", 5 ) mem_after = runner._get_current_memory() print(f" После посещения: {mem_after:.1f} MB " f"(Δ = {mem_after - mem_before:+.1f} MB)") await asyncio.sleep(3) # Тест 3: Параллельные браузеры print("\n3️⃣ Параллельные браузеры:") for count in [1, 2, 3, 5]: mem_before = runner._get_current_memory() async def visit_page(idx): return await runner.browser_service.visit_page( "https://httpbin.org/headers", 3 ) tasks = [visit_page(i) for i in range(count)] await asyncio.gather(*tasks) await asyncio.sleep(2) mem_during = runner._get_current_memory() print(f" {count} браузеров: было {mem_before:.1f} MB, " f"стало {mem_during:.1f} MB " f"(Δ = {mem_during - mem_before:+.1f} MB)") await asyncio.sleep(5) # Ждем освобождения памяти print("\n✅ Тест памяти завершен") async def main(): """Точка входа.""" import argparse parser = argparse.ArgumentParser(description="Тест с мониторингом ресурсов") parser.add_argument("--url", type=str, help="URL для тестирования") parser.add_argument("--quick", action="store_true", help="Быстрый тест") parser.add_argument("--memory", action="store_true", help="Тест только памяти") parser.add_argument("--full", action="store_true", help="Полный тест") args = parser.parse_args() try: if args.memory: await run_memory_test() elif args.quick: runner = ResourceTestRunner() await runner.setup() await runner.test_single_visit( args.url or "https://httpbin.org/headers", 10 ) runner._print_results() runner._generate_charts() else: # Полный тест по умолчанию runner = ResourceTestRunner() await runner.run_comprehensive_test(args.url) except KeyboardInterrupt: print("\n⚠️ Тест прерван") except Exception as e: logger.error(f"Ошибка: {e}", exc_info=True) if __name__ == "__main__": print(""" ╔══════════════════════════════════════════════════════════════╗ ║ 🧪 ТЕСТ С МОНИТОРИНГОМ РЕСУРСОВ (CPU, RAM, DISK) ║ ╚══════════════════════════════════════════════════════════════╝ """) asyncio.run(main())