Research-Stack/5-Applications/nodupe/tests/utils/performance.py
2026-05-05 21:15:26 -05:00

620 lines
18 KiB
Python

"""NoDupeLabs Performance Test Utilities
Helper functions for performance benchmarking and testing.
"""
import time
import tempfile
from pathlib import Path
from typing import Dict, Any, List, Optional, Union, Callable
import contextlib
import os
import sys
from unittest.mock import MagicMock, patch
try:
import resource
RESOURCE_AVAILABLE = True
except ImportError:
RESOURCE_AVAILABLE = False
try:
import psutil
PSUTIL_AVAILABLE = True
except ImportError:
PSUTIL_AVAILABLE = False
def benchmark_function_performance(
func: Callable,
iterations: int = 100,
warmup_iterations: int = 10,
*args,
**kwargs
) -> Dict[str, float]:
"""
Benchmark a function's performance.
Args:
func: Function to benchmark
iterations: Number of benchmark iterations
warmup_iterations: Number of warmup iterations
*args: Positional arguments for the function
**kwargs: Keyword arguments for the function
Returns:
Dictionary of performance metrics
"""
# Warmup
for _ in range(warmup_iterations):
func(*args, **kwargs)
# Benchmark
start_time = time.time()
for _ in range(iterations):
func(*args, **kwargs)
end_time = time.time()
total_time = end_time - start_time
avg_time = total_time / iterations
ops_per_sec = iterations / total_time
return {
"total_time": total_time,
"average_time": avg_time,
"operations_per_second": ops_per_sec,
"iterations": iterations
}
def measure_memory_usage(
func: Callable,
iterations: int = 10,
*args,
**kwargs
) -> Dict[str, float]:
"""
Measure memory usage of a function.
Args:
func: Function to measure
iterations: Number of iterations
*args: Positional arguments for the function
**kwargs: Keyword arguments for the function
Returns:
Dictionary of memory usage metrics
"""
if not PSUTIL_AVAILABLE:
# Fallback implementation when psutil is not available
# Run the function but return mock memory values
for _ in range(iterations):
func(*args, **kwargs)
return {
"initial_memory": 0,
"final_memory": 0,
"total_memory_used": 0,
"average_memory_per_call": 0,
"iterations": iterations,
"warning": "psutil not available, using fallback implementation"
}
# Get initial memory usage
process = psutil.Process(os.getpid())
initial_mem = process.memory_info().rss
# Run function multiple times
for _ in range(iterations):
func(*args, **kwargs)
# Get final memory usage
final_mem = process.memory_info().rss
# Calculate memory usage
total_memory_used = final_mem - initial_mem
avg_memory_per_call = total_memory_used / iterations
return {
"initial_memory": initial_mem,
"final_memory": final_mem,
"total_memory_used": total_memory_used,
"average_memory_per_call": avg_memory_per_call,
"iterations": iterations
}
def create_performance_test_scenarios() -> List[Dict[str, Any]]:
"""
Create performance test scenarios.
Returns:
List of performance test scenarios
"""
return [
{
"name": "small_dataset",
"data_size": 100,
"expected_max_time": 0.1,
"expected_max_memory": 1024 * 1024 # 1MB
},
{
"name": "medium_dataset",
"data_size": 10000,
"expected_max_time": 1.0,
"expected_max_memory": 10 * 1024 * 1024 # 10MB
},
{
"name": "large_dataset",
"data_size": 1000000,
"expected_max_time": 10.0,
"expected_max_memory": 100 * 1024 * 1024 # 100MB
}
]
def simulate_resource_constraints(
cpu_limit: Optional[float] = None,
memory_limit: Optional[int] = None
) -> Callable:
"""
Create a context manager to simulate resource constraints.
Args:
cpu_limit: CPU limit (percentage)
memory_limit: Memory limit in bytes
Returns:
Context manager for resource constraints
"""
@contextlib.contextmanager
def resource_context():
"""Inner context manager for simulating resource constraints."""
original_limits = {}
try:
if cpu_limit is not None:
# Simulate CPU limit by adding artificial delay
original_limits['cpu'] = cpu_limit
if memory_limit is not None and RESOURCE_AVAILABLE:
# Set memory limit using resource module
if hasattr(resource, 'RLIMIT_AS'):
original_limits['memory'] = resource.getrlimit(resource.RLIMIT_AS)
resource.setrlimit(resource.RLIMIT_AS, (memory_limit, memory_limit))
else:
# Resource module available but RLIMIT_AS not supported
print("Warning: RLIMIT_AS not available on this platform")
elif memory_limit is not None and not RESOURCE_AVAILABLE:
print("Warning: resource module not available, memory limits will not be enforced")
yield
finally:
# Restore original limits
if 'memory' in original_limits and RESOURCE_AVAILABLE and hasattr(resource, 'RLIMIT_AS'):
resource.setrlimit(resource.RLIMIT_AS, original_limits['memory'])
return resource_context
def create_load_test_scenarios() -> List[Dict[str, Any]]:
"""
Create load test scenarios.
Returns:
List of load test scenarios
"""
return [
{
"name": "low_load",
"concurrent_users": 10,
"request_rate": 100,
"duration": 60,
"expected_response_time": 0.1
},
{
"name": "medium_load",
"concurrent_users": 100,
"request_rate": 1000,
"duration": 300,
"expected_response_time": 0.5
},
{
"name": "high_load",
"concurrent_users": 1000,
"request_rate": 10000,
"duration": 600,
"expected_response_time": 1.0
}
]
def benchmark_file_operations(
file_size: int = 1024 * 1024, # 1MB
operations: List[str] = None,
iterations: int = 100
) -> Dict[str, float]:
"""
Benchmark file operations performance.
Args:
file_size: Size of test file in bytes
operations: List of operations to benchmark
iterations: Number of iterations
Returns:
Dictionary of file operation timings
"""
if operations is None:
operations = ["read", "write", "copy", "delete"]
results = {}
temp_dir = Path(tempfile.mkdtemp())
try:
test_file = temp_dir / "test_file.dat"
test_file_copy = temp_dir / "test_file_copy.dat"
# Create test file
with open(test_file, "wb") as f:
f.write(os.urandom(file_size))
for op in operations:
if op == "read":
def read_operation():
"""Read operation for benchmarking."""
with open(test_file, "rb") as f:
f.read()
results[op] = benchmark_function_performance(
read_operation, iterations
)["average_time"]
elif op == "write":
def write_operation():
"""Write operation for benchmarking."""
with open(test_file, "wb") as f:
f.write(os.urandom(file_size))
results[op] = benchmark_function_performance(
write_operation, iterations
)["average_time"]
elif op == "copy":
def copy_operation():
"""Copy operation for benchmarking."""
import shutil
shutil.copy2(test_file, test_file_copy)
if test_file_copy.exists():
test_file_copy.unlink()
results[op] = benchmark_function_performance(
copy_operation, iterations
)["average_time"]
elif op == "delete":
def delete_operation():
"""Delete operation for benchmarking."""
test_file.unlink()
with open(test_file, "wb") as f:
f.write(os.urandom(file_size))
results[op] = benchmark_function_performance(
delete_operation, iterations
)["average_time"]
finally:
# Cleanup
import shutil
shutil.rmtree(temp_dir)
return results
def create_performance_monitor() -> Callable:
"""
Create a performance monitor context manager.
Returns:
Context manager for performance monitoring
"""
@contextlib.contextmanager
def monitor_context():
"""Inner context manager for performance monitoring."""
start_time = time.time()
if PSUTIL_AVAILABLE:
start_mem = psutil.Process(os.getpid()).memory_info().rss
yield
end_time = time.time()
if PSUTIL_AVAILABLE:
end_mem = psutil.Process(os.getpid()).memory_info().rss
memory_used = end_mem - start_mem
cpu_usage = psutil.cpu_percent(interval=0.1)
else:
memory_used = 0
cpu_usage = 0
elapsed_time = end_time - start_time
print(f"Performance Monitor Results:")
print(f" Execution Time: {elapsed_time:.4f} seconds")
if PSUTIL_AVAILABLE:
print(f" Memory Used: {memory_used / 1024 / 1024:.2f} MB")
print(f" CPU Usage: {cpu_usage}%")
else:
print(f" Memory Used: N/A (psutil not available)")
print(f" CPU Usage: N/A (psutil not available)")
return monitor_context
def simulate_slow_operations(
delay: float = 0.1,
variability: float = 0.05
) -> Callable:
"""
Create a context manager to simulate slow operations.
Args:
delay: Base delay in seconds
variability: Random variability in delay
Returns:
Context manager for slow operation simulation
"""
import random
@contextlib.contextmanager
def slow_context():
"""Inner context manager for simulating slow operations."""
original_monotonic = time.monotonic
start_time = original_monotonic()
def slow_monotonic():
"""Mock monotonic time that adds artificial delay."""
elapsed = original_monotonic() - start_time
variability_factor = 1.0 + random.uniform(-variability, variability)
return elapsed + (delay * variability_factor)
# Patch time functions
with patch('time.monotonic', side_effect=slow_monotonic):
with patch('time.sleep', side_effect=lambda x: time.sleep(x * 10)):
yield
return slow_context
def create_stress_test_scenarios() -> List[Dict[str, Any]]:
"""
Create stress test scenarios.
Returns:
List of stress test scenarios
"""
return [
{
"name": "memory_stress",
"type": "memory",
"target": "high_memory_usage",
"duration": 300,
"expected_behavior": "graceful_degradation"
},
{
"name": "cpu_stress",
"type": "cpu",
"target": "high_cpu_usage",
"duration": 180,
"expected_behavior": "resource_throttling"
},
{
"name": "io_stress",
"type": "io",
"target": "high_disk_usage",
"duration": 240,
"expected_behavior": "queue_management"
}
]
def benchmark_database_operations(
db_connection: Any,
queries: List[str],
iterations: int = 100
) -> Dict[str, float]:
"""
Benchmark database operations performance.
Args:
db_connection: Database connection
queries: List of SQL queries to benchmark
iterations: Number of iterations
Returns:
Dictionary of database operation timings
"""
results = {}
for i, query in enumerate(queries):
def query_operation():
"""Execute a database query for benchmarking."""
cursor = db_connection.cursor()
cursor.execute(query)
cursor.fetchall()
cursor.close()
timing = benchmark_function_performance(
query_operation, iterations
)["average_time"]
results[f"query_{i}"] = timing
return results
def create_network_performance_test_scenarios() -> List[Dict[str, Any]]:
"""
Create network performance test scenarios.
Returns:
List of network performance test scenarios
"""
return [
{
"name": "low_latency",
"latency": 10, # ms
"bandwidth": 100, # Mbps
"packet_loss": 0.0 # 0%
},
{
"name": "medium_latency",
"latency": 100, # ms
"bandwidth": 10, # Mbps
"packet_loss": 0.1 # 1%
},
{
"name": "high_latency",
"latency": 500, # ms
"bandwidth": 1, # Mbps
"packet_loss": 0.5 # 5%
}
]
def measure_concurrency_performance(
func: Callable,
worker_counts: List[int] = [1, 2, 4, 8, 16],
iterations: int = 100,
*args,
**kwargs
) -> Dict[int, float]:
"""
Measure performance with different levels of concurrency.
Args:
func: Function to test
worker_counts: List of worker counts to test
iterations: Number of iterations per worker count
*args: Positional arguments for the function
**kwargs: Keyword arguments for the function
Returns:
Dictionary mapping worker counts to performance metrics
"""
import concurrent.futures
results = {}
for workers in worker_counts:
def concurrent_operation():
"""Execute function with multiple workers."""
with concurrent.futures.ThreadPoolExecutor(max_workers=workers) as executor:
futures = [executor.submit(func, *args, **kwargs) for _ in range(iterations)]
concurrent.futures.wait(futures)
timing = benchmark_function_performance(
concurrent_operation, 1
)["total_time"]
results[workers] = {
"total_time": timing,
"throughput": iterations / timing
}
return results
def create_performance_regression_test_scenarios() -> List[Dict[str, Any]]:
"""
Create performance regression test scenarios.
Returns:
List of performance regression test scenarios
"""
return [
{
"name": "baseline_performance",
"description": "Establish baseline performance metrics",
"metrics": {
"max_response_time": 0.5,
"max_memory_usage": 50 * 1024 * 1024, # 50MB
"min_throughput": 100 # operations per second
}
},
{
"name": "regression_detection",
"description": "Detect performance regressions",
"thresholds": {
"response_time_increase": 0.2, # 20% increase
"memory_increase": 0.1, # 10% increase
"throughput_decrease": 0.15 # 15% decrease
}
}
]
def simulate_performance_degradation(
degradation_factor: float = 0.1,
degradation_type: str = "linear"
) -> Callable:
"""
Create a context manager to simulate performance degradation.
Args:
degradation_factor: Performance degradation factor
degradation_type: Type of degradation (linear, exponential)
Returns:
Context manager for performance degradation simulation
"""
@contextlib.contextmanager
def degradation_context():
"""Inner context manager for simulating performance degradation."""
call_count = 0
original_monotonic = time.monotonic
start_time = original_monotonic()
def degraded_monotonic():
"""Mock monotonic time with performance degradation."""
nonlocal call_count
call_count += 1
elapsed = original_monotonic() - start_time
if degradation_type == "linear":
degradation = degradation_factor * call_count
else: # exponential
degradation = degradation_factor ** call_count
return elapsed + degradation
def degraded_sleep(seconds):
"""Mock sleep with performance degradation."""
original_sleep = time.sleep
original_sleep(seconds * (1 + degradation_factor))
with patch('time.monotonic', side_effect=degraded_monotonic):
with patch('time.sleep', side_effect=lambda x: time.sleep(x * (1 + degradation_factor))):
yield
return degradation_context
def create_resource_monitoring_scenarios() -> List[Dict[str, Any]]:
"""
Create resource monitoring test scenarios.
Returns:
List of resource monitoring test scenarios
"""
return [
{
"name": "normal_operation",
"expected_cpu_usage": 0.3, # 30%
"expected_memory_usage": 100 * 1024 * 1024, # 100MB
"expected_disk_io": 1024 * 1024 # 1MB/s
},
{
"name": "high_load_operation",
"expected_cpu_usage": 0.8, # 80%
"expected_memory_usage": 500 * 1024 * 1024, # 500MB
"expected_disk_io": 10 * 1024 * 1024 # 10MB/s
},
{
"name": "resource_leak_detection",
"monitoring_duration": 300, # 5 minutes
"leak_threshold": 0.05 # 5% increase
}
]