Files
gogo2/COBY/tests/test_load_performance.py
Dobromir Popov 622d059aae 18: tests, fixes
2025-08-05 14:11:49 +03:00

590 lines
21 KiB
Python

"""
Load testing and performance benchmarks for high-frequency data scenarios.
"""
import pytest
import asyncio
import time
import statistics
from datetime import datetime, timezone
from concurrent.futures import ThreadPoolExecutor
from typing import List, Dict, Any
import psutil
import gc
from ..models.core import OrderBookSnapshot, TradeEvent, PriceLevel
from ..connectors.binance_connector import BinanceConnector
from ..processing.data_processor import DataProcessor
from ..aggregation.aggregation_engine import AggregationEngine
from ..monitoring.metrics_collector import MetricsCollector
from ..monitoring.latency_tracker import LatencyTracker
from ..utils.logging import get_logger
logger = get_logger(__name__)
class LoadTestConfig:
"""Configuration for load tests"""
# Test parameters
DURATION_SECONDS = 60
TARGET_TPS = 1000 # Transactions per second
RAMP_UP_SECONDS = 10
# Performance thresholds
MAX_LATENCY_MS = 100
MAX_MEMORY_MB = 500
MIN_SUCCESS_RATE = 99.0
# Data generation
SYMBOLS = ["BTCUSDT", "ETHUSDT", "ADAUSDT", "DOTUSDT"]
EXCHANGES = ["binance", "coinbase", "kraken", "bybit"]
class DataGenerator:
"""Generate realistic test data for load testing"""
def __init__(self):
self.base_prices = {
"BTCUSDT": 50000.0,
"ETHUSDT": 3000.0,
"ADAUSDT": 1.0,
"DOTUSDT": 25.0
}
self.counter = 0
def generate_orderbook(self, symbol: str, exchange: str) -> OrderBookSnapshot:
"""Generate realistic order book data"""
base_price = self.base_prices.get(symbol, 100.0)
# Add some randomness
price_variation = (self.counter % 100) * 0.01
mid_price = base_price + price_variation
# Generate bids (below mid price)
bids = []
for i in range(10):
price = mid_price - (i + 1) * 0.1
size = 1.0 + (i * 0.1)
bids.append(PriceLevel(price=price, size=size))
# Generate asks (above mid price)
asks = []
for i in range(10):
price = mid_price + (i + 1) * 0.1
size = 1.0 + (i * 0.1)
asks.append(PriceLevel(price=price, size=size))
self.counter += 1
return OrderBookSnapshot(
symbol=symbol,
exchange=exchange,
timestamp=datetime.now(timezone.utc),
bids=bids,
asks=asks
)
def generate_trade(self, symbol: str, exchange: str) -> TradeEvent:
"""Generate realistic trade data"""
base_price = self.base_prices.get(symbol, 100.0)
price_variation = (self.counter % 50) * 0.01
price = base_price + price_variation
self.counter += 1
return TradeEvent(
symbol=symbol,
exchange=exchange,
timestamp=datetime.now(timezone.utc),
price=price,
size=0.1 + (self.counter % 10) * 0.01,
side="buy" if self.counter % 2 == 0 else "sell",
trade_id=str(self.counter)
)
class PerformanceMonitor:
"""Monitor performance during load tests"""
def __init__(self):
self.start_time = None
self.end_time = None
self.latencies = []
self.errors = []
self.memory_samples = []
self.cpu_samples = []
self.process = psutil.Process()
def start(self):
"""Start monitoring"""
self.start_time = time.time()
self.latencies.clear()
self.errors.clear()
self.memory_samples.clear()
self.cpu_samples.clear()
def stop(self):
"""Stop monitoring"""
self.end_time = time.time()
def record_latency(self, latency_ms: float):
"""Record operation latency"""
self.latencies.append(latency_ms)
def record_error(self, error: Exception):
"""Record error"""
self.errors.append(str(error))
def sample_system_metrics(self):
"""Sample system metrics"""
try:
memory_mb = self.process.memory_info().rss / 1024 / 1024
cpu_percent = self.process.cpu_percent()
self.memory_samples.append(memory_mb)
self.cpu_samples.append(cpu_percent)
except Exception as e:
logger.warning(f"Error sampling system metrics: {e}")
def get_results(self) -> Dict[str, Any]:
"""Get performance test results"""
duration = self.end_time - self.start_time if self.end_time else 0
total_operations = len(self.latencies)
results = {
'duration_seconds': duration,
'total_operations': total_operations,
'operations_per_second': total_operations / duration if duration > 0 else 0,
'error_count': len(self.errors),
'success_rate': ((total_operations - len(self.errors)) / total_operations * 100) if total_operations > 0 else 0,
'latency': {
'min_ms': min(self.latencies) if self.latencies else 0,
'max_ms': max(self.latencies) if self.latencies else 0,
'avg_ms': statistics.mean(self.latencies) if self.latencies else 0,
'p50_ms': statistics.median(self.latencies) if self.latencies else 0,
'p95_ms': self._percentile(self.latencies, 95) if self.latencies else 0,
'p99_ms': self._percentile(self.latencies, 99) if self.latencies else 0
},
'memory': {
'min_mb': min(self.memory_samples) if self.memory_samples else 0,
'max_mb': max(self.memory_samples) if self.memory_samples else 0,
'avg_mb': statistics.mean(self.memory_samples) if self.memory_samples else 0
},
'cpu': {
'min_percent': min(self.cpu_samples) if self.cpu_samples else 0,
'max_percent': max(self.cpu_samples) if self.cpu_samples else 0,
'avg_percent': statistics.mean(self.cpu_samples) if self.cpu_samples else 0
}
}
return results
def _percentile(self, data: List[float], percentile: int) -> float:
"""Calculate percentile"""
if not data:
return 0.0
sorted_data = sorted(data)
index = int((percentile / 100.0) * len(sorted_data))
index = min(index, len(sorted_data) - 1)
return sorted_data[index]
@pytest.mark.load
class TestLoadPerformance:
"""Load testing and performance benchmarks"""
@pytest.fixture
def data_generator(self):
"""Create data generator"""
return DataGenerator()
@pytest.fixture
def performance_monitor(self):
"""Create performance monitor"""
return PerformanceMonitor()
@pytest.mark.asyncio
async def test_orderbook_processing_load(self, data_generator, performance_monitor):
"""Test order book processing under high load"""
processor = DataProcessor()
monitor = performance_monitor
monitor.start()
# Generate load
tasks = []
for i in range(LoadTestConfig.TARGET_TPS):
symbol = LoadTestConfig.SYMBOLS[i % len(LoadTestConfig.SYMBOLS)]
exchange = LoadTestConfig.EXCHANGES[i % len(LoadTestConfig.EXCHANGES)]
task = asyncio.create_task(
self._process_orderbook_with_timing(
processor, data_generator, symbol, exchange, monitor
)
)
tasks.append(task)
# Add small delay to simulate realistic load
if i % 100 == 0:
await asyncio.sleep(0.01)
# Wait for all tasks to complete
await asyncio.gather(*tasks, return_exceptions=True)
monitor.stop()
results = monitor.get_results()
# Verify performance requirements
assert results['operations_per_second'] >= LoadTestConfig.TARGET_TPS * 0.8, \
f"Throughput too low: {results['operations_per_second']:.2f} ops/sec"
assert results['latency']['p95_ms'] <= LoadTestConfig.MAX_LATENCY_MS, \
f"P95 latency too high: {results['latency']['p95_ms']:.2f}ms"
assert results['success_rate'] >= LoadTestConfig.MIN_SUCCESS_RATE, \
f"Success rate too low: {results['success_rate']:.2f}%"
logger.info(f"Load test results: {results}")
async def _process_orderbook_with_timing(self, processor, data_generator,
symbol, exchange, monitor):
"""Process order book with timing measurement"""
try:
start_time = time.time()
# Generate and process order book
orderbook = data_generator.generate_orderbook(symbol, exchange)
processed = processor.normalize_orderbook(orderbook.__dict__, exchange)
end_time = time.time()
latency_ms = (end_time - start_time) * 1000
monitor.record_latency(latency_ms)
monitor.sample_system_metrics()
except Exception as e:
monitor.record_error(e)
@pytest.mark.asyncio
async def test_trade_processing_load(self, data_generator, performance_monitor):
"""Test trade processing under high load"""
processor = DataProcessor()
monitor = performance_monitor
monitor.start()
# Generate sustained load for specified duration
end_time = time.time() + LoadTestConfig.DURATION_SECONDS
operation_count = 0
while time.time() < end_time:
symbol = LoadTestConfig.SYMBOLS[operation_count % len(LoadTestConfig.SYMBOLS)]
exchange = LoadTestConfig.EXCHANGES[operation_count % len(LoadTestConfig.EXCHANGES)]
try:
start_time = time.time()
# Generate and process trade
trade = data_generator.generate_trade(symbol, exchange)
processed = processor.normalize_trade(trade.__dict__, exchange)
process_time = time.time()
latency_ms = (process_time - start_time) * 1000
monitor.record_latency(latency_ms)
if operation_count % 100 == 0:
monitor.sample_system_metrics()
operation_count += 1
# Control rate to avoid overwhelming
await asyncio.sleep(0.001) # 1ms delay
except Exception as e:
monitor.record_error(e)
monitor.stop()
results = monitor.get_results()
# Verify performance
assert results['latency']['avg_ms'] <= LoadTestConfig.MAX_LATENCY_MS, \
f"Average latency too high: {results['latency']['avg_ms']:.2f}ms"
assert results['memory']['max_mb'] <= LoadTestConfig.MAX_MEMORY_MB, \
f"Memory usage too high: {results['memory']['max_mb']:.2f}MB"
logger.info(f"Trade processing results: {results}")
@pytest.mark.asyncio
async def test_aggregation_performance(self, data_generator, performance_monitor):
"""Test aggregation engine performance"""
aggregator = AggregationEngine()
monitor = performance_monitor
monitor.start()
# Generate multiple order books for aggregation
orderbooks = []
for i in range(100):
symbol = LoadTestConfig.SYMBOLS[i % len(LoadTestConfig.SYMBOLS)]
exchange = LoadTestConfig.EXCHANGES[i % len(LoadTestConfig.EXCHANGES)]
orderbook = data_generator.generate_orderbook(symbol, exchange)
orderbooks.append(orderbook)
# Test aggregation performance
start_time = time.time()
for orderbook in orderbooks:
try:
# Test price bucketing
buckets = aggregator.create_price_buckets(orderbook)
# Test heatmap generation
heatmap = aggregator.generate_heatmap(buckets)
# Test metrics calculation
metrics = aggregator.calculate_metrics(orderbook)
process_time = time.time()
latency_ms = (process_time - start_time) * 1000
monitor.record_latency(latency_ms)
start_time = process_time
except Exception as e:
monitor.record_error(e)
monitor.stop()
results = monitor.get_results()
# Verify aggregation performance
assert results['latency']['p95_ms'] <= 50, \
f"Aggregation P95 latency too high: {results['latency']['p95_ms']:.2f}ms"
logger.info(f"Aggregation performance results: {results}")
@pytest.mark.asyncio
async def test_concurrent_exchange_processing(self, data_generator, performance_monitor):
"""Test concurrent processing from multiple exchanges"""
processor = DataProcessor()
monitor = performance_monitor
monitor.start()
# Create concurrent tasks for each exchange
tasks = []
for exchange in LoadTestConfig.EXCHANGES:
task = asyncio.create_task(
self._simulate_exchange_load(processor, data_generator, exchange, monitor)
)
tasks.append(task)
# Run all exchanges concurrently
await asyncio.gather(*tasks, return_exceptions=True)
monitor.stop()
results = monitor.get_results()
# Verify concurrent processing performance
expected_total_ops = len(LoadTestConfig.EXCHANGES) * 100 # 100 ops per exchange
assert results['total_operations'] >= expected_total_ops * 0.9, \
f"Not enough operations completed: {results['total_operations']}"
assert results['success_rate'] >= 95.0, \
f"Success rate too low under concurrent load: {results['success_rate']:.2f}%"
logger.info(f"Concurrent processing results: {results}")
async def _simulate_exchange_load(self, processor, data_generator, exchange, monitor):
"""Simulate load from a single exchange"""
for i in range(100):
try:
symbol = LoadTestConfig.SYMBOLS[i % len(LoadTestConfig.SYMBOLS)]
start_time = time.time()
# Alternate between order books and trades
if i % 2 == 0:
data = data_generator.generate_orderbook(symbol, exchange)
processed = processor.normalize_orderbook(data.__dict__, exchange)
else:
data = data_generator.generate_trade(symbol, exchange)
processed = processor.normalize_trade(data.__dict__, exchange)
end_time = time.time()
latency_ms = (end_time - start_time) * 1000
monitor.record_latency(latency_ms)
# Small delay to simulate realistic timing
await asyncio.sleep(0.01)
except Exception as e:
monitor.record_error(e)
@pytest.mark.asyncio
async def test_memory_usage_under_load(self, data_generator):
"""Test memory usage patterns under sustained load"""
processor = DataProcessor()
process = psutil.Process()
# Get baseline memory
gc.collect() # Force garbage collection
baseline_memory = process.memory_info().rss / 1024 / 1024 # MB
memory_samples = []
# Generate sustained load
for i in range(1000):
symbol = LoadTestConfig.SYMBOLS[i % len(LoadTestConfig.SYMBOLS)]
exchange = LoadTestConfig.EXCHANGES[i % len(LoadTestConfig.EXCHANGES)]
# Generate data
orderbook = data_generator.generate_orderbook(symbol, exchange)
trade = data_generator.generate_trade(symbol, exchange)
# Process data
processor.normalize_orderbook(orderbook.__dict__, exchange)
processor.normalize_trade(trade.__dict__, exchange)
# Sample memory every 100 operations
if i % 100 == 0:
current_memory = process.memory_info().rss / 1024 / 1024
memory_samples.append(current_memory)
# Force garbage collection periodically
if i % 500 == 0:
gc.collect()
# Final memory check
gc.collect()
final_memory = process.memory_info().rss / 1024 / 1024
# Calculate memory statistics
max_memory = max(memory_samples) if memory_samples else final_memory
memory_growth = final_memory - baseline_memory
# Verify memory usage is reasonable
assert memory_growth < 100, \
f"Memory growth too high: {memory_growth:.2f}MB"
assert max_memory < baseline_memory + 200, \
f"Peak memory usage too high: {max_memory:.2f}MB"
logger.info(f"Memory usage: baseline={baseline_memory:.2f}MB, "
f"final={final_memory:.2f}MB, growth={memory_growth:.2f}MB, "
f"peak={max_memory:.2f}MB")
@pytest.mark.asyncio
async def test_stress_test_extreme_load(self, data_generator, performance_monitor):
"""Stress test with extreme load conditions"""
processor = DataProcessor()
monitor = performance_monitor
# Extreme load parameters
EXTREME_TPS = 5000
STRESS_DURATION = 30 # seconds
monitor.start()
# Generate extreme load
tasks = []
operations_per_batch = 100
batches = EXTREME_TPS // operations_per_batch
for batch in range(batches):
batch_tasks = []
for i in range(operations_per_batch):
symbol = LoadTestConfig.SYMBOLS[i % len(LoadTestConfig.SYMBOLS)]
exchange = LoadTestConfig.EXCHANGES[i % len(LoadTestConfig.EXCHANGES)]
task = asyncio.create_task(
self._process_orderbook_with_timing(
processor, data_generator, symbol, exchange, monitor
)
)
batch_tasks.append(task)
# Process batch
await asyncio.gather(*batch_tasks, return_exceptions=True)
# Small delay between batches
await asyncio.sleep(0.1)
monitor.stop()
results = monitor.get_results()
# Under extreme load, we accept lower performance but system should remain stable
assert results['success_rate'] >= 80.0, \
f"System failed under stress: {results['success_rate']:.2f}% success rate"
assert results['latency']['p99_ms'] <= 500, \
f"P99 latency too high under stress: {results['latency']['p99_ms']:.2f}ms"
logger.info(f"Stress test results: {results}")
@pytest.mark.benchmark
class TestPerformanceBenchmarks:
"""Performance benchmarks for regression testing"""
def test_orderbook_processing_benchmark(self, benchmark):
"""Benchmark order book processing speed"""
processor = DataProcessor()
generator = DataGenerator()
def process_orderbook():
orderbook = generator.generate_orderbook("BTCUSDT", "binance")
return processor.normalize_orderbook(orderbook.__dict__, "binance")
result = benchmark(process_orderbook)
assert result is not None
def test_trade_processing_benchmark(self, benchmark):
"""Benchmark trade processing speed"""
processor = DataProcessor()
generator = DataGenerator()
def process_trade():
trade = generator.generate_trade("BTCUSDT", "binance")
return processor.normalize_trade(trade.__dict__, "binance")
result = benchmark(process_trade)
assert result is not None
def test_aggregation_benchmark(self, benchmark):
"""Benchmark aggregation engine performance"""
aggregator = AggregationEngine()
generator = DataGenerator()
def aggregate_data():
orderbook = generator.generate_orderbook("BTCUSDT", "binance")
buckets = aggregator.create_price_buckets(orderbook)
return aggregator.generate_heatmap(buckets)
result = benchmark(aggregate_data)
assert result is not None
def pytest_configure(config):
"""Configure pytest with custom markers"""
config.addinivalue_line("markers", "load: mark test as load test")
config.addinivalue_line("markers", "benchmark: mark test as benchmark")
def pytest_addoption(parser):
"""Add custom command line options"""
parser.addoption(
"--load",
action="store_true",
default=False,
help="run load tests"
)
parser.addoption(
"--benchmark",
action="store_true",
default=False,
help="run benchmark tests"
)