anya_core/testing/performance/
cache.rs1use 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum CacheAlgorithm {
12 LRU,
14
15 FIFO,
17
18 Random,
20}
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub enum AccessPattern {
25 Uniform,
27
28 Zipfian,
30
31 Sequential,
33
34 Repeated,
36}
37
38#[derive(Debug, Clone)]
40pub struct CacheConfig {
41 pub size: usize,
43
44 pub algorithm: CacheAlgorithm,
46
47 pub access_pattern: AccessPattern,
49
50 pub key_space_size: usize,
52
53 pub zipf_param: f64,
55
56 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, repeated_set_size: 100,
69 }
70 }
71}
72
73#[derive(Debug)]
75pub struct SimpleCache<K, V> {
76 algorithm: CacheAlgorithm,
78
79 max_size: usize,
81
82 current_size: usize,
84
85 data: HashMap<K, V>,
87
88 access_order: VecDeque<K>,
90
91 insertion_order: VecDeque<K>,
93
94 stats: CacheStats,
96}
97
98#[derive(Debug, Default, Clone)]
100pub struct CacheStats {
101 pub hits: usize,
103
104 pub misses: usize,
106
107 pub evictions: usize,
109
110 pub insertions: usize,
112
113 pub reads: usize,
115
116 pub writes: usize,
118
119 pub read_time_ms: u64,
121
122 pub write_time_ms: u64,
124}
125
126impl<K: Clone + Eq + std::hash::Hash, V: Clone> SimpleCache<K, V> {
127 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 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 if self.algorithm == CacheAlgorithm::LRU {
152 if let Some(pos) = self.access_order.iter().position(|k| k == key) {
154 self.access_order.remove(pos);
155 }
156 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 pub fn put(&mut self, key: K, value: V) {
175 let mut timer = Timer::new();
176 timer.start();
177
178 let is_new = !self.data.contains_key(&key);
180
181 self.data.insert(key.clone(), value);
183
184 if is_new {
185 self.stats.insertions += 1;
186
187 self.current_size += 1;
189
190 self.insertion_order.push_back(key.clone());
192
193 if self.algorithm == CacheAlgorithm::LRU {
195 self.access_order.push_back(key);
196 }
197
198 self.evict_if_needed();
200 } else {
201 if self.algorithm == CacheAlgorithm::LRU {
203 if let Some(pos) = self.access_order.iter().position(|k| k == &key) {
205 self.access_order.remove(pos);
206 }
207 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 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 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 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 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 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 pub fn get_stats(&self) -> CacheStats {
268 self.stats.clone()
269 }
270
271 pub fn reset_stats(&mut self) {
273 self.stats = CacheStats::default();
274 }
275}
276
277pub struct CachePerformanceTest {
279 config: CacheConfig,
281
282 keys: Vec<String>,
284}
285
286impl CachePerformanceTest {
287 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 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 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 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 for i in 0..iterations {
353 let key = self.generate_key(i)?;
354
355 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 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 println!("Warming up cache for {warmup_iterations} iterations...");
405 if warmup_iterations > 0 {
406 let _ = self.run_algorithm_test(warmup_iterations);
407 }
408
409 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 let duration_ms = timer.elapsed_ms()?;
424
425 let mut metrics = HashMap::new();
427 let mut metric_types = HashMap::new();
428
429 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 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 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 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 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#[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 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 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 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}