use std::collections::HashMap;
use std::fmt;
use std::hash::{Hash, Hasher};
use std::sync::Mutex;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct CodeHash(u64);
impl CodeHash {
#[must_use]
pub fn from_code(code: &str) -> Self {
use std::collections::hash_map::DefaultHasher;
let mut hasher = DefaultHasher::new();
code.hash(&mut hasher);
Self(hasher.finish())
}
#[must_use]
pub fn as_u64(self) -> u64 {
self.0
}
}
impl fmt::Display for CodeHash {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{:016x}", self.0)
}
}
#[derive(Debug, Clone)]
pub struct CacheConfig {
pub max_entries: usize,
pub max_total_size: usize,
}
impl Default for CacheConfig {
fn default() -> Self {
Self {
max_entries: 100,
max_total_size: 100 * 1024 * 1024, }
}
}
#[derive(Debug, Clone)]
struct CacheEntry {
binary: Vec<u8>,
last_access: u64,
}
#[derive(Debug)]
struct CacheInner {
entries: HashMap<CodeHash, CacheEntry>,
total_size: usize,
access_counter: u64,
}
#[derive(Debug)]
pub struct CompilationCache {
inner: Mutex<CacheInner>,
config: CacheConfig,
}
impl CompilationCache {
#[must_use]
pub fn new(config: CacheConfig) -> Self {
Self {
inner: Mutex::new(CacheInner {
entries: HashMap::new(),
total_size: 0,
access_counter: 0,
}),
config,
}
}
pub fn get(&self, hash: CodeHash) -> Option<Vec<u8>> {
let mut inner = self.inner.lock().ok()?;
if inner.entries.contains_key(&hash) {
inner.access_counter += 1;
let access = inner.access_counter;
if let Some(entry) = inner.entries.get_mut(&hash) {
entry.last_access = access;
return Some(entry.binary.clone());
}
}
None
}
pub fn insert(&self, hash: CodeHash, binary: Vec<u8>) {
let Ok(mut inner) = self.inner.lock() else {
return;
};
let binary_size = binary.len();
while inner.entries.len() >= self.config.max_entries
|| inner.total_size + binary_size > self.config.max_total_size
{
if !self.evict_lru(&mut inner) {
break;
}
}
inner.access_counter += 1;
let access = inner.access_counter;
if let Some(old_entry) = inner.entries.insert(
hash,
CacheEntry {
binary,
last_access: access,
},
) {
inner.total_size -= old_entry.binary.len();
}
inner.total_size += binary_size;
}
pub fn clear(&self) {
if let Ok(mut inner) = self.inner.lock() {
inner.entries.clear();
inner.total_size = 0;
}
}
#[must_use]
pub fn stats(&self) -> CacheStats {
let inner = self.inner.lock().ok();
match inner {
Some(inner) => CacheStats {
entry_count: inner.entries.len(),
total_size: inner.total_size,
max_entries: self.config.max_entries,
max_total_size: self.config.max_total_size,
},
None => CacheStats::default(),
}
}
fn evict_lru(&self, inner: &mut CacheInner) -> bool {
let lru_key = inner
.entries
.iter()
.min_by_key(|(_, entry)| entry.last_access)
.map(|(key, _)| *key);
if let Some(key) = lru_key {
if let Some(entry) = inner.entries.remove(&key) {
inner.total_size -= entry.binary.len();
return true;
}
}
false
}
}
impl Default for CompilationCache {
fn default() -> Self {
Self::new(CacheConfig::default())
}
}
#[derive(Debug, Clone, Default)]
pub struct CacheStats {
pub entry_count: usize,
pub total_size: usize,
pub max_entries: usize,
pub max_total_size: usize,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn code_hash_same_code_same_hash() {
let hash1 = CodeHash::from_code("fn test() {}");
let hash2 = CodeHash::from_code("fn test() {}");
assert_eq!(hash1, hash2);
}
#[test]
fn code_hash_different_code_different_hash() {
let hash1 = CodeHash::from_code("fn test1() {}");
let hash2 = CodeHash::from_code("fn test2() {}");
assert_ne!(hash1, hash2);
}
#[test]
fn code_hash_display() {
let hash = CodeHash::from_code("test");
let display = hash.to_string();
assert_eq!(display.len(), 16); }
#[test]
fn code_hash_as_u64() {
let hash = CodeHash::from_code("test");
let _value: u64 = hash.as_u64();
}
#[test]
fn code_hash_is_copy() {
let hash1 = CodeHash::from_code("test");
let hash2 = hash1; assert_eq!(hash1, hash2);
}
#[test]
fn cache_config_default() {
let config = CacheConfig::default();
assert_eq!(config.max_entries, 100);
assert_eq!(config.max_total_size, 100 * 1024 * 1024);
}
#[test]
fn cache_config_clone() {
let config1 = CacheConfig {
max_entries: 50,
max_total_size: 50 * 1024 * 1024,
};
let config2 = config1.clone();
assert_eq!(config1.max_entries, config2.max_entries);
}
#[test]
fn cache_get_missing_returns_none() {
let cache = CompilationCache::default();
let hash = CodeHash::from_code("test");
assert!(cache.get(hash).is_none());
}
#[test]
fn cache_insert_and_get() {
let cache = CompilationCache::default();
let hash = CodeHash::from_code("test");
let binary = vec![1, 2, 3, 4];
cache.insert(hash, binary.clone());
let retrieved = cache.get(hash);
assert_eq!(retrieved, Some(binary));
}
#[test]
fn cache_respects_max_entries() {
let config = CacheConfig {
max_entries: 2,
max_total_size: 1024 * 1024,
};
let cache = CompilationCache::new(config);
cache.insert(CodeHash::from_code("a"), vec![1]);
cache.insert(CodeHash::from_code("b"), vec![2]);
cache.insert(CodeHash::from_code("c"), vec![3]);
let stats = cache.stats();
assert!(stats.entry_count <= 2);
}
#[test]
fn cache_respects_max_size() {
let config = CacheConfig {
max_entries: 100,
max_total_size: 10, };
let cache = CompilationCache::new(config);
cache.insert(CodeHash::from_code("a"), vec![1, 2, 3, 4, 5]);
cache.insert(CodeHash::from_code("b"), vec![1, 2, 3, 4, 5]);
cache.insert(CodeHash::from_code("c"), vec![1, 2, 3, 4, 5]);
let stats = cache.stats();
assert!(stats.total_size <= 10);
}
#[test]
fn cache_clear() {
let cache = CompilationCache::default();
cache.insert(CodeHash::from_code("test"), vec![1, 2, 3]);
cache.clear();
assert_eq!(cache.stats().entry_count, 0);
assert_eq!(cache.stats().total_size, 0);
}
#[test]
fn cache_stats() {
let cache = CompilationCache::default();
cache.insert(CodeHash::from_code("test1"), vec![1, 2, 3]);
cache.insert(CodeHash::from_code("test2"), vec![4, 5]);
let stats = cache.stats();
assert_eq!(stats.entry_count, 2);
assert_eq!(stats.total_size, 5);
}
#[test]
fn cache_lru_eviction() {
let config = CacheConfig {
max_entries: 2,
max_total_size: 1024 * 1024,
};
let cache = CompilationCache::new(config);
let hash_a = CodeHash::from_code("a");
let hash_b = CodeHash::from_code("b");
let hash_c = CodeHash::from_code("c");
cache.insert(hash_a, vec![1]);
cache.insert(hash_b, vec![2]);
cache.get(hash_a);
cache.insert(hash_c, vec![3]);
assert!(cache.get(hash_a).is_some());
assert!(cache.get(hash_b).is_none()); assert!(cache.get(hash_c).is_some());
}
#[test]
fn cache_update_existing() {
let cache = CompilationCache::default();
let hash = CodeHash::from_code("test");
cache.insert(hash, vec![1, 2, 3]);
let stats_before = cache.stats();
cache.insert(hash, vec![4, 5]); let stats_after = cache.stats();
assert_eq!(stats_before.entry_count, 1);
assert_eq!(stats_after.entry_count, 1);
assert_eq!(stats_before.total_size, 3);
assert_eq!(stats_after.total_size, 2);
}
#[test]
fn cache_default_is_new_with_default_config() {
let cache1 = CompilationCache::default();
let cache2 = CompilationCache::new(CacheConfig::default());
let stats1 = cache1.stats();
let stats2 = cache2.stats();
assert_eq!(stats1.max_entries, stats2.max_entries);
assert_eq!(stats1.max_total_size, stats2.max_total_size);
}
#[test]
fn cache_stats_default() {
let stats = CacheStats::default();
assert_eq!(stats.entry_count, 0);
assert_eq!(stats.total_size, 0);
assert_eq!(stats.max_entries, 0);
assert_eq!(stats.max_total_size, 0);
}
}