Files
foxhunt/services/load_tests/tests/throughput_tests.rs
jgrusewski 83629f9ca8 feat(deployment): Complete Runpod GPU deployment infrastructure
Implement comprehensive Runpod deployment with S3 volume mount architecture for
FP32 ML model training on Tesla V100 GPUs.

## Infrastructure Components

### Deployment Scripts (scripts/)
- runpod_deploy.sh: Master deployment orchestrator (8-step workflow)
- runpod_upload.sh: S3 upload for binaries and test data
- upload_env_to_runpod.sh: Secure .env credentials upload
- runpod_deploy_test.sh: Prerequisites validation

### Docker Configuration
- Dockerfile.runpod: Multi-stage CUDA 12.1 runtime (~2GB, no binaries)
- entrypoint.sh: Volume verification and training execution
- Architecture: Volume mount (NO S3 downloads in pods)

### S3 Configuration
- Bucket: se3zdnb5o4 (Iceland region: eur-is-1)
- Endpoint: https://s3api-eur-is-1.runpod.io
- Structure: binaries/, test_data/, models/, .env

### OpenTofu Infrastructure (terraform/runpod/)
- main.tf: Pod and volume resources
- variables.tf: Configuration variables
- outputs.tf: Pod connection info
- Security: NO credentials in state (uses volume .env)

## Deployment Assets Uploaded

### Training Binaries (77MB)
- train_tft_parquet (23M) - TFT-225 features
- train_mamba2_parquet (22M) - MAMBA-2 state space
- train_dqn (22M) - Deep Q-Network
- train_ppo (13M) - Proximal Policy Optimization

### Test Data (13.8 MB)
- 9 Parquet files: ES.FUT, NQ.FUT, 6E.FUT, ZN.FUT (180-day datasets)

### Credentials
- .env file (1.5 KB, private access, chmod 600)

## Documentation

### Deployment Guides
- RUNPOD_DEPLOYMENT_READY_SUMMARY.md: Complete deployment status
- RUNPOD_VOLUME_DEPLOYMENT_GUIDE.md: Step-by-step guide (42KB)
- RUNPOD_DEPLOYMENT_QUICK_START.md: Quick reference
- RUNPOD_UPLOAD_GUIDE.md: S3 upload instructions
- RUNPOD_VOLUME_CONFIGURATION_COMPLETE.md: S3 setup report
- RUNPOD_S3_PARQUET_UPLOAD_REPORT.md: Data upload verification

### Architecture Documentation
- RUNPOD_VOLUME_MOUNT_ARCHITECTURE.md: Volume mount design
- RUNPOD_S3_ARCHITECTURE_DIAGRAM.txt: S3 API vs filesystem access
- DOCKERFILE_RUNPOD_FINAL_SUMMARY.md: Docker image specification

### Decision Documentation
- RUNPOD_DEPLOYMENT_CHECKLIST.md: Go/no-go decision matrix (27KB)
- RUNPOD_DEPLOYMENT_DECISION_TREE.md: Decision workflow
- FP32_RUNPOD_DEPLOYMENT_READY.md: FP32 deployment readiness

## QAT Enhancements

### Core QAT Infrastructure
- ml/src/memory_optimization/qat.rs: Enhanced QAT observer (+226 lines)
- ml/src/memory_optimization/auto_batch_size.rs: OOM recovery (+84 lines)
- ml/src/tft/qat_tft.rs: QAT TFT wrapper (+154 lines)
- ml/src/trainers/tft.rs: QAT training integration (+433 lines)
- ml/src/qat_metrics_exporter.rs: NEW - QAT metrics export

### QAT Testing
- ml/tests/qat_integration_tests.rs: NEW - Integration test suite
- ml/tests/qat_gradient_clipping_test.rs: NEW - Gradient clipping tests
- ml/tests/qat_device_consistency_test.rs: Device mismatch tests (+205 lines)
- ml/tests/qat_accuracy_validation_test.rs: Accuracy validation
- ml/tests/qat_tft_integration_test.rs: TFT QAT integration

### QAT Documentation
- ml/docs/QAT_GUIDE.md: Comprehensive QAT guide (+616 lines)
- ml/docs/QAT_GRADIENT_CHECKPOINTING_WORKAROUND.md: NEW - Workaround guide
- QAT_BLOCKERS_ROOT_CAUSE_ANALYSIS.md: P0 blocker analysis (44KB)
- QAT_ACCURACY_VALIDATION_REPORT.md: Accuracy comparison
- QAT_GRADIENT_CLIPPING_VALIDATION_REPORT.md: Clipping validation

