#[cfg(target_arch = "wasm32")]
use std::sync::RwLock;
use std::{
collections::HashMap,
sync::atomic::{AtomicU64, AtomicUsize, Ordering},
};
use instant::Instant;
#[cfg(not(target_arch = "wasm32"))]
use parking_lot::RwLock;
macro_rules! read_lock {
($rwlock:expr) => {{
#[cfg(not(target_arch = "wasm32"))]
{
$rwlock.read()
}
#[cfg(target_arch = "wasm32")]
{
$rwlock.read().unwrap()
}
}};
}
macro_rules! write_lock {
($rwlock:expr) => {{
#[cfg(not(target_arch = "wasm32"))]
{
$rwlock.write()
}
#[cfg(target_arch = "wasm32")]
{
$rwlock.write().unwrap()
}
}};
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum IndexBackend {
InMemory,
DiskBacked,
}
#[derive(Debug)]
pub struct IndexStats {
pub memory_bytes: usize,
pub disk_bytes: usize,
pub access_count: AtomicU64,
pub last_access: Instant,
pub backend: IndexBackend,
}
impl Clone for IndexStats {
fn clone(&self) -> Self {
IndexStats {
memory_bytes: self.memory_bytes,
disk_bytes: self.disk_bytes,
access_count: AtomicU64::new(self.access_count.load(Ordering::Relaxed)),
last_access: self.last_access,
backend: self.backend,
}
}
}
impl IndexStats {
pub fn new(memory_bytes: usize, disk_bytes: usize, backend: IndexBackend) -> Self {
IndexStats {
memory_bytes,
disk_bytes,
access_count: AtomicU64::new(0),
last_access: Instant::now(),
backend,
}
}
pub fn record_access(&mut self) {
self.access_count.fetch_add(1, Ordering::Relaxed);
self.last_access = Instant::now();
}
pub fn get_access_count(&self) -> u64 {
self.access_count.load(Ordering::Relaxed)
}
}
#[derive(Debug)]
pub struct ResourceTracker {
memory_used: AtomicUsize,
disk_used: AtomicUsize,
index_stats: RwLock<HashMap<String, IndexStats>>,
}
impl Clone for ResourceTracker {
fn clone(&self) -> Self {
ResourceTracker {
memory_used: AtomicUsize::new(self.memory_used.load(Ordering::Relaxed)),
disk_used: AtomicUsize::new(self.disk_used.load(Ordering::Relaxed)),
index_stats: RwLock::new(read_lock!(self.index_stats).clone()),
}
}
}
impl ResourceTracker {
pub fn new() -> Self {
ResourceTracker {
memory_used: AtomicUsize::new(0),
disk_used: AtomicUsize::new(0),
index_stats: RwLock::new(HashMap::new()),
}
}
pub fn memory_used(&self) -> usize {
self.memory_used.load(Ordering::Relaxed)
}
pub fn disk_used(&self) -> usize {
self.disk_used.load(Ordering::Relaxed)
}
pub fn register_index(
&mut self,
index_name: String,
memory_bytes: usize,
disk_bytes: usize,
backend: IndexBackend,
) {
let normalized = index_name.to_lowercase();
self.memory_used.fetch_add(memory_bytes, Ordering::Relaxed);
self.disk_used.fetch_add(disk_bytes, Ordering::Relaxed);
let stats = IndexStats::new(memory_bytes, disk_bytes, backend);
write_lock!(self.index_stats).insert(normalized, stats);
}
pub fn unregister_index(&mut self, index_name: &str) {
let normalized = index_name.to_lowercase();
if let Some(stats) = write_lock!(self.index_stats).remove(&normalized) {
self.memory_used.fetch_sub(stats.memory_bytes, Ordering::Relaxed);
self.disk_used.fetch_sub(stats.disk_bytes, Ordering::Relaxed);
}
}
pub fn record_access(&self, index_name: &str) {
let normalized = index_name.to_lowercase();
if let Some(stats) = write_lock!(self.index_stats).get_mut(&normalized) {
stats.record_access();
}
}
pub fn mark_spilled(&mut self, index_name: &str, new_disk_bytes: usize) {
let normalized = index_name.to_lowercase();
if let Some(stats) = write_lock!(self.index_stats).get_mut(&normalized) {
self.memory_used.fetch_sub(stats.memory_bytes, Ordering::Relaxed);
let disk_delta = new_disk_bytes.saturating_sub(stats.disk_bytes);
self.disk_used.fetch_add(disk_delta, Ordering::Relaxed);
stats.memory_bytes = 0;
stats.disk_bytes = new_disk_bytes;
stats.backend = IndexBackend::DiskBacked;
}
}
pub fn get_index_stats(&self, index_name: &str) -> Option<IndexStats> {
let normalized = index_name.to_lowercase();
read_lock!(self.index_stats).get(&normalized).cloned()
}
pub fn find_coldest_in_memory_index(&self) -> Option<(String, Instant)> {
read_lock!(self.index_stats)
.iter()
.filter(|(_, stats)| stats.backend == IndexBackend::InMemory)
.min_by_key(|(_, stats)| stats.last_access)
.map(|(name, stats)| (name.clone(), stats.last_access))
}
pub fn get_in_memory_indexes_by_lru(&self) -> Vec<String> {
let mut indexes: Vec<_> = read_lock!(self.index_stats)
.iter()
.filter(|(_, stats)| stats.backend == IndexBackend::InMemory)
.map(|(name, stats)| (name.clone(), stats.last_access))
.collect();
indexes.sort_by_key(|(_, last_access)| *last_access);
indexes.into_iter().map(|(name, _)| name).collect()
}
pub fn get_backend(&self, index_name: &str) -> Option<IndexBackend> {
let normalized = index_name.to_lowercase();
read_lock!(self.index_stats).get(&normalized).map(|stats| stats.backend)
}
}
impl Default for ResourceTracker {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use std::thread;
use instant::Duration;
use super::*;
#[test]
fn test_resource_tracker_basic() {
let mut tracker = ResourceTracker::new();
assert_eq!(tracker.memory_used(), 0);
assert_eq!(tracker.disk_used(), 0);
tracker.register_index(
"idx1".to_string(),
1000, 0, IndexBackend::InMemory,
);
assert_eq!(tracker.memory_used(), 1000);
assert_eq!(tracker.disk_used(), 0);
tracker.register_index(
"idx2".to_string(),
0, 5000, IndexBackend::DiskBacked,
);
assert_eq!(tracker.memory_used(), 1000);
assert_eq!(tracker.disk_used(), 5000);
}
#[test]
fn test_unregister_index() {
let mut tracker = ResourceTracker::new();
tracker.register_index("idx1".to_string(), 1000, 0, IndexBackend::InMemory);
tracker.register_index("idx2".to_string(), 2000, 0, IndexBackend::InMemory);
assert_eq!(tracker.memory_used(), 3000);
tracker.unregister_index("idx1");
assert_eq!(tracker.memory_used(), 2000);
tracker.unregister_index("idx2");
assert_eq!(tracker.memory_used(), 0);
}
#[test]
fn test_access_tracking() {
let mut tracker = ResourceTracker::new();
tracker.register_index("idx1".to_string(), 1000, 0, IndexBackend::InMemory);
tracker.record_access("idx1");
tracker.record_access("idx1");
tracker.record_access("idx1");
let stats = tracker.get_index_stats("idx1").unwrap();
assert_eq!(stats.get_access_count(), 3);
}
#[test]
fn test_lru_tracking() {
let mut tracker = ResourceTracker::new();
tracker.register_index("idx1".to_string(), 1000, 0, IndexBackend::InMemory);
thread::sleep(Duration::from_millis(10));
tracker.register_index("idx2".to_string(), 2000, 0, IndexBackend::InMemory);
thread::sleep(Duration::from_millis(10));
tracker.register_index("idx3".to_string(), 3000, 0, IndexBackend::InMemory);
let (coldest, _) = tracker.find_coldest_in_memory_index().unwrap();
assert_eq!(coldest, "idx1");
tracker.record_access("idx1");
thread::sleep(Duration::from_millis(10));
let (coldest, _) = tracker.find_coldest_in_memory_index().unwrap();
assert_eq!(coldest, "idx2");
}
#[test]
fn test_spill_to_disk() {
let mut tracker = ResourceTracker::new();
tracker.register_index("idx1".to_string(), 1000, 0, IndexBackend::InMemory);
assert_eq!(tracker.memory_used(), 1000);
assert_eq!(tracker.disk_used(), 0);
assert_eq!(tracker.get_backend("idx1"), Some(IndexBackend::InMemory));
tracker.mark_spilled("idx1", 800);
assert_eq!(tracker.memory_used(), 0);
assert_eq!(tracker.disk_used(), 800);
assert_eq!(tracker.get_backend("idx1"), Some(IndexBackend::DiskBacked));
}
#[test]
fn test_get_in_memory_indexes_by_lru() {
let mut tracker = ResourceTracker::new();
tracker.register_index("idx1".to_string(), 1000, 0, IndexBackend::InMemory);
thread::sleep(Duration::from_millis(10));
tracker.register_index("idx2".to_string(), 2000, 0, IndexBackend::InMemory);
thread::sleep(Duration::from_millis(10));
tracker.register_index("idx3".to_string(), 0, 3000, IndexBackend::DiskBacked);
thread::sleep(Duration::from_millis(10));
tracker.register_index("idx4".to_string(), 4000, 0, IndexBackend::InMemory);
let lru_order = tracker.get_in_memory_indexes_by_lru();
assert_eq!(lru_order, vec!["idx1", "idx2", "idx4"]);
}
}