use std::collections::{BTreeMap, HashMap};
use std::io::{Read, Write};
use std::sync::{Arc, Mutex, RwLock};
use std::fs::File;
use std::time::{Duration, Instant};
use std::path::PathBuf;
use std::hash::{Hash, Hasher, DefaultHasher};
#[derive(Debug, Clone)]
struct L3Entry {
file_path: PathBuf,
file_size: usize,
last_accessed: Instant,
access_count: u32,
is_mapped: bool,
}
#[derive(Debug, Clone, Default)]
pub struct L3Stats {
pub hits: u64,
pub misses: u64,
pub evictions: u64,
pub memory_mapped_files: usize,
pub disk_files: usize,
pub total_memory_bytes: usize,
}
impl L3Stats {
pub fn hit_rate(&self) -> f64 {
let total: _ = self.hits + self.misses;
if total > 0 {
self.hits as f64 / total as f64
} else {
0.0
}
}
pub fn memory_usage_mb(&self) -> f64 {
self.total_memory_bytes as f64 / (1024.0 * 1024.0)
}
}
pub struct L3MmapCache {
entries: Arc<RwLock<HashMap<String, L3Entry>>>,
stats: Arc<RwLock<L3Stats>>,
cache_dir: PathBuf,
max_memory_mapped: usize,
max_disk_files: usize,
cleanup_interval: Duration,
}
impl L3MmapCache {
pub fn new() -> Self {
let cache_dir: _ = PathBuf::from("/tmp/amberjs_l3_cache");
std::fs::create_dir_all(&cache_dir).unwrap_or(());
Self {
entries: Arc::new(Mutex::new(HashMap::new())),
stats: Arc::new(Mutex::new(L3Stats::default())),
cache_dir,
max_memory_mapped: 1024 * 1024 * 1024, max_disk_files: 10000,
cleanup_interval: Duration::from_secs(300), }
}
pub async fn put(&self, key: &str, data: &[u8]) {
let file_name: _ = format!("{}.cache", self.hash_key(key));
let file_path: _ = self.cache_dir.join(&file_name);
if let Ok(mut file) = File::create(&file_path) {
let _: _ = file.write_all(data);
}
let entry: _ = L3Entry {
file_path,
file_size: data.len(),
last_accessed: Instant::now(),
access_count: 0,
is_mapped: false,
};
{
let mut entries = self.entries.write().unwrap();
entries.insert(key.to_string(), entry);
}
let mut stats = self.stats.write().unwrap();
stats.disk_files += 1;
self.cleanup_if_needed().await;
}
pub async fn get(&self, key: &str) -> Option<Vec<u8>> {
let mut stats = self.stats.write().unwrap();
if let Some(entry) = self.entries.read().unwrap().get(key).cloned() {
{
let mut entries = self.entries.write().unwrap();
if let Some(entry_mut) = entries.get_mut(key) {
entry_mut.access_count += 1;
entry_mut.last_accessed = Instant::now();
}
}
stats.hits += 1;
self.read_from_disk(key).await
} else {
stats.misses += 1;
None
}
}
pub async fn invalidate(&self, key: &str) {
if let Some(entry) = self.entries.write().unwrap().remove(key) {
let _: _ = std::fs::remove_file(&entry.file_path);
let mut stats = self.stats.write().unwrap();
stats.disk_files = stats.disk_files.saturating_sub(1);
if entry.is_mapped {
stats.memory_mapped_files = stats.memory_mapped_files.saturating_sub(1);
}
}
}
pub async fn gc(&self) {
let mut entries = self.entries.write().unwrap();
let cutoff: _ = Instant::now() - Duration::from_secs(7200); let min_access_count: _ = 3;
let keys_to_remove: Vec<String> = entries
.iter()
.filter(|(_, entry)| entry.last_accessed < cutoff && entry.access_count < min_access_count)
.map(|(key, _)| key.clone())
.collect();
for key in keys_to_remove {
if let Some(entry) = entries.remove(&key) {
let _: _ = std::fs::remove_file(&entry.file_path);
drop(entry); }
}
let mut stats = self.stats.write().unwrap();
stats.disk_files = entries.len();
}
pub async fn get_stats(&self) -> L3Stats {
self.stats.read().unwrap().clone()
}
async fn read_from_disk(&self, key: &str) -> Option<Vec<u8>> {
if let Some(entry) = self.entries.read().unwrap().get(key).cloned() {
match File::open(&entry.file_path) {
Ok(mut file) => {
let mut data = Vec::new();
if file.read_to_end(&mut data).is_ok() {
Some(data)
} else {
None
}
}
Err(_) => None,
}
} else {
None
}
}
async fn cleanup_if_needed(&self) {
let stats: _ = self.stats.read().unwrap();
if stats.disk_files > self.max_disk_files {
self.evict_old_entries().await;
}
}
async fn evict_old_entries(&self) {
let mut entries = self.entries.write().unwrap();
let keys_to_remove: Vec<String> = {
let mut sorted_entries: Vec<_> = entries.iter().collect();
sorted_entries.sort_by(|a, b| a.1.last_accessed.cmp(&b.1.last_accessed));
let to_remove: _ = sorted_entries.len() / 4;
sorted_entries.into_iter().take(to_remove).map(|(k, _)| k.clone()).collect()
};
for key in keys_to_remove {
if let Some(entry) = entries.remove(&key) {
let _: _ = std::fs::remove_file(&entry.file_path);
}
}
}
fn hash_key(&self, key: &str) -> String {
let mut hasher = DefaultHasher::new();
key.hash(&mut hasher);
format!("{:x}", hasher.finish())
}
pub fn size(&self) -> usize {
self.entries.read().unwrap().len()
}
pub fn is_empty(&self) -> bool {
self.entries.read().unwrap().is_empty()
}
}
impl Default for L3MmapCache {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
#[tokio::test]
async fn test_l3_basic_operations() {
let cache: _ = L3MmapCache::new();
cache.put("test.js", b"console.log('test');").await;
let result: _ = cache.get("test.js").await;
assert!(result.is_some());
assert_eq!(result.unwrap(), b"console.log('test');");
}
#[tokio::test]
async fn test_l3_file_storage() {
let cache: _ = L3MmapCache::new();
let large_data: _ = b"x".repeat(10000);
cache.put("large.js", &large_data).await;
let result: _ = cache.get("large.js").await;
assert!(result.is_some());
assert_eq!(result.unwrap().len(), 10000);
}
#[tokio::test]
async fn test_l3_invalidation() {
let cache: _ = L3MmapCache::new();
cache.put("test.js", b"test").await;
assert!(cache.get("test.js").await.is_some());
cache.invalidate("test.js").await;
assert!(cache.get("test.js").await.is_none());
}
#[tokio::test]
async fn test_l3_stats() {
let cache: _ = L3MmapCache::new();
cache.put("test.js", b"test").await;
cache.get("test.js").await;
let stats: _ = cache.get_stats().await;
assert_eq!(stats.hits, 1);
assert!(stats.disk_files > 0);
}
}