### QAT Monitoring
- config/grafana/dashboards/qat-training-metrics.json: NEW - Grafana dashboard

## AWS CLI Configuration

### Credentials Setup
- ~/.aws/credentials: Runpod profile configured
  - Access Key: user_2xxA3XcIFj16yfL3aBon9niiSpr
  - Secret Key: (from RUNPOD_S3_SECRET)
- ~/.aws/config: Iceland region (eur-is-1)

## Production Readiness

### FP32 Models:  READY FOR DEPLOYMENT
- DQN: 15-20s training, ~6MB GPU memory
- PPO: 7-10s training, ~145MB GPU memory
- MAMBA-2: 2-3 min training, ~164MB GPU memory
- TFT-225: 3-5 min training, ~500MB GPU memory
- Total GPU Budget: 815MB (fits on 4GB+ Tesla V100)

### QAT Models: 🔴 BLOCKED
- 24 tests implemented but DO NOT COMPILE (11 errors)
- 3 P0 blockers: device mismatch, gradient checkpointing, OOM recovery
- Timeline: 1-2 weeks to fix (13h P0 fixes + validation)

### Wave D Features:  OPERATIONAL
- 225 features fully integrated
- Feature extraction: 5.10μs/bar (196x faster than target)
- Wave D backtest: Sharpe 2.00, Win Rate 60%, Drawdown 15%
- Database migration 045: Applied cleanly, zero conflicts

## Cost Analysis

### One-Time Setup
- Network Volume: $4/month (50GB SSD)
- Upload costs: FREE (S3 API included)

### Per Training Run (TFT-225)
- GPU: Tesla V100-PCIE-16GB @ $0.29/hr
- Training Time: ~4 hours
- Cost per run: $1.16

### Monthly (20 Training Runs)
- Storage: $4.00/month
- Training: $23.20/month (20 runs × $1.16)
- Total: $27.20/month

## Security

### Credentials Management
-  NO credentials in Docker image
-  NO credentials in Terraform state
-  .env gitignored and not committed
-  .env file private on S3 (HTTP 401 on public access)
-  Docker Hub repository PRIVATE (jgrusewski/foxhunt)

### Access Control
- S3 API: Local client uploads only
- Volume mount: Pod filesystem access only
- Authentication: AWS CLI with Runpod profile required

## Next Steps

1.  COMPLETE: Build Docker image
2.  PENDING: Push to Docker Hub
3.  PENDING: Deploy pod via Runpod console
4.  PENDING: Validate training on Tesla V100

## Performance Targets

- Build time: 5-10 min
- Upload time: ~20 sec (90MB total)
- Pod startup: ~30 sec
- Training time: 3-5 min (TFT-225)
- Total deployment: ~40 min from start to first training run

## Test Status

- FP32 tests: 597/608 passing (98.2%)
- QAT tests: 0/24 passing (compilation errors)
- Overall: 2,062/2,086 passing (98.8% excluding QAT)

