use std::collections::HashMap;
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::Instant;
use parking_lot::RwLock;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CacheConfig {
pub vram_capacity: u64,
pub ram_capacity: u64,
}
impl Default for CacheConfig {
fn default() -> Self {
Self {
vram_capacity: 10 * 1024 * 1024 * 1024, ram_capacity: 32 * 1024 * 1024 * 1024, }
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum CacheTier {
Vram,
Ram,
None,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct CacheStats {
pub vram_used: u64,
pub ram_used: u64,
pub hits: u64,
pub misses: u64,
pub vram_evictions: u64,
pub ram_evictions: u64,
pub fragments_cached: u64,
}
impl CacheStats {
pub fn hit_rate(&self) -> f64 {
let total = self.hits + self.misses;
if total == 0 {
return 0.0;
}
self.hits as f64 / total as f64
}
pub fn vram_utilization(&self, capacity: u64) -> f64 {
if capacity == 0 {
return 0.0;
}
self.vram_used as f64 / capacity as f64
}
pub fn ram_utilization(&self, capacity: u64) -> f64 {
if capacity == 0 {
return 0.0;
}
self.ram_used as f64 / capacity as f64
}
}
#[derive(Debug, Clone)]
struct CacheEntry {
_fragment_id: String,
size: u64,
tier: CacheTier,
last_access: Instant,
access_count: u64,
users: FragmentUsers,
}
#[derive(Debug, Clone, Copy, Default)]
struct FragmentUsers {
infernum: bool,
dantalion: bool,
}
impl FragmentUsers {
fn count(&self) -> u32 {
self.infernum as u32 + self.dantalion as u32
}
}
pub struct FragmentCache {
config: CacheConfig,
entries: RwLock<HashMap<String, CacheEntry>>,
vram_used: AtomicU64,
ram_used: AtomicU64,
hits: AtomicU64,
misses: AtomicU64,
vram_evictions: AtomicU64,
ram_evictions: AtomicU64,
}
impl FragmentCache {
pub fn new(config: CacheConfig) -> Self {
Self {
config,
entries: RwLock::new(HashMap::new()),
vram_used: AtomicU64::new(0),
ram_used: AtomicU64::new(0),
hits: AtomicU64::new(0),
misses: AtomicU64::new(0),
vram_evictions: AtomicU64::new(0),
ram_evictions: AtomicU64::new(0),
}
}
pub fn config(&self) -> &CacheConfig {
&self.config
}
pub fn contains(&self, fragment_id: &str) -> bool {
self.entries.read().contains_key(fragment_id)
}
pub fn get_tier(&self, fragment_id: &str) -> CacheTier {
self.entries
.read()
.get(fragment_id)
.map(|e| e.tier)
.unwrap_or(CacheTier::None)
}
pub fn access(&self, fragment_id: &str) -> CacheTier {
let mut entries = self.entries.write();
if let Some(entry) = entries.get_mut(fragment_id) {
entry.last_access = Instant::now();
entry.access_count += 1;
self.hits.fetch_add(1, Ordering::Relaxed);
entry.tier
} else {
self.misses.fetch_add(1, Ordering::Relaxed);
CacheTier::None
}
}
pub fn insert(
&self,
fragment_id: impl Into<String>,
size: u64,
tier: CacheTier,
for_infernum: bool,
) {
let fragment_id = fragment_id.into();
self.ensure_capacity(size, tier);
let entry = CacheEntry {
_fragment_id: fragment_id.clone(),
size,
tier,
last_access: Instant::now(),
access_count: 1,
users: FragmentUsers {
infernum: for_infernum,
dantalion: !for_infernum,
},
};
match tier {
CacheTier::Vram => {
self.vram_used.fetch_add(size, Ordering::Relaxed);
},
CacheTier::Ram => {
self.ram_used.fetch_add(size, Ordering::Relaxed);
},
CacheTier::None => {},
}
self.entries.write().insert(fragment_id, entry);
}
pub fn remove(&self, fragment_id: &str) {
let mut entries = self.entries.write();
if let Some(entry) = entries.remove(fragment_id) {
match entry.tier {
CacheTier::Vram => {
self.vram_used.fetch_sub(entry.size, Ordering::Relaxed);
},
CacheTier::Ram => {
self.ram_used.fetch_sub(entry.size, Ordering::Relaxed);
},
CacheTier::None => {},
}
}
}
pub fn promote(&self, fragment_id: &str, to_tier: CacheTier) {
let mut entries = self.entries.write();
if let Some(entry) = entries.get_mut(fragment_id) {
let from_tier = entry.tier;
if to_tier == from_tier {
return;
}
match from_tier {
CacheTier::Vram => {
self.vram_used.fetch_sub(entry.size, Ordering::Relaxed);
},
CacheTier::Ram => {
self.ram_used.fetch_sub(entry.size, Ordering::Relaxed);
},
CacheTier::None => {},
}
match to_tier {
CacheTier::Vram => {
self.vram_used.fetch_add(entry.size, Ordering::Relaxed);
},
CacheTier::Ram => {
self.ram_used.fetch_add(entry.size, Ordering::Relaxed);
},
CacheTier::None => {},
}
entry.tier = to_tier;
}
}
pub fn demote(&self, fragment_id: &str, to_tier: CacheTier) {
self.promote(fragment_id, to_tier);
}
pub fn mark_shared(&self, fragment_id: &str) {
let mut entries = self.entries.write();
if let Some(entry) = entries.get_mut(fragment_id) {
entry.users.infernum = true;
entry.users.dantalion = true;
}
}
pub fn stats(&self) -> CacheStats {
CacheStats {
vram_used: self.vram_used.load(Ordering::Relaxed),
ram_used: self.ram_used.load(Ordering::Relaxed),
hits: self.hits.load(Ordering::Relaxed),
misses: self.misses.load(Ordering::Relaxed),
vram_evictions: self.vram_evictions.load(Ordering::Relaxed),
ram_evictions: self.ram_evictions.load(Ordering::Relaxed),
fragments_cached: self.entries.read().len() as u64,
}
}
pub fn vram_used(&self) -> u64 {
self.vram_used.load(Ordering::Relaxed)
}
pub fn ram_used(&self) -> u64 {
self.ram_used.load(Ordering::Relaxed)
}
pub fn clear(&self) {
self.entries.write().clear();
self.vram_used.store(0, Ordering::Relaxed);
self.ram_used.store(0, Ordering::Relaxed);
}
fn ensure_capacity(&self, size: u64, tier: CacheTier) {
let (capacity, used) = match tier {
CacheTier::Vram => (
self.config.vram_capacity,
self.vram_used.load(Ordering::Relaxed),
),
CacheTier::Ram => (
self.config.ram_capacity,
self.ram_used.load(Ordering::Relaxed),
),
CacheTier::None => return,
};
if used + size <= capacity {
return;
}
let needed = used + size - capacity;
self.evict_lru(tier, needed);
}
fn evict_lru(&self, tier: CacheTier, needed: u64) {
let mut entries = self.entries.write();
let mut candidates: Vec<_> = entries
.iter()
.filter(|(_, e)| e.tier == tier)
.map(|(id, e)| (id.clone(), e.last_access, e.size, e.users.count()))
.collect();
candidates.sort_by(|a, b| a.3.cmp(&b.3).then(a.1.cmp(&b.1)));
let mut freed = 0u64;
for (id, _, _size, _) in candidates {
if freed >= needed {
break;
}
if let Some(entry) = entries.remove(&id) {
freed += entry.size;
match tier {
CacheTier::Vram => {
self.vram_used.fetch_sub(entry.size, Ordering::Relaxed);
self.vram_evictions.fetch_add(1, Ordering::Relaxed);
},
CacheTier::Ram => {
self.ram_used.fetch_sub(entry.size, Ordering::Relaxed);
self.ram_evictions.fetch_add(1, Ordering::Relaxed);
},
CacheTier::None => {},
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_cache_insert_and_access() {
let cache = FragmentCache::new(CacheConfig {
vram_capacity: 1000,
ram_capacity: 1000,
});
cache.insert("frag1", 100, CacheTier::Vram, true);
assert!(cache.contains("frag1"));
assert_eq!(cache.get_tier("frag1"), CacheTier::Vram);
let tier = cache.access("frag1");
assert_eq!(tier, CacheTier::Vram);
let stats = cache.stats();
assert_eq!(stats.hits, 1);
assert_eq!(stats.vram_used, 100);
}
#[test]
fn test_cache_miss() {
let cache = FragmentCache::new(CacheConfig::default());
let tier = cache.access("nonexistent");
assert_eq!(tier, CacheTier::None);
let stats = cache.stats();
assert_eq!(stats.misses, 1);
}
#[test]
fn test_cache_eviction() {
let cache = FragmentCache::new(CacheConfig {
vram_capacity: 200,
ram_capacity: 1000,
});
cache.insert("frag1", 100, CacheTier::Vram, true);
cache.insert("frag2", 100, CacheTier::Vram, true);
cache.insert("frag3", 100, CacheTier::Vram, true);
let stats = cache.stats();
assert!(stats.vram_evictions >= 1);
assert!(stats.vram_used <= 200);
}
#[test]
fn test_cache_promote_demote() {
let cache = FragmentCache::new(CacheConfig {
vram_capacity: 1000,
ram_capacity: 1000,
});
cache.insert("frag1", 100, CacheTier::Ram, true);
assert_eq!(cache.ram_used(), 100);
assert_eq!(cache.vram_used(), 0);
cache.promote("frag1", CacheTier::Vram);
assert_eq!(cache.ram_used(), 0);
assert_eq!(cache.vram_used(), 100);
assert_eq!(cache.get_tier("frag1"), CacheTier::Vram);
}
#[test]
fn test_hit_rate() {
let stats = CacheStats {
hits: 80,
misses: 20,
..Default::default()
};
assert!((stats.hit_rate() - 0.8).abs() < 0.001);
}
}