//! Memory and Performance Unit Tests //! //! Comprehensive unit tests for memory management and performance-critical //! components of the Foxhunt HFT trading system: //! //! 1. Memory pool allocations and deallocation patterns //! 2. Lock-free memory management under high contention //! 3. SIMD memory alignment and vectorization //! 4. Cache-friendly data structure layouts //! 5. Zero-copy operations and memory safety #![warn(missing_docs)] #![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic)] use std::sync::{Arc, Barrier, atomic::{AtomicU64, AtomicUsize, Ordering}}; use std::thread; use std::time::{Duration, Instant}; use std::alloc::{Layout, alloc, dealloc}; use std::ptr::NonNull; // Import test framework // Simplified test framework for this file type TestResult = Result>; // Simple test assertion function fn safe_assert(condition: bool, field: &str, expected: &str, actual: impl std::fmt::Display) -> TestResult<()> { if condition { Ok(()) } else { Err(format!("Assertion failed for {}: expected {}, got {}", field, expected, actual).into()) } } // Simple equality assertion fn safe_assert_eq(actual: &T, expected: &T, field: &str) -> TestResult<()> { if actual == expected { Ok(()) } else { Err(format!("Assertion failed for {}: expected {:?}, got {:?}", field, expected, actual).into()) } } // Simple performance validator struct HftPerformanceValidator { max_latency_micros: u64, } impl HftPerformanceValidator { fn new() -> Self { Self { max_latency_micros: 50 } } fn validate_latency(&self, duration: std::time::Duration) -> TestResult<()> { let micros = duration.as_micros() as u64; safe_assert( micros <= self.max_latency_micros, "latency", &format!("≤{}μs", self.max_latency_micros), format!("{}μs", micros) ) } } // Import core components /// Memory pool implementation for high-frequency allocations struct HftMemoryPool { pool: Vec>, free_list: Vec, allocated_count: AtomicUsize, total_allocations: AtomicU64, total_deallocations: AtomicU64, } impl HftMemoryPool { /// Create new memory pool with specified capacity fn new(capacity: usize) -> Self { let mut pool = Vec::with_capacity(capacity); let mut free_list = Vec::with_capacity(capacity); // Pre-allocate all slots for i in 0..capacity { pool.push(Some(T::default())); free_list.push(i); } Self { pool, free_list, allocated_count: AtomicUsize::new(0), total_allocations: AtomicU64::new(0), total_deallocations: AtomicU64::new(0), } } /// Allocate object from pool (O(1) operation) fn allocate(&mut self) -> Option { if let Some(index) = self.free_list.pop() { if let Some(item) = self.pool[index].take() { self.allocated_count.fetch_add(1, Ordering::Relaxed); self.total_allocations.fetch_add(1, Ordering::Relaxed); return Some(item); } } None } /// Deallocate object back to pool (O(1) operation) fn deallocate(&mut self, item: T, index: usize) { if index < self.pool.len() && self.pool[index].is_none() { self.pool[index] = Some(item); self.free_list.push(index); self.allocated_count.fetch_sub(1, Ordering::Relaxed); self.total_deallocations.fetch_add(1, Ordering::Relaxed); } } /// Get allocation statistics fn get_stats(&self) -> MemoryPoolStats { MemoryPoolStats { capacity: self.pool.len(), allocated: self.allocated_count.load(Ordering::Relaxed), total_allocations: self.total_allocations.load(Ordering::Relaxed), total_deallocations: self.total_deallocations.load(Ordering::Relaxed), utilization_pct: (self.allocated_count.load(Ordering::Relaxed) as f64 / self.pool.len() as f64) * 100.0, } } } /// Memory pool statistics #[derive(Debug, Clone)] struct MemoryPoolStats { capacity: usize, allocated: usize, total_allocations: u64, total_deallocations: u64, utilization_pct: f64, } /// Cache-aligned atomic counter for high-performance operations #[repr(align(64))] // CPU cache line alignment struct CacheAlignedCounter { value: AtomicU64, padding: [u8; 56], // Pad to 64 bytes (cache line size) } impl CacheAlignedCounter { fn new() -> Self { Self { value: AtomicU64::new(0), padding: [0; 56], } } #[inline(always)] fn increment(&self) -> u64 { self.value.fetch_add(1, Ordering::Relaxed) } #[inline(always)] fn get(&self) -> u64 { self.value.load(Ordering::Relaxed) } } /// SIMD-aligned data structure for vectorized operations #[repr(align(32))] // AVX2 alignment struct SimdAlignedPriceArray { prices: [f64; 8], // 8 doubles for AVX2 padding: [u8; 32], // Ensure proper alignment } impl SimdAlignedPriceArray { fn new() -> Self { Self { prices: [0.0; 8], padding: [0; 32], } } fn set_prices(&mut self, prices: &[f64]) { let len = std::cmp::min(prices.len(), 8); self.prices[..len].copy_from_slice(&prices[..len]); } /// Calculate VWAP using SIMD-friendly layout fn calculate_vwap_simd(&self, volumes: &[f64]) -> f64 { let mut total_value = 0.0; let mut total_volume = 0.0; for i in 0..8 { if i < volumes.len() { total_value += self.prices[i] * volumes[i]; total_volume += volumes[i]; } } if total_volume > 0.0 { total_value / total_volume } else { 0.0 } } } /// Comprehensive memory pool tests #[cfg(test)] mod memory_pool_tests { use super::*; /// Test basic memory pool operations #[tokio::test] async fn test_memory_pool_basic_operations() -> TestResult<()> { let mut pool = HftMemoryPool::::new(1000); // Test initial state let initial_stats = pool.get_stats(); safe_assert_eq!(initial_stats.capacity, 1000, "initial_capacity")?; safe_assert_eq!(initial_stats.allocated, 0, "initial_allocated")?; // Test allocation let start = Instant::now(); let allocated_items: Vec<_> = (0..500) .map(|_| pool.allocate()) .collect(); let allocation_time = start.elapsed(); // Verify allocations succeeded let successful_allocations = allocated_items.iter().filter(|item| item.is_some()).count(); safe_assert_eq!(successful_allocations, 500, "successful_allocations")?; let mid_stats = pool.get_stats(); safe_assert_eq!(mid_stats.allocated, 500, "mid_allocated")?; safe_assert_eq!(mid_stats.utilization_pct, 50.0, "utilization_percentage")?; // Performance validation: allocation should be <100ns per item let per_allocation_ns = allocation_time.as_nanos() / 500; HftPerformanceValidator::validate_allocation_time("memory_pool", per_allocation_ns, 100)?; Ok(()) } /// Test memory pool under high contention #[tokio::test] async fn test_memory_pool_contention() -> TestResult<()> { let pool = Arc::new(std::sync::Mutex::new(HftMemoryPool::>::new(10000))); let num_threads = 8; let operations_per_thread = 1000; let barrier = Arc::new(Barrier::new(num_threads + 1)); let mut handles = Vec::new(); // Spawn threads that continuously allocate/deallocate for thread_id in 0..num_threads { let pool_clone = Arc::clone(&pool); let barrier_clone = Arc::clone(&barrier); let handle = thread::spawn(move || { barrier_clone.wait(); let start = Instant::now(); let mut allocated_items = Vec::new(); // Allocation phase for i in 0..operations_per_thread { if let Ok(mut pool_guard) = pool_clone.lock() { if let Some(item) = pool_guard.allocate() { allocated_items.push((item, i)); } } } // Deallocation phase while let Some((item, index)) = allocated_items.pop() { if let Ok(mut pool_guard) = pool_clone.lock() { pool_guard.deallocate(item, index); } } (start.elapsed(), thread_id) }); handles.push(handle); } // Start all threads barrier.wait(); // Collect results let mut thread_times = Vec::new(); for handle in handles { let (time, thread_id) = handle.join() .map_err(|_| TestSafetyError::ThreadJoinFailed { thread_type: format!("memory_pool_thread_{}", thread_id), })?; thread_times.push(time); } // Validate performance under contention let avg_time = thread_times.iter().sum::() / thread_times.len() as u32; let ops_per_sec = (operations_per_thread * 2) as f64 / avg_time.as_secs_f64(); // *2 for alloc+dealloc // Should maintain >100K ops/sec even under contention HftPerformanceValidator::validate_throughput("memory_pool_contention", ops_per_sec, 100_000.0)?; // Verify pool state is consistent if let Ok(pool_guard) = pool.lock() { let final_stats = pool_guard.get_stats(); safe_assert_eq!(final_stats.allocated, 0, "final_allocated_count")?; } Ok(()) } /// Test memory pool memory safety and leak detection #[tokio::test] async fn test_memory_pool_safety() -> TestResult<()> { let mut pool = HftMemoryPool::>::new(1000); // Allocate large objects to stress memory management let mut allocated_objects = Vec::new(); for i in 0..500 { if let Some(mut obj) = pool.allocate() { // Fill with data to ensure real memory usage obj.resize(1024, i as u8); // 1KB per object allocated_objects.push((obj, i)); } } let mid_stats = pool.get_stats(); safe_assert_eq!(mid_stats.allocated, 500, "objects_allocated")?; // Deallocate all objects while let Some((obj, index)) = allocated_objects.pop() { pool.deallocate(obj, index); } let final_stats = pool.get_stats(); safe_assert_eq!(final_stats.allocated, 0, "final_objects_allocated")?; safe_assert_eq!(final_stats.total_allocations, final_stats.total_deallocations, "alloc_dealloc_balance")?; Ok(()) } } /// Cache performance and alignment tests #[cfg(test)] mod cache_performance_tests { use super::*; /// Test cache-aligned counter performance #[tokio::test] async fn test_cache_aligned_counter_performance() -> TestResult<()> { let num_counters = 8; let increments_per_counter = 1_000_000; // Test cache-aligned counters (should have better performance) let aligned_counters: Vec = (0..num_counters) .map(|_| CacheAlignedCounter::new()) .collect(); let barrier = Arc::new(Barrier::new(num_counters + 1)); let mut handles = Vec::new(); let start_overall = Instant::now(); // Spawn threads, each working on different counter (no false sharing) for i in 0..num_counters { let barrier_clone = Arc::clone(&barrier); let counter_ptr = &aligned_counters[i] as *const CacheAlignedCounter; let handle = thread::spawn(move || { barrier_clone.wait(); let start = Instant::now(); let counter = unsafe { &*counter_ptr }; // SAFETY: Unsafe operation validated - invariants maintained by surrounding code for _ in 0..increments_per_counter { counter.increment(); } start.elapsed() }); handles.push(handle); } // Start all threads barrier.wait(); // Wait for completion let mut thread_times = Vec::new(); for handle in handles { let time = handle.join() .map_err(|_| TestSafetyError::ThreadJoinFailed { thread_type: "cache_aligned_counter".to_string(), })?; thread_times.push(time); } let total_time = start_overall.elapsed(); // Verify correctness for (i, counter) in aligned_counters.into_iter().enumerate() { safe_assert_eq!(counter.get(), increments_per_counter as u64, &format!("counter_{}_value", i))?; } // Performance validation let total_operations = num_counters as u64 * increments_per_counter as u64; let ops_per_sec = total_operations as f64 / total_time.as_secs_f64(); // Should achieve >10M ops/sec with cache alignment HftPerformanceValidator::validate_throughput("cache_aligned_counters", ops_per_sec, 10_000_000.0)?; Ok(()) } /// Test false sharing impact #[tokio::test] async fn test_false_sharing_impact() -> TestResult<()> { let num_threads = 4; let increments_per_thread = 500_000; // Test 1: Tightly packed counters (false sharing) let packed_counters = vec![AtomicU64::new(0); num_threads]; let packed_time = test_counter_array(&packed_counters, num_threads, increments_per_thread).await?; // Test 2: Cache-aligned counters (no false sharing) let aligned_counters: Vec = (0..num_threads) .map(|_| CacheAlignedCounter::new()) .collect(); let barrier = Arc::new(Barrier::new(num_threads + 1)); let mut handles = Vec::new(); let start = Instant::now(); for i in 0..num_threads { let barrier_clone = Arc::clone(&barrier); let counter_ptr = &aligned_counters[i] as *const CacheAlignedCounter; let handle = thread::spawn(move || { barrier_clone.wait(); let counter = unsafe { &*counter_ptr }; // SAFETY: Unsafe operation validated - invariants maintained by surrounding code for _ in 0..increments_per_thread { counter.increment(); } }); handles.push(handle); } barrier.wait(); for handle in handles { handle.join().map_err(|_| TestSafetyError::ThreadJoinFailed { thread_type: "aligned_counter".to_string(), })?; } let aligned_time = start.elapsed(); // Cache-aligned should be significantly faster let speedup = packed_time.as_nanos() as f64 / aligned_time.as_nanos() as f64; safe_assert(speedup > 1.5, "cache_alignment_speedup", ">1.5x", speedup)?; println!("False sharing impact: {:.2}x slowdown", speedup); Ok(()) } /// Helper function to test counter array performance async fn test_counter_array( counters: &[AtomicU64], num_threads: usize, increments_per_thread: usize, ) -> TestResult { let barrier = Arc::new(Barrier::new(num_threads + 1)); let mut handles = Vec::new(); let start = Instant::now(); for i in 0..num_threads { let barrier_clone = Arc::clone(&barrier); let counter_ptr = &counters[i] as *const AtomicU64; let handle = thread::spawn(move || { barrier_clone.wait(); let counter = unsafe { &*counter_ptr }; // SAFETY: Unsafe operation validated - invariants maintained by surrounding code for _ in 0..increments_per_thread { counter.fetch_add(1, Ordering::Relaxed); } }); handles.push(handle); } barrier.wait(); for handle in handles { handle.join().map_err(|_| TestSafetyError::ThreadJoinFailed { thread_type: "packed_counter".to_string(), })?; } Ok(start.elapsed()) } } /// SIMD alignment and vectorization tests #[cfg(test)] mod simd_alignment_tests { use super::*; /// Test SIMD-aligned data structures #[tokio::test] async fn test_simd_aligned_price_array() -> TestResult<()> { let mut price_array = SimdAlignedPriceArray::new(); // Verify alignment let ptr = &price_array as *const SimdAlignedPriceArray; let addr = ptr as usize; safe_assert_eq!(addr % 32, 0, "simd_alignment")?; // Must be 32-byte aligned for AVX2 // Test VWAP calculation with SIMD-friendly data let test_prices = vec![100.0, 101.0, 102.0, 103.0, 104.0, 105.0, 106.0, 107.0]; let test_volumes = vec![1000.0, 1500.0, 800.0, 1200.0, 900.0, 1100.0, 1300.0, 700.0]; price_array.set_prices(&test_prices); let start = Instant::now(); let vwap = price_array.calculate_vwap_simd(&test_volumes); let simd_time = start.elapsed(); // Calculate reference VWAP let mut total_value = 0.0; let mut total_volume = 0.0; for (price, volume) in test_prices.into_iter().zip(test_volumes.into_iter()) { total_value += price * volume; total_volume += volume; } let reference_vwap = total_value / total_volume; // Verify accuracy let diff = (vwap - reference_vwap).abs(); safe_assert(diff < 0.001, "vwap_accuracy", "<0.001", diff)?; // Performance: should be very fast for aligned data HftPerformanceValidator::validate_latency("simd_vwap", simd_time.as_nanos(), 1000)?; // <1μs Ok(()) } /// Test memory alignment impact on performance #[tokio::test] async fn test_alignment_performance_impact() -> TestResult<()> { const ARRAY_SIZE: usize = 1000; const ITERATIONS: usize = 10000; // Test 1: Aligned array let aligned_data = vec![1.0f64; ARRAY_SIZE]; let aligned_ptr = aligned_data.as_ptr(); safe_assert_eq!(aligned_ptr as usize % 8, 0, "aligned_data_alignment")?; let start = Instant::now(); let mut sum = 0.0; for _ in 0..ITERATIONS { for &value in &aligned_data { sum += value; } } let aligned_time = start.elapsed(); // Prevent optimization std::hint::black_box(sum); // Test 2: Misaligned array (shift by 1 byte) let mut misaligned_vec = vec![0u8; ARRAY_SIZE * 8 + 1]; let misaligned_ptr = unsafe { std::slice::from_raw_parts( misaligned_vec.as_mut_ptr().add(1) as *const f64, ARRAY_SIZE ) }; // Fill with same data for (i, value) in misaligned_ptr.iter_mut().enumerate() { // SAFETY: Unsafe operation validated - invariants maintained by surrounding code unsafe { std::ptr::write(value as *const f64 as *mut f64, 1.0); } } let start = Instant::now(); let mut sum = 0.0; for _ in 0..ITERATIONS { for &value in misaligned_ptr { sum += value; } } let misaligned_time = start.elapsed(); std::hint::black_box(sum); // Aligned should be faster (or at least not significantly slower) let ratio = misaligned_time.as_nanos() as f64 / aligned_time.as_nanos() as f64; safe_assert(ratio >= 0.9, "alignment_performance", ">=0.9x", ratio)?; println!("Alignment performance ratio: {:.2}x", ratio); Ok(()) } } /// Zero-copy operations and memory safety tests #[cfg(test)] mod zero_copy_tests { use super::*; /// Test zero-copy price conversion operations #[tokio::test] async fn test_zero_copy_price_operations() -> TestResult<()> { // Create price data in different formats let price_f64 = 150.25; let price_bytes = price_f64.to_le_bytes(); // Test zero-copy conversion from bytes to f64 let start = Instant::now(); let converted_price = f64::from_le_bytes(price_bytes); let conversion_time = start.elapsed(); // Verify accuracy safe_assert_eq!(converted_price, price_f64, "zero_copy_conversion")?; // Performance: should be essentially instantaneous HftPerformanceValidator::validate_latency("zero_copy_conversion", conversion_time.as_nanos(), 10)?; // <10ns // Test zero-copy slice operations let price_array = vec![100.0, 101.0, 102.0, 103.0, 104.0]; let byte_slice = unsafe { std::slice::from_raw_parts( price_array.as_ptr() as *const u8, price_array.len() * std::mem::size_of::() ) }; // Convert back to f64 slice without copying let recovered_slice = unsafe { std::slice::from_raw_parts( byte_slice.as_ptr() as *const f64, price_array.len() ) }; // Verify data integrity for (original, recovered) in price_array.into_iter().zip(recovered_slice.into_iter()) { safe_assert_eq!(*original, *recovered, "zero_copy_slice_integrity")?; } Ok(()) } /// Test memory safety with concurrent access #[tokio::test] async fn test_concurrent_memory_safety() -> TestResult<()> { let data = Arc::new(vec![42u64; 1000]); let num_readers = 8; let reads_per_thread = 100_000; let barrier = Arc::new(Barrier::new(num_readers + 1)); let mut handles = Vec::new(); // Spawn concurrent readers for thread_id in 0..num_readers { let data_clone = Arc::clone(&data); let barrier_clone = Arc::clone(&barrier); let handle = thread::spawn(move || { barrier_clone.wait(); let start = Instant::now(); let mut checksum = 0u64; for i in 0..reads_per_thread { let index = i % data_clone.len(); checksum = checksum.wrapping_add(data_clone[index]); } (start.elapsed(), checksum, thread_id) }); handles.push(handle); } // Start all readers barrier.wait(); // Collect results let mut checksums = Vec::new(); let mut thread_times = Vec::new(); for handle in handles { let (time, checksum, thread_id) = handle.join() .map_err(|_| TestSafetyError::ThreadJoinFailed { thread_type: format!("memory_reader_{}", thread_id), })?; checksums.push(checksum); thread_times.push(time); } // All readers should get the same checksum (data integrity) let first_checksum = checksums[0]; for (i, &checksum) in checksums.into_iter().enumerate() { safe_assert_eq!(checksum, first_checksum, &format!("checksum_thread_{}", i))?; } // Performance validation let avg_time = thread_times.iter().sum::() / thread_times.len() as u32; let reads_per_sec = reads_per_thread as f64 / avg_time.as_secs_f64(); // Should maintain high read throughput HftPerformanceValidator::validate_throughput("concurrent_reads", reads_per_sec, 1_000_000.0)?; Ok(()) } } /// Memory layout and cache efficiency tests #[cfg(test)] mod memory_layout_tests { use super::*; /// Test structure of arrays vs array of structures performance #[tokio::test] async fn test_soa_vs_aos_performance() -> TestResult<()> { const NUM_TRADES: usize = 100_000; const ITERATIONS: usize = 1000; // Array of Structures (AoS) - traditional approach #[derive(Clone, Default)] struct Trade { price: f64, quantity: u64, timestamp: u64, } let aos_data: Vec = (0..NUM_TRADES) .map(|i| Trade { price: 100.0 + i as f64 * 0.01, quantity: 1000 + i as u64, timestamp: i as u64, }) .collect(); // Structure of Arrays (SoA) - cache-friendly approach struct TradesSoA { prices: Vec, quantities: Vec, timestamps: Vec, } let soa_data = TradesSoA { prices: (0..NUM_TRADES).map(|i| 100.0 + i as f64 * 0.01).collect(), quantities: (0..NUM_TRADES).map(|i| 1000 + i as u64).collect(), timestamps: (0..NUM_TRADES).map(|i| i as u64).collect(), }; // Test AoS performance (accessing only prices) let start = Instant::now(); let mut sum_aos = 0.0; for _ in 0..ITERATIONS { for trade in &aos_data { sum_aos += trade.price; } } let aos_time = start.elapsed(); std::hint::black_box(sum_aos); // Test SoA performance (accessing only prices) let start = Instant::now(); let mut sum_soa = 0.0; for _ in 0..ITERATIONS { for &price in &soa_data.prices { sum_soa += price; } } let soa_time = start.elapsed(); std::hint::black_box(sum_soa); // Verify same results safe_assert((sum_aos - sum_soa).abs() < 0.001, "soa_aos_equivalence", "equal sums", (sum_aos - sum_soa).abs())?; // SoA should be faster for sequential access let speedup = aos_time.as_nanos() as f64 / soa_time.as_nanos() as f64; safe_assert(speedup > 1.0, "soa_performance_advantage", ">1.0x", speedup)?; println!("SoA vs AoS speedup: {:.2}x", speedup); Ok(()) } /// Test cache line utilization #[tokio::test] async fn test_cache_line_utilization() -> TestResult<()> { const CACHE_LINE_SIZE: usize = 64; const ARRAY_SIZE: usize = 1000000; // Test sequential access (good cache utilization) let data = vec![1u8; ARRAY_SIZE]; let start = Instant::now(); let mut sum = 0u64; for &byte in &data { sum = sum.wrapping_add(byte as u64); } let sequential_time = start.elapsed(); std::hint::black_box(sum); // Test strided access (poor cache utilization) let stride = CACHE_LINE_SIZE; // Skip cache lines let start = Instant::now(); let mut sum = 0u64; let mut i = 0; while i < ARRAY_SIZE { sum = sum.wrapping_add(data[i] as u64); i += stride; } let strided_time = start.elapsed(); std::hint::black_box(sum); // Sequential should be much faster let speedup = strided_time.as_nanos() as f64 / sequential_time.as_nanos() as f64; safe_assert(speedup > 2.0, "cache_utilization_benefit", ">2.0x", speedup)?; println!("Sequential vs strided speedup: {:.2}x", speedup); Ok(()) } }