anya_core/testing/performance/
cache.rs

1/// Cache performance testing
2use crate::testing::performance::{
3    MetricType, PerfTestError, PerformanceTestable, Result, TestConfig, TestResult, Timer,
4};
5use rand::{thread_rng, Rng};
6use rand_distr::{Distribution, Zipf};
7use std::collections::{HashMap, VecDeque};
8
9/// Cache algorithm
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum CacheAlgorithm {
12    /// Least Recently Used
13    LRU,
14
15    /// First In First Out
16    FIFO,
17
18    /// Random Replacement
19    Random,
20}
21
22/// Cache access pattern
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub enum AccessPattern {
25    /// Uniform random access
26    Uniform,
27
28    /// Zipfian distribution (skewed access)
29    Zipfian,
30
31    /// Sequential access
32    Sequential,
33
34    /// Repeated access to a small set
35    Repeated,
36}
37
38/// Cache configuration
39#[derive(Debug, Clone)]
40pub struct CacheConfig {
41    /// Cache size in items
42    pub size: usize,
43
44    /// Cache algorithm
45    pub algorithm: CacheAlgorithm,
46
47    /// Access pattern
48    pub access_pattern: AccessPattern,
49
50    /// Key space size
51    pub key_space_size: usize,
52
53    /// Zipfian parameter (if using Zipfian distribution)
54    pub zipf_param: f64,
55
56    /// Repeated set size (if using repeated pattern)
57    pub repeated_set_size: usize,
58}
59
60impl Default for CacheConfig {
61    fn default() -> Self {
62        Self {
63            size: 1000,
64            algorithm: CacheAlgorithm::LRU,
65            access_pattern: AccessPattern::Zipfian,
66            key_space_size: 10000,
67            zipf_param: 1.07, // Typical web traffic
68            repeated_set_size: 100,
69        }
70    }
71}
72
73/// Simple cache implementation
74#[derive(Debug)]
75pub struct SimpleCache<K, V> {
76    /// Cache algorithm
77    algorithm: CacheAlgorithm,
78
79    /// Maximum size
80    max_size: usize,
81
82    /// Current size
83    current_size: usize,
84
85    /// Data
86    data: HashMap<K, V>,
87
88    /// Access order for LRU
89    access_order: VecDeque<K>,
90
91    /// Insertion order for FIFO
92    insertion_order: VecDeque<K>,
93
94    /// Stats
95    stats: CacheStats,
96}
97
98/// Cache statistics
99#[derive(Debug, Default, Clone)]
100pub struct CacheStats {
101    /// Cache hits
102    pub hits: usize,
103
104    /// Cache misses
105    pub misses: usize,
106
107    /// Evictions
108    pub evictions: usize,
109
110    /// Insertions
111    pub insertions: usize,
112
113    /// Read operations
114    pub reads: usize,
115
116    /// Write operations
117    pub writes: usize,
118
119    /// Total time spent in read operations (ms)
120    pub read_time_ms: u64,
121
122    /// Total time spent in write operations (ms)
123    pub write_time_ms: u64,
124}
125
126impl<K: Clone + Eq + std::hash::Hash, V: Clone> SimpleCache<K, V> {
127    /// Create a new simple cache
128    pub fn new(algorithm: CacheAlgorithm, max_size: usize) -> Self {
129        Self {
130            algorithm,
131            max_size,
132            current_size: 0,
133            data: HashMap::new(),
134            access_order: VecDeque::new(),
135            insertion_order: VecDeque::new(),
136            stats: CacheStats::default(),
137        }
138    }
139
140    /// Get an item from the cache
141    pub fn get(&mut self, key: &K) -> Option<V> {
142        let mut timer = Timer::new();
143        timer.start();
144
145        let result = self.data.get(key).cloned();
146
147        if result.is_some() {
148            self.stats.hits += 1;
149
150            // Update access order for LRU
151            if self.algorithm == CacheAlgorithm::LRU {
152                // Remove from current position
153                if let Some(pos) = self.access_order.iter().position(|k| k == key) {
154                    self.access_order.remove(pos);
155                }
156                // Add to the end
157                self.access_order.push_back(key.clone());
158            }
159        } else {
160            self.stats.misses += 1;
161        }
162
163        self.stats.reads += 1;
164
165        timer.stop();
166        if let Ok(elapsed) = timer.elapsed_ms() {
167            self.stats.read_time_ms += elapsed;
168        }
169
170        result
171    }
172
173    /// Put an item in the cache
174    pub fn put(&mut self, key: K, value: V) {
175        let mut timer = Timer::new();
176        timer.start();
177
178        // Check if key already exists
179        let is_new = !self.data.contains_key(&key);
180
181        // Add to data
182        self.data.insert(key.clone(), value);
183
184        if is_new {
185            self.stats.insertions += 1;
186
187            // Update size
188            self.current_size += 1;
189
190            // Update insertion order for FIFO
191            self.insertion_order.push_back(key.clone());
192
193            // Update access order for LRU
194            if self.algorithm == CacheAlgorithm::LRU {
195                self.access_order.push_back(key);
196            }
197
198            // Evict if necessary
199            self.evict_if_needed();
200        } else {
201            // Update access order for LRU
202            if self.algorithm == CacheAlgorithm::LRU {
203                // Remove from current position
204                if let Some(pos) = self.access_order.iter().position(|k| k == &key) {
205                    self.access_order.remove(pos);
206                }
207                // Add to the end
208                self.access_order.push_back(key);
209            }
210        }
211
212        self.stats.writes += 1;
213
214        timer.stop();
215        if let Ok(elapsed) = timer.elapsed_ms() {
216            self.stats.write_time_ms += elapsed;
217        }
218    }
219
220    /// Evict an item from the cache if needed
221    fn evict_if_needed(&mut self) {
222        if self.current_size <= self.max_size {
223            return;
224        }
225
226        match self.algorithm {
227            CacheAlgorithm::LRU => {
228                // Evict least recently used
229                if let Some(key) = self.access_order.pop_front() {
230                    self.data.remove(&key);
231                    self.current_size -= 1;
232                    self.stats.evictions += 1;
233                }
234            }
235            CacheAlgorithm::FIFO => {
236                // Evict first in
237                if let Some(key) = self.insertion_order.pop_front() {
238                    self.data.remove(&key);
239                    self.current_size -= 1;
240                    self.stats.evictions += 1;
241                }
242            }
243            CacheAlgorithm::Random => {
244                // Evict random item
245                let mut rng = thread_rng();
246                if !self.data.is_empty() {
247                    let keys: Vec<K> = self.data.keys().cloned().collect();
248                    let idx = rng.gen_range(0..keys.len());
249                    let key = &keys[idx];
250                    self.data.remove(key);
251                    self.current_size -= 1;
252                    self.stats.evictions += 1;
253
254                    // Remove from access and insertion orders
255                    if let Some(pos) = self.access_order.iter().position(|k| k == key) {
256                        self.access_order.remove(pos);
257                    }
258                    if let Some(pos) = self.insertion_order.iter().position(|k| k == key) {
259                        self.insertion_order.remove(pos);
260                    }
261                }
262            }
263        }
264    }
265
266    /// Get stats
267    pub fn get_stats(&self) -> CacheStats {
268        self.stats.clone()
269    }
270
271    /// Reset stats
272    pub fn reset_stats(&mut self) {
273        self.stats = CacheStats::default();
274    }
275}
276
277/// Cache performance test
278pub struct CachePerformanceTest {
279    /// Cache configuration
280    config: CacheConfig,
281
282    /// Access keys (for sequential and repeated patterns)
283    keys: Vec<String>,
284}
285
286impl CachePerformanceTest {
287    /// Create a new cache performance test
288    pub fn new(config: CacheConfig) -> Self {
289        let mut keys = Vec::with_capacity(config.key_space_size);
290
291        for i in 0..config.key_space_size {
292            keys.push(format!("key_{i}"));
293        }
294
295        Self { config, keys }
296    }
297
298    /// Generate a key based on the access pattern
299    fn generate_key(&self, iteration: usize) -> Result<String> {
300        Ok(match self.config.access_pattern {
301            AccessPattern::Uniform => {
302                let mut rng = thread_rng();
303                let idx = rng.gen_range(0..self.config.key_space_size);
304                self.keys[idx].clone()
305            }
306            AccessPattern::Zipfian => {
307                let mut rng = thread_rng();
308                let zipf = Zipf::new(self.config.key_space_size as u64, self.config.zipf_param)
309                    .map_err(|_| {
310                        PerfTestError::ConfigurationError(
311                            "Failed to create Zipf distribution".to_string(),
312                        )
313                    })?;
314                let idx = zipf.sample(&mut rng) as usize - 1;
315                self.keys[idx].clone()
316            }
317            AccessPattern::Sequential => {
318                let idx = iteration % self.config.key_space_size;
319                self.keys[idx].clone()
320            }
321            AccessPattern::Repeated => {
322                let mut rng = thread_rng();
323                let set_size = self
324                    .config
325                    .repeated_set_size
326                    .min(self.config.key_space_size);
327                let idx = rng.gen_range(0..set_size);
328                self.keys[idx].clone()
329            }
330        })
331    }
332
333    /// Generate a random value
334    fn generate_value(&self) -> String {
335        let mut rng = thread_rng();
336        let size = rng.gen_range(10..100);
337        let mut value = String::with_capacity(size);
338
339        for _ in 0..size {
340            let c = rng.gen_range(0..26) as u8 + b'a';
341            value.push(c as char);
342        }
343
344        value
345    }
346
347    /// Run a test with a specific algorithm and access pattern
348    fn run_algorithm_test(&self, iterations: usize) -> Result<CacheStats> {
349        let mut cache = SimpleCache::<String, String>::new(self.config.algorithm, self.config.size);
350
351        // Run test iterations
352        for i in 0..iterations {
353            let key = self.generate_key(i)?;
354
355            // 80% of operations are reads, 20% are writes
356            let mut rng = thread_rng();
357            let is_read = rng.gen_range(0..100) < 80;
358
359            if is_read {
360                let _ = cache.get(&key);
361            } else {
362                let value = self.generate_value();
363                cache.put(key, value);
364            }
365        }
366
367        Ok(cache.get_stats())
368    }
369}
370
371impl PerformanceTestable for CachePerformanceTest {
372    fn run_test(&self, config: &TestConfig) -> Result<TestResult> {
373        let iterations = config.iterations;
374        let warmup_iterations = config.warmup_iterations;
375
376        // Parameters
377        let mut parameters = HashMap::new();
378        parameters.insert("cache_size".to_string(), self.config.size.to_string());
379        parameters.insert(
380            "algorithm".to_string(),
381            format!("{:?}", self.config.algorithm),
382        );
383        parameters.insert(
384            "access_pattern".to_string(),
385            format!("{:?}", self.config.access_pattern),
386        );
387        parameters.insert(
388            "key_space_size".to_string(),
389            self.config.key_space_size.to_string(),
390        );
391
392        if self.config.access_pattern == AccessPattern::Zipfian {
393            parameters.insert("zipf_param".to_string(), self.config.zipf_param.to_string());
394        }
395
396        if self.config.access_pattern == AccessPattern::Repeated {
397            parameters.insert(
398                "repeated_set_size".to_string(),
399                self.config.repeated_set_size.to_string(),
400            );
401        }
402
403        // Warmup (this initializes the system and JIT compiler)
404        println!("Warming up cache for {warmup_iterations} iterations...");
405        if warmup_iterations > 0 {
406            let _ = self.run_algorithm_test(warmup_iterations);
407        }
408
409        // Actual test
410        println!(
411            "Running cache test for {} iterations with {:?} algorithm and {:?} access pattern...",
412            iterations, self.config.algorithm, self.config.access_pattern
413        );
414
415        let mut timer = Timer::new();
416        timer.start();
417
418        let stats = self.run_algorithm_test(iterations)?;
419
420        timer.stop();
421
422        // Calculate results
423        let duration_ms = timer.elapsed_ms()?;
424
425        // Calculate metrics
426        let mut metrics = HashMap::new();
427        let mut metric_types = HashMap::new();
428
429        // Cache hit rate
430        let total_reads = stats.hits + stats.misses;
431        let cache_hit_rate = if total_reads > 0 {
432            (stats.hits as f64) / (total_reads as f64) * 100.0
433        } else {
434            0.0
435        };
436
437        metrics.insert("cache_hit_rate".to_string(), cache_hit_rate);
438        metric_types.insert("cache_hit_rate".to_string(), MetricType::CacheHitRate);
439
440        // Operations per second
441        let total_ops = stats.reads + stats.writes;
442        let ops_per_second = (total_ops as f64) / (duration_ms as f64 / 1000.0);
443
444        metrics.insert("operations_per_second".to_string(), ops_per_second);
445        metric_types.insert(
446            "operations_per_second".to_string(),
447            MetricType::DbOpsPerSecond,
448        );
449
450        // Average read latency
451        if stats.reads > 0 {
452            let avg_read_ms = (stats.read_time_ms as f64) / (stats.reads as f64);
453            metrics.insert("avg_read_latency_ms".to_string(), avg_read_ms);
454            metric_types.insert("avg_read_latency_ms".to_string(), MetricType::LatencyMs);
455        }
456
457        // Average write latency
458        if stats.writes > 0 {
459            let avg_write_ms = (stats.write_time_ms as f64) / (stats.writes as f64);
460            metrics.insert("avg_write_latency_ms".to_string(), avg_write_ms);
461            metric_types.insert("avg_write_latency_ms".to_string(), MetricType::LatencyMs);
462        }
463
464        // Eviction rate
465        let eviction_rate = (stats.evictions as f64) / (stats.insertions as f64) * 100.0;
466        metrics.insert("eviction_rate".to_string(), eviction_rate);
467        metric_types.insert("eviction_rate".to_string(), MetricType::CacheHitRate);
468
469        Ok(TestResult {
470            name: format!(
471                "{}_{:?}_{:?}",
472                self.name(),
473                self.config.algorithm,
474                self.config.access_pattern
475            ),
476            timestamp: chrono::Utc::now().to_rfc3339(),
477            duration_ms,
478            metrics,
479            metric_types,
480            parameters,
481        })
482    }
483
484    fn name(&self) -> &str {
485        "cache_performance"
486    }
487}
488
489/// Create a standard set of cache performance tests
490#[allow(clippy::vec_init_then_push)]
491pub fn create_standard_cache_tests() -> Vec<Box<dyn PerformanceTestable>> {
492    let mut tests: Vec<Box<dyn PerformanceTestable>> = Vec::new();
493
494    // LRU tests with different access patterns
495    tests.push(Box::new(CachePerformanceTest::new(CacheConfig {
496        algorithm: CacheAlgorithm::LRU,
497        access_pattern: AccessPattern::Uniform,
498        ..CacheConfig::default()
499    })) as Box<dyn PerformanceTestable>);
500
501    tests.push(Box::new(CachePerformanceTest::new(CacheConfig {
502        algorithm: CacheAlgorithm::LRU,
503        access_pattern: AccessPattern::Zipfian,
504        ..CacheConfig::default()
505    })) as Box<dyn PerformanceTestable>);
506
507    tests.push(Box::new(CachePerformanceTest::new(CacheConfig {
508        algorithm: CacheAlgorithm::LRU,
509        access_pattern: AccessPattern::Sequential,
510        ..CacheConfig::default()
511    })) as Box<dyn PerformanceTestable>);
512
513    tests.push(Box::new(CachePerformanceTest::new(CacheConfig {
514        algorithm: CacheAlgorithm::LRU,
515        access_pattern: AccessPattern::Repeated,
516        ..CacheConfig::default()
517    })) as Box<dyn PerformanceTestable>);
518
519    // FIFO tests with different access patterns
520    tests.push(Box::new(CachePerformanceTest::new(CacheConfig {
521        algorithm: CacheAlgorithm::FIFO,
522        access_pattern: AccessPattern::Uniform,
523        ..CacheConfig::default()
524    })) as Box<dyn PerformanceTestable>);
525
526    tests.push(Box::new(CachePerformanceTest::new(CacheConfig {
527        algorithm: CacheAlgorithm::FIFO,
528        access_pattern: AccessPattern::Zipfian,
529        ..CacheConfig::default()
530    })) as Box<dyn PerformanceTestable>);
531
532    // Random tests with different access patterns
533    tests.push(Box::new(CachePerformanceTest::new(CacheConfig {
534        algorithm: CacheAlgorithm::Random,
535        access_pattern: AccessPattern::Uniform,
536        ..CacheConfig::default()
537    })) as Box<dyn PerformanceTestable>);
538
539    tests.push(Box::new(CachePerformanceTest::new(CacheConfig {
540        algorithm: CacheAlgorithm::Random,
541        access_pattern: AccessPattern::Zipfian,
542        ..CacheConfig::default()
543    })) as Box<dyn PerformanceTestable>);
544
545    tests
546}