Files
Click/test_with_resource_monitoring.py
T
2026-05-14 16:15:58 +07:00

642 lines
26 KiB
Python

"""
Расширенный тест с мониторингом использования ресурсов.
Отслеживает 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())