//! Wave D Memory Stress Test - 100K+ Symbol Simulation //! //! Agent D27: Validates memory scalability and leak detection for production-scale deployments. //! //! ## Test Scenario //! - Simulate 100,000 concurrent symbols (realistic for multi-exchange production) //! - Each symbol maintains a FeatureExtractionPipeline instance //! - Generate synthetic OHLCV data (1000 bars per symbol) //! - Run for 10,000 update cycles //! //! ## Memory Targets //! - Expected: 100K symbols × 4.6KB = 460MB //! - Maximum allowed: 500MB //! - No memory leaks: stable after initial allocation //! //! ## Success Criteria //! - ✅ Memory usage <500MB for 100K symbols //! - ✅ No memory leaks detected (stable RSS) //! - ✅ Linear scaling O(n) with symbol count //! - ✅ GC pressure minimal (<1% CPU) use chrono::Utc; use ml::features::extraction::OHLCVBar; use ml::features::pipeline::{FeatureConfig, FeatureExtractionPipeline}; use std::collections::HashMap; use std::time::{Duration, Instant}; use sysinfo::System; /// Memory checkpoint for tracking allocations #[derive(Debug, Clone)] struct MemoryCheckpoint { timestamp: Instant, symbol_count: usize, rss_bytes: u64, virtual_bytes: u64, available_bytes: u64, } impl MemoryCheckpoint { fn capture(sys: &System, symbol_count: usize, start: Instant) -> Self { let pid = sysinfo::get_current_pid().expect("Failed to get PID"); let process = sys.process(pid).expect("Process not found"); Self { timestamp: start, symbol_count, rss_bytes: process.memory(), virtual_bytes: process.virtual_memory(), available_bytes: sys.available_memory(), } } fn memory_per_symbol(&self) -> f64 { if self.symbol_count == 0 { 0.0 } else { self.rss_bytes as f64 / self.symbol_count as f64 } } } /// Generate synthetic OHLCV bar for testing fn generate_synthetic_bar(base_price: f64, index: usize) -> OHLCVBar { let open = base_price + (index as f64 * 0.1) % 10.0; let high = open + 0.5; let low = open - 0.5; let close = open + (index as f64 * 0.05) % 1.0; let volume = 1000.0 + (index as f64 * 10.0) % 500.0; OHLCVBar { timestamp: Utc::now(), open, high, low, close, volume, } } /// Stress test metrics #[derive(Debug)] struct StressTestMetrics { start_time: Instant, end_time: Instant, total_symbols: usize, total_updates: u64, checkpoints: Vec, warmup_duration: Duration, stress_duration: Duration, } impl StressTestMetrics { fn new() -> Self { let now = Instant::now(); Self { start_time: now, end_time: now, total_symbols: 0, total_updates: 0, checkpoints: Vec::new(), warmup_duration: Duration::ZERO, stress_duration: Duration::ZERO, } } fn memory_growth(&self) -> f64 { if self.checkpoints.len() < 2 { return 0.0; } let first = &self.checkpoints[0]; let last = &self.checkpoints[self.checkpoints.len() - 1]; ((last.rss_bytes as f64 - first.rss_bytes as f64) / first.rss_bytes as f64) * 100.0 } fn memory_leak_detected(&self) -> bool { // Check if memory grew >5% after initial allocation stabilized if self.checkpoints.len() < 4 { return false; } // Compare middle checkpoint (after warmup) to final checkpoint let mid_idx = self.checkpoints.len() / 2; let mid = &self.checkpoints[mid_idx]; let last = &self.checkpoints[self.checkpoints.len() - 1]; let growth = ((last.rss_bytes as f64 - mid.rss_bytes as f64) / mid.rss_bytes as f64) * 100.0; growth > 5.0 } fn print_summary(&self) { println!("\n{}", "=".repeat(80)); println!("Wave D Memory Stress Test - Summary"); println!("{}", "=".repeat(80)); println!("Total Symbols: {}", self.total_symbols); println!("Total Updates: {}", self.total_updates); println!("Warmup Duration: {:?}", self.warmup_duration); println!("Stress Duration: {:?}", self.stress_duration); println!("Total Duration: {:?}", self.end_time - self.start_time); println!("\nMemory Checkpoints:"); println!("{}", "-".repeat(80)); println!( "{:<15} {:<15} {:<15} {:<15}", "Symbols", "RSS (MB)", "Virtual (MB)", "Per Symbol (KB)" ); println!("{}", "-".repeat(80)); for checkpoint in &self.checkpoints { println!( "{:<15} {:<15.2} {:<15.2} {:<15.2}", checkpoint.symbol_count, checkpoint.rss_bytes as f64 / 1_048_576.0, checkpoint.virtual_bytes as f64 / 1_048_576.0, checkpoint.memory_per_symbol() / 1024.0 ); } println!("{}", "-".repeat(80)); println!("\nMemory Analysis:"); println!(" Memory Growth: {:.2}%", self.memory_growth()); println!( " Leak Detected: {}", if self.memory_leak_detected() { "❌ YES" } else { "✅ NO" } ); if let Some(last) = self.checkpoints.last() { let final_mb = last.rss_bytes as f64 / 1_048_576.0; let target_mb = 500.0; println!(" Final RSS: {:.2} MB", final_mb); println!(" Target: {:.2} MB", target_mb); println!( " Status: {}", if final_mb <= target_mb { "✅ PASS" } else { "❌ FAIL" } ); } println!("{}\n", "=".repeat(80)); } } #[test] #[ignore = "Expensive test - run explicitly with: cargo test wave_d_memory_stress_100k_symbols -- --ignored --nocapture"] fn wave_d_memory_stress_100k_symbols() { println!("\n🚀 Starting Wave D Memory Stress Test - 100K Symbols"); println!("Target: <500MB memory usage, no leaks, linear scaling\n"); let mut metrics = StressTestMetrics::new(); let mut sys = System::new_all(); sys.refresh_all(); // Capture baseline memory let baseline = MemoryCheckpoint::capture(&sys, 0, Instant::now()); metrics.checkpoints.push(baseline.clone()); println!( "📊 Baseline RSS: {:.2} MB", baseline.rss_bytes as f64 / 1_048_576.0 ); // Phase 1: Allocate 100K pipelines (simulating 100K symbols) const TOTAL_SYMBOLS: usize = 100_000; const CHECKPOINT_INTERVALS: [usize; 4] = [1_000, 10_000, 50_000, 100_000]; const WARMUP_BARS: usize = 50; println!( "\n🔧 Phase 1: Allocating {} FeatureExtractionPipeline instances...", TOTAL_SYMBOLS ); let phase1_start = Instant::now(); let config = FeatureConfig { enable_price: true, enable_volume: true, enable_time: true, enable_indicators: true, enable_microstructure: true, enable_statistical: true, warmup_bars: WARMUP_BARS, }; let mut pipelines: HashMap = HashMap::with_capacity(TOTAL_SYMBOLS); for i in 0..TOTAL_SYMBOLS { let symbol = format!("SYM{:06}", i); let pipeline = FeatureExtractionPipeline::with_config(config.clone()); pipelines.insert(symbol, pipeline); // Memory checkpoints if CHECKPOINT_INTERVALS.contains(&(i + 1)) { sys.refresh_all(); let checkpoint = MemoryCheckpoint::capture(&sys, i + 1, phase1_start); metrics.checkpoints.push(checkpoint.clone()); println!( " ✓ {} symbols allocated: RSS {:.2} MB ({:.2} KB/symbol)", checkpoint.symbol_count, checkpoint.rss_bytes as f64 / 1_048_576.0, checkpoint.memory_per_symbol() / 1024.0 ); } // Progress indicator every 10K if (i + 1) % 10_000 == 0 && !CHECKPOINT_INTERVALS.contains(&(i + 1)) { println!(" ... {} symbols allocated", i + 1); } } metrics.total_symbols = TOTAL_SYMBOLS; metrics.warmup_duration = phase1_start.elapsed(); println!( "✓ Phase 1 Complete: {} symbols in {:?}", TOTAL_SYMBOLS, metrics.warmup_duration ); // Phase 2: Warmup (feed 50 bars to each pipeline) println!( "\n🔥 Phase 2: Warming up pipelines ({} bars per symbol)...", WARMUP_BARS ); let phase2_start = Instant::now(); for (symbol, pipeline) in pipelines.iter_mut() { let base_price = 100.0 + (symbol.chars().last().unwrap() as u32 as f64); for bar_idx in 0..WARMUP_BARS { let bar = generate_synthetic_bar(base_price, bar_idx); pipeline.update(&bar); } } let warmup_elapsed = phase2_start.elapsed(); println!( "✓ Phase 2 Complete: Warmup finished in {:?}", warmup_elapsed ); // Capture post-warmup memory sys.refresh_all(); let post_warmup = MemoryCheckpoint::capture(&sys, TOTAL_SYMBOLS, phase2_start); metrics.checkpoints.push(post_warmup.clone()); println!( " RSS after warmup: {:.2} MB ({:.2} KB/symbol)", post_warmup.rss_bytes as f64 / 1_048_576.0, post_warmup.memory_per_symbol() / 1024.0 ); // Phase 3: Stress test (10K update cycles on all symbols) const UPDATE_CYCLES: usize = 10_000; const CYCLE_CHECKPOINTS: [usize; 5] = [1_000, 2_500, 5_000, 7_500, 10_000]; println!( "\n💪 Phase 3: Stress testing with {} update cycles...", UPDATE_CYCLES ); let phase3_start = Instant::now(); for cycle in 0..UPDATE_CYCLES { // Update all pipelines in this cycle for (symbol, pipeline) in pipelines.iter_mut() { let base_price = 100.0 + (symbol.chars().last().unwrap() as u32 as f64); let bar = generate_synthetic_bar(base_price, WARMUP_BARS + cycle); pipeline.update(&bar); metrics.total_updates += 1; } // Memory checkpoints if CYCLE_CHECKPOINTS.contains(&(cycle + 1)) { sys.refresh_all(); let checkpoint = MemoryCheckpoint::capture(&sys, TOTAL_SYMBOLS, phase3_start); metrics.checkpoints.push(checkpoint.clone()); println!( " ✓ Cycle {}/{}: RSS {:.2} MB ({:.2} KB/symbol)", cycle + 1, UPDATE_CYCLES, checkpoint.rss_bytes as f64 / 1_048_576.0, checkpoint.memory_per_symbol() / 1024.0 ); } // Progress indicator every 1000 cycles if (cycle + 1) % 1_000 == 0 && !CYCLE_CHECKPOINTS.contains(&(cycle + 1)) { println!(" ... Cycle {}/{}", cycle + 1, UPDATE_CYCLES); } } metrics.stress_duration = phase3_start.elapsed(); metrics.end_time = Instant::now(); println!( "✓ Phase 3 Complete: {} update cycles in {:?}", UPDATE_CYCLES, metrics.stress_duration ); // Final memory capture sys.refresh_all(); let final_checkpoint = MemoryCheckpoint::capture(&sys, TOTAL_SYMBOLS, phase3_start); metrics.checkpoints.push(final_checkpoint.clone()); // Print comprehensive summary metrics.print_summary(); // Assertions let final_mb = final_checkpoint.rss_bytes as f64 / 1_048_576.0; assert!( final_mb <= 500.0, "Memory usage exceeded 500MB target: {:.2} MB", final_mb ); assert!( !metrics.memory_leak_detected(), "Memory leak detected: RSS grew >5% after warmup stabilization" ); // Check linear scaling (memory per symbol should be consistent) if metrics.checkpoints.len() >= 3 { let first = &metrics.checkpoints[1]; // After 1K symbols let last = &metrics.checkpoints[metrics.checkpoints.len() - 1]; let first_per_symbol = first.memory_per_symbol(); let last_per_symbol = last.memory_per_symbol(); let variance_pct = ((last_per_symbol - first_per_symbol).abs() / first_per_symbol) * 100.0; println!( "Linear Scaling Check: {:.2} KB/symbol (1K) vs {:.2} KB/symbol (100K) = {:.2}% variance", first_per_symbol / 1024.0, last_per_symbol / 1024.0, variance_pct ); assert!( variance_pct < 20.0, "Memory per symbol variance too high: {:.2}% (expected <20%)", variance_pct ); } println!("\n✅ Wave D Memory Stress Test: PASSED"); println!(" - Memory usage: {:.2} MB / 500 MB target", final_mb); println!(" - No memory leaks detected"); println!(" - Linear scaling confirmed"); } #[test] fn wave_d_memory_scaling_small() { // Quick test with 1K symbols for CI/CD (non-ignored) println!("\n🧪 Wave D Memory Scaling Test - 1K Symbols (Quick)"); let mut sys = System::new_all(); sys.refresh_all(); let baseline = MemoryCheckpoint::capture(&sys, 0, Instant::now()); println!( "Baseline RSS: {:.2} MB", baseline.rss_bytes as f64 / 1_048_576.0 ); const SYMBOLS: usize = 1_000; let config = FeatureConfig::default(); let mut pipelines: Vec = Vec::with_capacity(SYMBOLS); // Allocate 1K pipelines for _ in 0..SYMBOLS { pipelines.push(FeatureExtractionPipeline::with_config(config.clone())); } // Warmup each pipeline for (i, pipeline) in pipelines.iter_mut().enumerate() { let base_price = 100.0 + i as f64; for bar_idx in 0..50 { let bar = generate_synthetic_bar(base_price, bar_idx); pipeline.update(&bar); } } sys.refresh_all(); let after = MemoryCheckpoint::capture(&sys, SYMBOLS, Instant::now()); // Calculate delta memory (excluding baseline process overhead) let delta_mb = (after.rss_bytes as i64 - baseline.rss_bytes as i64) as f64 / 1_048_576.0; let per_symbol_kb = (delta_mb * 1024.0) / SYMBOLS as f64; println!( "After 1K symbols: Total {:.2} MB, Delta {:.2} MB ({:.2} KB/symbol)", after.rss_bytes as f64 / 1_048_576.0, delta_mb, per_symbol_kb ); // For 1K symbols, expect <50MB delta (50KB per symbol including overhead) assert!( delta_mb <= 50.0, "Memory delta too high for 1K symbols: {:.2} MB (expected <50MB)", delta_mb ); // Verify each symbol uses reasonable memory (expected ~4.6KB, allow up to 50KB with overhead) assert!( per_symbol_kb <= 50.0, "Memory per symbol too high: {:.2} KB (expected <50KB)", per_symbol_kb ); println!("✅ Small-scale memory test: PASSED"); }