use std::hash::{Hash, Hasher};
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::Instant;
use dashmap::DashMap;
#[derive(Debug)]
pub struct AccessTracker {
last_access: AtomicU64,
access_count: AtomicU64,
}
impl AccessTracker {
pub fn new() -> Self {
Self {
last_access: AtomicU64::new(0),
access_count: AtomicU64::new(0),
}
}
pub fn with_timestamp(timestamp_us: u64) -> Self {
Self {
last_access: AtomicU64::new(timestamp_us),
access_count: AtomicU64::new(1),
}
}
#[inline]
pub fn touch(&self, now_us: u64) {
self.last_access.store(now_us, Ordering::Relaxed);
self.access_count.fetch_add(1, Ordering::Relaxed);
}
#[inline]
pub fn last_access(&self) -> u64 {
self.last_access.load(Ordering::Relaxed)
}
#[inline]
pub fn access_count(&self) -> u64 {
self.access_count.load(Ordering::Relaxed)
}
pub fn is_older_than(&self, other: &AccessTracker) -> bool {
let self_time = self.last_access();
let other_time = other.last_access();
if self_time != other_time {
self_time < other_time
} else {
self.access_count() < other.access_count()
}
}
pub fn coldness_score(&self, now_us: u64) -> u64 {
let age = now_us.saturating_sub(self.last_access());
let count = self.access_count().max(1);
age / count
}
}
impl Default for AccessTracker {
fn default() -> Self {
Self::new()
}
}
impl Clone for AccessTracker {
fn clone(&self) -> Self {
Self {
last_access: AtomicU64::new(self.last_access.load(Ordering::Relaxed)),
access_count: AtomicU64::new(self.access_count.load(Ordering::Relaxed)),
}
}
}
fn hash_path(path: &[u8]) -> u64 {
const FNV_OFFSET: u64 = 0xcbf29ce484222325;
const FNV_PRIME: u64 = 0x100000001b3;
let mut hash = FNV_OFFSET;
for &byte in path {
hash ^= byte as u64;
hash = hash.wrapping_mul(FNV_PRIME);
}
hash
}
fn hash_path_std<T: Hash>(path: &[T]) -> u64 {
use std::collections::hash_map::DefaultHasher;
let mut hasher = DefaultHasher::new();
path.hash(&mut hasher);
hasher.finish()
}
pub struct LruRegistry {
trackers: DashMap<u64, AccessTracker>,
epoch_start: Instant,
max_entries: usize,
}
impl LruRegistry {
pub fn new() -> Self {
Self::with_capacity(1_000_000)
}
pub fn with_capacity(capacity: usize) -> Self {
Self {
trackers: DashMap::with_capacity(capacity.min(65536)),
epoch_start: Instant::now(),
max_entries: capacity,
}
}
#[inline]
pub fn touch(&self, path: &[u8]) {
let hash = hash_path(path);
let now = self.now_us();
if let Some(tracker) = self.trackers.get(&hash) {
tracker.touch(now);
return;
}
if self.trackers.len() < self.max_entries {
self.trackers
.insert(hash, AccessTracker::with_timestamp(now));
}
}
#[inline]
pub fn touch_hash(&self, hash: u64) {
let now = self.now_us();
if let Some(tracker) = self.trackers.get(&hash) {
tracker.touch(now);
return;
}
if self.trackers.len() < self.max_entries {
self.trackers
.insert(hash, AccessTracker::with_timestamp(now));
}
}
pub fn last_access(&self, path: &[u8]) -> Option<u64> {
let hash = hash_path(path);
self.trackers.get(&hash).map(|t| t.last_access())
}
pub fn coldness_score(&self, path: &[u8]) -> u64 {
let hash = hash_path(path);
let now = self.now_us();
self.trackers
.get(&hash)
.map(|t| t.coldness_score(now))
.unwrap_or(u64::MAX) }
pub fn coldness_score_hash(&self, hash: u64) -> u64 {
let now = self.now_us();
self.trackers
.get(&hash)
.map(|t| t.coldness_score(now))
.unwrap_or(u64::MAX)
}
pub fn remove(&self, path: &[u8]) {
let hash = hash_path(path);
self.trackers.remove(&hash);
}
pub fn remove_hash(&self, hash: u64) {
self.trackers.remove(&hash);
}
pub fn len(&self) -> usize {
self.trackers.len()
}
pub fn is_empty(&self) -> bool {
self.trackers.is_empty()
}
pub fn clear(&self) {
self.trackers.clear();
}
pub fn coldest_n(&self, n: usize) -> Vec<u64> {
let now = self.now_us();
let mut entries: Vec<_> = self
.trackers
.iter()
.map(|entry| (*entry.key(), entry.value().coldness_score(now)))
.collect();
let len = entries.len();
if n < len / 4 {
entries.select_nth_unstable_by_key(n.min(len.saturating_sub(1)), |(_h, score)| {
std::cmp::Reverse(*score)
});
entries.truncate(n);
} else {
entries.sort_unstable_by_key(|(_h, score)| std::cmp::Reverse(*score));
entries.truncate(n);
}
entries.into_iter().map(|(h, _)| h).collect()
}
pub fn prune_to(&self, target_size: usize) -> usize {
if self.trackers.len() <= target_size {
return 0;
}
let to_remove = self.trackers.len() - target_size;
let coldest = self.coldest_n(to_remove);
for hash in &coldest {
self.trackers.remove(hash);
}
coldest.len()
}
#[inline]
fn now_us(&self) -> u64 {
self.epoch_start.elapsed().as_micros() as u64
}
pub fn path_hash(path: &[u8]) -> u64 {
hash_path(path)
}
}
impl Default for LruRegistry {
fn default() -> Self {
Self::new()
}
}
pub fn hash_char_path(path: &[char]) -> u64 {
hash_path_std(path)
}
#[cfg(test)]
mod tests {
use super::*;
use std::thread;
use std::time::Duration;
#[test]
fn test_access_tracker_basic() {
let tracker = AccessTracker::new();
assert_eq!(tracker.last_access(), 0);
assert_eq!(tracker.access_count(), 0);
tracker.touch(1000);
assert_eq!(tracker.last_access(), 1000);
assert_eq!(tracker.access_count(), 1);
tracker.touch(2000);
assert_eq!(tracker.last_access(), 2000);
assert_eq!(tracker.access_count(), 2);
}
#[test]
fn test_access_tracker_comparison() {
let old = AccessTracker::with_timestamp(1000);
let new = AccessTracker::with_timestamp(2000);
assert!(old.is_older_than(&new));
assert!(!new.is_older_than(&old));
}
#[test]
fn test_access_tracker_coldness() {
let tracker = AccessTracker::with_timestamp(1000);
let now = 2000;
let score1 = tracker.coldness_score(now);
tracker.touch(1500);
tracker.touch(1500);
let score2 = tracker.coldness_score(now);
assert!(score2 < score1);
}
#[test]
fn test_lru_registry_basic() {
let registry = LruRegistry::new();
assert!(registry.is_empty());
registry.touch(b"hello");
assert_eq!(registry.len(), 1);
registry.touch(b"world");
assert_eq!(registry.len(), 2);
registry.remove(b"hello");
assert_eq!(registry.len(), 1);
registry.clear();
assert!(registry.is_empty());
}
#[test]
fn test_lru_registry_last_access() {
let registry = LruRegistry::new();
assert!(registry.last_access(b"missing").is_none());
registry.touch(b"test");
let access1 = registry.last_access(b"test");
assert!(access1.is_some());
thread::sleep(Duration::from_micros(100));
registry.touch(b"test");
let access2 = registry.last_access(b"test");
assert!(access2.unwrap() > access1.unwrap());
}
#[test]
fn test_lru_registry_coldest() {
let registry = LruRegistry::new();
registry.touch(b"cold1");
thread::sleep(Duration::from_micros(100));
registry.touch(b"cold2");
thread::sleep(Duration::from_micros(100));
registry.touch(b"hot");
for _ in 0..10 {
registry.touch(b"hot");
}
let coldest = registry.coldest_n(2);
assert_eq!(coldest.len(), 2);
let hot_hash = hash_path(b"hot");
assert!(!coldest.contains(&hot_hash));
}
#[test]
fn test_lru_registry_prune() {
let registry = LruRegistry::with_capacity(100);
for i in 0..50 {
registry.touch(&[i as u8]);
}
assert_eq!(registry.len(), 50);
let removed = registry.prune_to(30);
assert_eq!(removed, 20);
assert_eq!(registry.len(), 30);
}
#[test]
fn test_hash_path_deterministic() {
let path = b"test/path/to/node";
let hash1 = hash_path(path);
let hash2 = hash_path(path);
assert_eq!(hash1, hash2);
let hash3 = hash_path(b"different/path");
assert_ne!(hash1, hash3);
}
#[test]
fn test_concurrent_access() {
use std::sync::Arc;
let registry = Arc::new(LruRegistry::new());
let handles: Vec<_> = (0..4)
.map(|i| {
let reg = Arc::clone(®istry);
thread::spawn(move || {
for j in 0..100 {
let path = format!("thread{}path{}", i, j);
reg.touch(path.as_bytes());
}
})
})
.collect();
for handle in handles {
handle.join().expect("thread panicked");
}
assert!(registry.len() <= 400);
assert!(registry.len() >= 200); }
}