🤖 Generated with Claude Code (https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-24 01:11:43 +02:00

614 lines
20 KiB
Rust

//! Comprehensive throughput validation tests for trading service
//!
//! This module implements load tests to validate the trading system's capacity:
//! - 10,000 orders/sec sustained load (60 seconds)
//! - 50,000 orders/sec peak burst (10 seconds)
//! - 1M concurrent market data updates (streaming)
//! - Connection pool saturation (1000 concurrent clients)
//!
//! Run with: cargo test -p load_tests --release -- --nocapture
use anyhow::{Context, Result};
use hdrhistogram::Histogram;
use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
use std::sync::Arc;
use std::time::{Duration, Instant};
use sysinfo::{ProcessRefreshKind, RefreshKind, System};
use tokio::sync::Barrier;
use tokio::task::JoinSet;
use tonic::transport::Channel;
// Generated proto code
pub mod trading {
tonic::include_proto!("trading");
}
use trading::{
trading_service_client::TradingServiceClient, OrderSide, OrderType, StreamMarketDataRequest,
SubmitOrderRequest,
};
/// Test configuration
const TRADING_SERVICE_URL: &str = "http://localhost:50051";
const TEST_ACCOUNT_ID: &str = "load-test-account";
/// Metrics collected during load tests
#[derive(Debug)]
pub struct LoadTestMetrics {
/// Total requests sent
pub total_requests: AtomicU64,
/// Successful responses
pub successful_requests: AtomicU64,
/// Failed requests
pub failed_requests: AtomicU64,
/// Latency histogram (microseconds)
pub latency_us: parking_lot::Mutex<Histogram<u64>>,
/// Memory usage samples (bytes)
pub memory_samples: parking_lot::Mutex<Vec<u64>>,
/// Test start time
pub start_time: Instant,
}
impl Default for LoadTestMetrics {
fn default() -> Self {
Self::new()
}
}
impl LoadTestMetrics {
pub fn new() -> Self {
Self {
total_requests: AtomicU64::new(0),
successful_requests: AtomicU64::new(0),
failed_requests: AtomicU64::new(0),
latency_us: parking_lot::Mutex::new(
Histogram::<u64>::new_with_bounds(1, 60_000_000, 3).unwrap(),
),
memory_samples: parking_lot::Mutex::new(Vec::new()),
start_time: Instant::now(),
}
}
pub fn record_request(&self, duration: Duration, success: bool) {
self.total_requests.fetch_add(1, Ordering::Relaxed);
if success {
self.successful_requests.fetch_add(1, Ordering::Relaxed);
} else {
self.failed_requests.fetch_add(1, Ordering::Relaxed);
}
let mut hist = self.latency_us.lock();
let _ = hist.record(u64::try_from(duration.as_micros()).unwrap_or(u64::MAX));
}
pub fn record_memory(&self, bytes: u64) {
self.memory_samples.lock().push(bytes);
}
pub fn report(&self) -> LoadTestReport {
let elapsed = self.start_time.elapsed();
let total = self.total_requests.load(Ordering::Relaxed);
let successful = self.successful_requests.load(Ordering::Relaxed);
let failed = self.failed_requests.load(Ordering::Relaxed);
let hist = self.latency_us.lock();
let p50 = hist.value_at_quantile(0.50);
let p95 = hist.value_at_quantile(0.95);
let p99 = hist.value_at_quantile(0.99);
let max = hist.max();
let throughput = total as f64 / elapsed.as_secs_f64();
let error_rate = if total > 0 {
(failed as f64 / total as f64) * 100.0
} else {
0.0
};
let memory_samples = self.memory_samples.lock();
let avg_memory_mb = if !memory_samples.is_empty() {
memory_samples.iter().sum::<u64>() as f64 / memory_samples.len() as f64 / 1_048_576.0
} else {
0.0
};
LoadTestReport {
test_duration: elapsed,
total_requests: total,
successful_requests: successful,
failed_requests: failed,
throughput_per_sec: throughput,
error_rate_percent: error_rate,
latency_p50_us: p50,
latency_p95_us: p95,
latency_p99_us: p99,
latency_max_us: max,
avg_memory_mb,
}
}
}
#[derive(Debug, Clone)]
pub struct LoadTestReport {
pub test_duration: Duration,
pub total_requests: u64,
pub successful_requests: u64,
pub failed_requests: u64,
pub throughput_per_sec: f64,
pub error_rate_percent: f64,
pub latency_p50_us: u64,
pub latency_p95_us: u64,
pub latency_p99_us: u64,
pub latency_max_us: u64,
pub avg_memory_mb: f64,
}
impl LoadTestReport {
pub fn print(&self, test_name: &str) {
let separator = "=".repeat(80);
println!("\n{separator}");
println!("Load Test Report: {test_name}");
println!("{separator}");
let test_duration = self.test_duration;
let total_requests = self.total_requests;
let successful_requests = self.successful_requests;
let failed_requests = self.failed_requests;
let throughput_per_sec = self.throughput_per_sec;
let error_rate_percent = self.error_rate_percent;
let latency_p50_us = self.latency_p50_us;
let latency_p95_us = self.latency_p95_us;
let latency_p99_us = self.latency_p99_us;
let latency_max_us = self.latency_max_us;
let avg_memory_mb = self.avg_memory_mb;
println!("Duration: {test_duration:?}");
println!("Total Requests: {total_requests} (Success: {successful_requests}, Failed: {failed_requests})");
println!("Throughput: {throughput_per_sec:.2} req/sec");
println!("Error Rate: {error_rate_percent:.2}%");
println!("\nLatency (microseconds):");
println!(" P50: {latency_p50_us} μs");
println!(" P95: {latency_p95_us} μs");
println!(" P99: {latency_p99_us} μs");
println!(" Max: {latency_max_us} μs");
println!("\nMemory: {avg_memory_mb:.2} MB (avg)");
println!("{separator}\n");
}
}
/// Create authenticated gRPC client
async fn create_client() -> Result<TradingServiceClient<Channel>> {
let channel = Channel::from_static(TRADING_SERVICE_URL)
.connect()
.await
.context("Failed to connect to trading service")?;
Ok(TradingServiceClient::new(channel))
}
/// Submit a single order and measure latency
async fn submit_order(
client: &mut TradingServiceClient<Channel>,
symbol: String,
order_id: u64,
) -> Result<Duration> {
let start = Instant::now();
let request = SubmitOrderRequest {
symbol,
side: (OrderSide::Buy as i32),
quantity: 100.0,
order_type: (OrderType::Market as i32),
price: None,
stop_price: None,
account_id: TEST_ACCOUNT_ID.to_string(),
metadata: std::collections::HashMap::new(),
};
let _ = client.submit_order(request).await?;
Ok(start.elapsed())
}
/// Test 1: 10,000 orders/sec sustained load (60 seconds)
#[tokio::test]
#[ignore = "Run explicitly with --ignored"]
async fn test_sustained_load_10k_orders() -> Result<()> {
const TARGET_RPS: usize = 10_000;
const DURATION_SECS: u64 = 60;
const CONCURRENT_CLIENTS: usize = 100;
println!("\n🚀 Starting Sustained Load Test: 10,000 orders/sec for 60s");
let metrics = Arc::new(LoadTestMetrics::new());
let mut join_set = JoinSet::new();
// Spawn concurrent clients
for client_id in 0..CONCURRENT_CLIENTS {
let metrics = Arc::clone(&metrics);
join_set.spawn(async move {
let mut client = create_client().await?;
let start = Instant::now();
let mut order_count = 0u64;
while start.elapsed().as_secs() < DURATION_SECS {
let sym_id = client_id % 100;
let symbol = format!("SYM{sym_id:04}");
match submit_order(&mut client, symbol, order_count).await {
Ok(duration) => metrics.record_request(duration, true),
Err(e) => {
tracing::warn!("Order failed: {:?}", e);
metrics.record_request(Duration::from_micros(0), false);
},
}
order_count += 1;
// Rate limiting: each client sends ~100 orders/sec
tokio::time::sleep(Duration::from_micros(10_000)).await;
}
Ok::<_, anyhow::Error>(())
});
}
// Memory monitoring task
let metrics_clone = Arc::clone(&metrics);
let monitor_handle = tokio::spawn(async move {
let mut sys = System::new_with_specifics(
RefreshKind::everything().with_processes(ProcessRefreshKind::everything()),
);
for _ in 0..60 {
tokio::time::sleep(Duration::from_secs(1)).await;
sys.refresh_all();
if let Some(process) = sys.process(sysinfo::get_current_pid().unwrap()) {
metrics_clone.record_memory(process.memory());
}
}
});
// Wait for all clients
while let Some(result) = join_set.join_next().await {
if let Err(e) = result {
tracing::error!("Client task failed: {:?}", e);
}
}
monitor_handle.abort();
// Generate report
let report = metrics.report();
report.print("Sustained Load: 10,000 orders/sec");
// Assertions
assert!(
report.throughput_per_sec >= 9_000.0,
"Throughput too low: {:.2} req/sec (expected >= 9,000)",
report.throughput_per_sec
);
assert!(
report.error_rate_percent < 1.0,
"Error rate too high: {:.2}% (expected < 1%)",
report.error_rate_percent
);
assert!(
report.latency_p95_us < 10_000,
"P95 latency too high: {} μs (expected < 10ms)",
report.latency_p95_us
);
Ok(())
}
/// Test 2: 50,000 orders/sec peak burst (10 seconds)
#[tokio::test]
#[ignore = "Long-running test - run with --ignored"]
async fn test_peak_burst_50k_orders() -> Result<()> {
const TARGET_RPS: usize = 50_000;
const DURATION_SECS: u64 = 10;
const CONCURRENT_CLIENTS: usize = 500;
println!("\n🚀 Starting Peak Burst Test: 50,000 orders/sec for 10s");
let metrics = Arc::new(LoadTestMetrics::new());
let barrier = Arc::new(Barrier::new(CONCURRENT_CLIENTS));
let mut join_set = JoinSet::new();
// Spawn all clients at once for burst
for client_id in 0..CONCURRENT_CLIENTS {
let metrics = Arc::clone(&metrics);
let barrier = Arc::clone(&barrier);
join_set.spawn(async move {
let mut client = create_client().await?;
// Wait for all clients to be ready
barrier.wait().await;
let start = Instant::now();
let mut order_count = 0u64;
while start.elapsed().as_secs() < DURATION_SECS {
let burst_id = client_id % 100;
let symbol = format!("BURST{burst_id:04}");
match submit_order(&mut client, symbol, order_count).await {
Ok(duration) => metrics.record_request(duration, true),
Err(e) => {
tracing::warn!("Burst order failed: {:?}", e);
metrics.record_request(Duration::from_micros(0), false);
},
}
order_count += 1;
// Each client sends ~100 orders/sec (500 clients * 100 = 50k/sec)
tokio::time::sleep(Duration::from_micros(10_000)).await;
}
Ok::<_, anyhow::Error>(())
});
}
// Wait for all clients
while let Some(result) = join_set.join_next().await {
if let Err(e) = result {
tracing::error!("Burst client failed: {:?}", e);
}
}
let report = metrics.report();
report.print("Peak Burst: 50,000 orders/sec");
// Assertions (more lenient for burst)
assert!(
report.throughput_per_sec >= 40_000.0,
"Burst throughput too low: {:.2} req/sec (expected >= 40,000)",
report.throughput_per_sec
);
assert!(
report.error_rate_percent < 5.0,
"Burst error rate too high: {:.2}% (expected < 5%)",
report.error_rate_percent
);
Ok(())
}
/// Test 3: 1M concurrent market data updates (streaming)
#[tokio::test]
#[ignore = "Long-running test - run with --ignored"]
async fn test_1m_market_data_streaming() -> Result<()> {
const NUM_STREAMS: usize = 1000;
const UPDATES_PER_STREAM: usize = 1000;
const DURATION_SECS: u64 = 30;
println!("\n🚀 Starting Market Data Streaming: 1M updates across {NUM_STREAMS} streams");
let metrics = Arc::new(LoadTestMetrics::new());
let update_count = Arc::new(AtomicUsize::new(0));
let mut join_set = JoinSet::new();
for stream_id in 0..NUM_STREAMS {
let metrics = Arc::clone(&metrics);
let update_count = Arc::clone(&update_count);
join_set.spawn(async move {
let mut client = create_client().await?;
let stream_sym_id = stream_id % 100;
let symbols = vec![format!("STREAM{stream_sym_id:04}")];
let request = StreamMarketDataRequest {
symbols,
data_types: vec![], // Empty vec means subscribe to all data types
};
let start = Instant::now();
match client.stream_market_data(request).await {
Ok(response) => {
let mut stream = response.into_inner();
let mut count = 0;
while let Ok(Some(_event)) = stream.message().await {
count += 1;
update_count.fetch_add(1, Ordering::Relaxed);
if count >= UPDATES_PER_STREAM || start.elapsed().as_secs() >= DURATION_SECS
{
break;
}
}
metrics.record_request(start.elapsed(), true);
},
Err(e) => {
tracing::warn!("Stream {} failed: {:?}", stream_id, e);
metrics.record_request(start.elapsed(), false);
},
}
Ok::<_, anyhow::Error>(())
});
}
// Wait for all streams
while let Some(result) = join_set.join_next().await {
if let Err(e) = result {
tracing::error!("Stream task failed: {:?}", e);
}
}
let total_updates = update_count.load(Ordering::Relaxed);
let report = metrics.report();
println!("\n📊 Market Data Streaming Results:");
println!("Total Updates Received: {total_updates}");
println!("Active Streams: {NUM_STREAMS}");
report.print("Market Data Streaming");
// Assertions
assert!(
total_updates >= 900_000,
"Not enough updates received: {} (expected >= 900k)",
total_updates
);
Ok(())
}
/// Test 4: Connection pool saturation (1000 concurrent clients)
#[tokio::test]
#[ignore = "Long-running test - run with --ignored"]
async fn test_connection_pool_saturation() -> Result<()> {
const NUM_CLIENTS: usize = 1000;
const REQUESTS_PER_CLIENT: usize = 100;
println!("\n🚀 Starting Connection Pool Saturation: {NUM_CLIENTS} concurrent clients");
let metrics = Arc::new(LoadTestMetrics::new());
let barrier = Arc::new(Barrier::new(NUM_CLIENTS));
let mut join_set = JoinSet::new();
// Create all clients simultaneously
for client_id in 0..NUM_CLIENTS {
let metrics = Arc::clone(&metrics);
let barrier = Arc::clone(&barrier);
join_set.spawn(async move {
// Connect client
let connect_start = Instant::now();
let mut client = match create_client().await {
Ok(c) => {
metrics.record_request(connect_start.elapsed(), true);
c
},
Err(e) => {
tracing::error!("Client {} connection failed: {:?}", client_id, e);
metrics.record_request(connect_start.elapsed(), false);
return Ok::<_, anyhow::Error>(());
},
};
// Wait for all clients to connect
barrier.wait().await;
// Submit requests
for req_id in 0..REQUESTS_PER_CLIENT {
let pool_id = client_id % 100;
let symbol = format!("POOL{pool_id:04}");
match submit_order(&mut client, symbol, req_id as u64).await {
Ok(duration) => metrics.record_request(duration, true),
Err(e) => {
tracing::warn!("Client {} request {} failed: {:?}", client_id, req_id, e);
metrics.record_request(Duration::from_micros(0), false);
},
}
tokio::time::sleep(Duration::from_millis(10)).await;
}
Ok::<_, anyhow::Error>(())
});
}
// Wait for all clients
while let Some(result) = join_set.join_next().await {
if let Err(e) = result {
tracing::error!("Connection pool client failed: {:?}", e);
}
}
let report = metrics.report();
report.print("Connection Pool Saturation");
// Assertions
assert!(
report.error_rate_percent < 5.0,
"Connection pool error rate too high: {:.2}% (expected < 5%)",
report.error_rate_percent
);
assert!(
report.latency_p99_us < 100_000,
"P99 latency too high under pool saturation: {} μs (expected < 100ms)",
report.latency_p99_us
);
Ok(())
}
/// Integration test: Run all throughput tests sequentially
///
/// NOTE: Commented out because #[tokio::test] functions cannot be called directly.
///
/// To run all tests sequentially, use:
/// cargo test -p load_tests --release -- --ignored --nocapture --test-threads=1
/*
#[tokio::test]
#[ignore = "Long-running test - run with --ignored"]
async fn test_comprehensive_throughput_suite() -> Result<()> {
let separator = "=".repeat(80);
println!("\n{separator}");
println!("🎯 Comprehensive Throughput Validation Suite");
println!("{separator}\n");
// Test 1: Sustained load
println!("Test 1/4: Sustained Load");
test_sustained_load_10k_orders().await?;
// Cool down
tokio::time::sleep(Duration::from_secs(5)).await;
// Test 2: Peak burst
println!("\nTest 2/4: Peak Burst");
test_peak_burst_50k_orders().await?;
// Cool down
tokio::time::sleep(Duration::from_secs(5)).await;
// Test 3: Market data streaming
println!("\nTest 3/4: Market Data Streaming");
test_1m_market_data_streaming().await?;
// Cool down
tokio::time::sleep(Duration::from_secs(5)).await;
// Test 4: Connection pool
println!("\nTest 4/4: Connection Pool Saturation");
test_connection_pool_saturation().await?;
let separator = "=".repeat(80);
println!("\n{separator}");
println!("✅ All throughput tests completed successfully!");
println!("{separator}\n");
Ok(())
}
*/
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_metrics_initialization() {
let metrics = LoadTestMetrics::new();
assert_eq!(metrics.total_requests.load(Ordering::Relaxed), 0);
assert_eq!(metrics.successful_requests.load(Ordering::Relaxed), 0);
assert_eq!(metrics.failed_requests.load(Ordering::Relaxed), 0);
}
#[test]
fn test_metrics_recording() {
let metrics = LoadTestMetrics::new();
metrics.record_request(Duration::from_micros(100), true);
metrics.record_request(Duration::from_micros(200), false);
assert_eq!(metrics.total_requests.load(Ordering::Relaxed), 2);
assert_eq!(metrics.successful_requests.load(Ordering::Relaxed), 1);
assert_eq!(metrics.failed_requests.load(Ordering::Relaxed), 1);
let report = metrics.report();
assert_eq!(report.total_requests, 2);
assert_eq!(report.error_rate_percent, 50.0);
}
}