use crate::testing::performance::{
MetricType, PerfTestError, PerformanceTestable, Result, TestConfig, TestResult, Timer,
};
use rand::{thread_rng, Rng};
use rand_distr::{Distribution, Zipf};
use std::collections::{HashMap, VecDeque};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CacheAlgorithm {
LRU,
FIFO,
Random,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AccessPattern {
Uniform,
Zipfian,
Sequential,
Repeated,
}
#[derive(Debug, Clone)]
pub struct CacheConfig {
pub size: usize,
pub algorithm: CacheAlgorithm,
pub access_pattern: AccessPattern,
pub key_space_size: usize,
pub zipf_param: f64,
pub repeated_set_size: usize,
}
impl Default for CacheConfig {
fn default() -> Self {
Self {
size: 1000,
algorithm: CacheAlgorithm::LRU,
access_pattern: AccessPattern::Zipfian,
key_space_size: 10000,
zipf_param: 1.07, repeated_set_size: 100,
}
}
}
#[derive(Debug)]
pub struct SimpleCache<K, V> {
algorithm: CacheAlgorithm,
max_size: usize,
current_size: usize,
data: HashMap<K, V>,
access_order: VecDeque<K>,
insertion_order: VecDeque<K>,
stats: CacheStats,
}
#[derive(Debug, Default, Clone)]
pub struct CacheStats {
pub hits: usize,
pub misses: usize,
pub evictions: usize,
pub insertions: usize,
pub reads: usize,
pub writes: usize,
pub read_time_ms: u64,
pub write_time_ms: u64,
}
impl<K: Clone + Eq + std::hash::Hash, V: Clone> SimpleCache<K, V> {
pub fn new(algorithm: CacheAlgorithm, max_size: usize) -> Self {
Self {
algorithm,
max_size,
current_size: 0,
data: HashMap::new(),
access_order: VecDeque::new(),
insertion_order: VecDeque::new(),
stats: CacheStats::default(),
}
}
pub fn get(&mut self, key: &K) -> Option<V> {
let mut timer = Timer::new();
timer.start();
let result = self.data.get(key).cloned();
if result.is_some() {
self.stats.hits += 1;
if self.algorithm == CacheAlgorithm::LRU {
if let Some(pos) = self.access_order.iter().position(|k| k == key) {
self.access_order.remove(pos);
}
self.access_order.push_back(key.clone());
}
} else {
self.stats.misses += 1;
}
self.stats.reads += 1;
timer.stop();
if let Ok(elapsed) = timer.elapsed_ms() {
self.stats.read_time_ms += elapsed;
}
result
}
pub fn put(&mut self, key: K, value: V) {
let mut timer = Timer::new();
timer.start();
let is_new = !self.data.contains_key(&key);
self.data.insert(key.clone(), value);
if is_new {
self.stats.insertions += 1;
self.current_size += 1;
self.insertion_order.push_back(key.clone());
if self.algorithm == CacheAlgorithm::LRU {
self.access_order.push_back(key);
}
self.evict_if_needed();
} else {
if self.algorithm == CacheAlgorithm::LRU {
if let Some(pos) = self.access_order.iter().position(|k| k == &key) {
self.access_order.remove(pos);
}
self.access_order.push_back(key);
}
}
self.stats.writes += 1;
timer.stop();
if let Ok(elapsed) = timer.elapsed_ms() {
self.stats.write_time_ms += elapsed;
}
}
fn evict_if_needed(&mut self) {
if self.current_size <= self.max_size {
return;
}
match self.algorithm {
CacheAlgorithm::LRU => {
if let Some(key) = self.access_order.pop_front() {
self.data.remove(&key);
self.current_size -= 1;
self.stats.evictions += 1;
}
}
CacheAlgorithm::FIFO => {
if let Some(key) = self.insertion_order.pop_front() {
self.data.remove(&key);
self.current_size -= 1;
self.stats.evictions += 1;
}
}
CacheAlgorithm::Random => {
let mut rng = thread_rng();
if !self.data.is_empty() {
let keys: Vec<K> = self.data.keys().cloned().collect();
let idx = rng.gen_range(0..keys.len());
let key = &keys[idx];
self.data.remove(key);
self.current_size -= 1;
self.stats.evictions += 1;
if let Some(pos) = self.access_order.iter().position(|k| k == key) {
self.access_order.remove(pos);
}
if let Some(pos) = self.insertion_order.iter().position(|k| k == key) {
self.insertion_order.remove(pos);
}
}
}
}
}
pub fn get_stats(&self) -> CacheStats {
self.stats.clone()
}
pub fn reset_stats(&mut self) {
self.stats = CacheStats::default();
}
}
pub struct CachePerformanceTest {
config: CacheConfig,
keys: Vec<String>,
}
impl CachePerformanceTest {
pub fn new(config: CacheConfig) -> Self {
let mut keys = Vec::with_capacity(config.key_space_size);
for i in 0..config.key_space_size {
keys.push(format!("key_{i}"));
}
Self { config, keys }
}
fn generate_key(&self, iteration: usize) -> Result<String> {
Ok(match self.config.access_pattern {
AccessPattern::Uniform => {
let mut rng = thread_rng();
let idx = rng.gen_range(0..self.config.key_space_size);
self.keys[idx].clone()
}
AccessPattern::Zipfian => {
let mut rng = thread_rng();
let zipf = Zipf::new(self.config.key_space_size as u64, self.config.zipf_param)
.map_err(|_| {
PerfTestError::ConfigurationError(
"Failed to create Zipf distribution".to_string(),
)
})?;
let idx = zipf.sample(&mut rng) as usize - 1;
self.keys[idx].clone()
}
AccessPattern::Sequential => {
let idx = iteration % self.config.key_space_size;
self.keys[idx].clone()
}
AccessPattern::Repeated => {
let mut rng = thread_rng();
let set_size = self
.config
.repeated_set_size
.min(self.config.key_space_size);
let idx = rng.gen_range(0..set_size);
self.keys[idx].clone()
}
})
}
fn generate_value(&self) -> String {
let mut rng = thread_rng();
let size = rng.gen_range(10..100);
let mut value = String::with_capacity(size);
for _ in 0..size {
let c = rng.gen_range(0..26) as u8 + b'a';
value.push(c as char);
}
value
}
fn run_algorithm_test(&self, iterations: usize) -> Result<CacheStats> {
let mut cache = SimpleCache::<String, String>::new(self.config.algorithm, self.config.size);
for i in 0..iterations {
let key = self.generate_key(i)?;
let mut rng = thread_rng();
let is_read = rng.gen_range(0..100) < 80;
if is_read {
let _ = cache.get(&key);
} else {
let value = self.generate_value();
cache.put(key, value);
}
}
Ok(cache.get_stats())
}
}
impl PerformanceTestable for CachePerformanceTest {
fn run_test(&self, config: &TestConfig) -> Result<TestResult> {
let iterations = config.iterations;
let warmup_iterations = config.warmup_iterations;
let mut parameters = HashMap::new();
parameters.insert("cache_size".to_string(), self.config.size.to_string());
parameters.insert(
"algorithm".to_string(),
format!("{:?}", self.config.algorithm),
);
parameters.insert(
"access_pattern".to_string(),
format!("{:?}", self.config.access_pattern),
);
parameters.insert(
"key_space_size".to_string(),
self.config.key_space_size.to_string(),
);
if self.config.access_pattern == AccessPattern::Zipfian {
parameters.insert("zipf_param".to_string(), self.config.zipf_param.to_string());
}
if self.config.access_pattern == AccessPattern::Repeated {
parameters.insert(
"repeated_set_size".to_string(),
self.config.repeated_set_size.to_string(),
);
}
println!("Warming up cache for {warmup_iterations} iterations...");
if warmup_iterations > 0 {
let _ = self.run_algorithm_test(warmup_iterations);
}
println!(
"Running cache test for {} iterations with {:?} algorithm and {:?} access pattern...",
iterations, self.config.algorithm, self.config.access_pattern
);
let mut timer = Timer::new();
timer.start();
let stats = self.run_algorithm_test(iterations)?;
timer.stop();
let duration_ms = timer.elapsed_ms()?;
let mut metrics = HashMap::new();
let mut metric_types = HashMap::new();
let total_reads = stats.hits + stats.misses;
let cache_hit_rate = if total_reads > 0 {
(stats.hits as f64) / (total_reads as f64) * 100.0
} else {
0.0
};
metrics.insert("cache_hit_rate".to_string(), cache_hit_rate);
metric_types.insert("cache_hit_rate".to_string(), MetricType::CacheHitRate);
let total_ops = stats.reads + stats.writes;
let ops_per_second = (total_ops as f64) / (duration_ms as f64 / 1000.0);
metrics.insert("operations_per_second".to_string(), ops_per_second);
metric_types.insert(
"operations_per_second".to_string(),
MetricType::DbOpsPerSecond,
);
if stats.reads > 0 {
let avg_read_ms = (stats.read_time_ms as f64) / (stats.reads as f64);
metrics.insert("avg_read_latency_ms".to_string(), avg_read_ms);
metric_types.insert("avg_read_latency_ms".to_string(), MetricType::LatencyMs);
}
if stats.writes > 0 {
let avg_write_ms = (stats.write_time_ms as f64) / (stats.writes as f64);
metrics.insert("avg_write_latency_ms".to_string(), avg_write_ms);
metric_types.insert("avg_write_latency_ms".to_string(), MetricType::LatencyMs);
}
let eviction_rate = (stats.evictions as f64) / (stats.insertions as f64) * 100.0;
metrics.insert("eviction_rate".to_string(), eviction_rate);
metric_types.insert("eviction_rate".to_string(), MetricType::CacheHitRate);
Ok(TestResult {
name: format!(
"{}_{:?}_{:?}",
self.name(),
self.config.algorithm,
self.config.access_pattern
),
timestamp: chrono::Utc::now().to_rfc3339(),
duration_ms,
metrics,
metric_types,
parameters,
})
}
fn name(&self) -> &str {
"cache_performance"
}
}
#[allow(clippy::vec_init_then_push)]
pub fn create_standard_cache_tests() -> Vec<Box<dyn PerformanceTestable>> {
let mut tests: Vec<Box<dyn PerformanceTestable>> = Vec::new();
tests.push(Box::new(CachePerformanceTest::new(CacheConfig {
algorithm: CacheAlgorithm::LRU,
access_pattern: AccessPattern::Uniform,
..CacheConfig::default()
})) as Box<dyn PerformanceTestable>);
tests.push(Box::new(CachePerformanceTest::new(CacheConfig {
algorithm: CacheAlgorithm::LRU,
access_pattern: AccessPattern::Zipfian,
..CacheConfig::default()
})) as Box<dyn PerformanceTestable>);
tests.push(Box::new(CachePerformanceTest::new(CacheConfig {
algorithm: CacheAlgorithm::LRU,
access_pattern: AccessPattern::Sequential,
..CacheConfig::default()
})) as Box<dyn PerformanceTestable>);
tests.push(Box::new(CachePerformanceTest::new(CacheConfig {
algorithm: CacheAlgorithm::LRU,
access_pattern: AccessPattern::Repeated,
..CacheConfig::default()
})) as Box<dyn PerformanceTestable>);
tests.push(Box::new(CachePerformanceTest::new(CacheConfig {
algorithm: CacheAlgorithm::FIFO,
access_pattern: AccessPattern::Uniform,
..CacheConfig::default()
})) as Box<dyn PerformanceTestable>);
tests.push(Box::new(CachePerformanceTest::new(CacheConfig {
algorithm: CacheAlgorithm::FIFO,
access_pattern: AccessPattern::Zipfian,
..CacheConfig::default()
})) as Box<dyn PerformanceTestable>);
tests.push(Box::new(CachePerformanceTest::new(CacheConfig {
algorithm: CacheAlgorithm::Random,
access_pattern: AccessPattern::Uniform,
..CacheConfig::default()
})) as Box<dyn PerformanceTestable>);
tests.push(Box::new(CachePerformanceTest::new(CacheConfig {
algorithm: CacheAlgorithm::Random,
access_pattern: AccessPattern::Zipfian,
..CacheConfig::default()
})) as Box<dyn PerformanceTestable>);
tests
}