use std::sync::Arc;
use crate::storage::cache::DecompressedChunkCache;
#[cfg(feature = "state_machine")]
use crate::Value;
use crate::{Config, Result};
#[derive(Debug)]
pub struct MemoryManager {
chunk_cache: Option<Arc<DecompressedChunkCache>>,
}
impl MemoryManager {
pub fn new(_config: &Config) -> Result<Self> {
Ok(Self { chunk_cache: None })
}
pub fn with_chunk_cache(chunk_cache: Arc<DecompressedChunkCache>) -> Self {
Self {
chunk_cache: Some(chunk_cache),
}
}
pub fn stats(&self) -> Result<MemoryStats> {
let mut stats = MemoryStats::default();
if let Some(cache) = &self.chunk_cache {
stats.block_cache_hits = cache.hit_count();
stats.block_cache_misses = cache.miss_count();
stats.block_cache_evictions = cache.eviction_count();
stats.block_cache_capacity_bytes = cache.budget_bytes();
stats.total_memory_used = cache.resident_bytes();
}
Ok(stats)
}
}
#[cfg(feature = "state_machine")]
pub(crate) fn estimate_value_size(value: &Value) -> usize {
match value {
Value::Null => 1,
Value::Boolean(_) => 1,
Value::Integer(_) => 4,
Value::BigInt(_) => 8,
Value::Counter(_) => 8,
Value::Float(_) => 8,
Value::Text(s) => s.len(),
Value::Blob(b) => b.len(),
Value::Timestamp(_) => 8,
Value::Date(_) => 4,
Value::Time(_) => 8,
Value::Uuid(_) => 16,
Value::Inet(bytes) => bytes.len(),
Value::Json(json) => json.to_string().len(),
Value::List(items) => items.iter().map(estimate_value_size).sum(),
Value::Map(map) => map
.iter()
.map(|(k, v)| estimate_value_size(k) + estimate_value_size(v))
.sum(),
Value::TinyInt(_) => 1,
Value::SmallInt(_) => 2,
Value::Float32(_) => 4,
Value::Set(items) => items.iter().map(estimate_value_size).sum(),
Value::Tuple(items) => items.iter().map(estimate_value_size).sum(),
Value::Udt(udt) => udt
.fields
.iter()
.map(|f| f.value.as_ref().map_or(0, estimate_value_size))
.sum(),
Value::Frozen(boxed_value) => estimate_value_size(boxed_value),
Value::Varint(data) => data.len(),
Value::Decimal { unscaled, .. } => 4 + unscaled.len(), Value::Duration { .. } => 12, Value::Tombstone(_) => 16, }
}
#[derive(Debug, Clone, Default)]
pub struct MemoryStats {
pub block_cache_hits: u64,
pub block_cache_misses: u64,
pub block_cache_evictions: u64,
pub block_cache_capacity_bytes: usize,
pub key_cache_hits: u64,
pub key_cache_misses: u64,
pub key_cache_evictions: u64,
pub key_cache_invalidations: u64,
pub key_cache_resident_bytes: usize,
pub key_cache_capacity_bytes: usize,
pub row_cache_hits: u64,
pub row_cache_misses: u64,
pub total_memory_used: usize,
pub buffer_allocations: u64,
pub buffer_deallocations: u64,
}
impl MemoryStats {
pub fn block_cache_hit_rate(&self) -> f64 {
let total = self.block_cache_hits + self.block_cache_misses;
if total > 0 {
self.block_cache_hits as f64 / total as f64
} else {
0.0
}
}
pub fn key_cache_hit_rate(&self) -> f64 {
let total = self.key_cache_hits + self.key_cache_misses;
if total > 0 {
self.key_cache_hits as f64 / total as f64
} else {
0.0
}
}
pub fn row_cache_hit_rate(&self) -> f64 {
let total = self.row_cache_hits + self.row_cache_misses;
if total > 0 {
self.row_cache_hits as f64 / total as f64
} else {
0.0
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::storage::cache::{ChunkKey, DecompressedChunkCache};
#[test]
fn stats_report_zero_without_a_wired_cache() {
let config = Config::default();
let manager = MemoryManager::new(&config).expect("construct manager");
let stats = manager.stats().expect("stats");
assert_eq!(stats.block_cache_hits, 0);
assert_eq!(stats.block_cache_misses, 0);
assert_eq!(stats.total_memory_used, 0);
assert_eq!(stats.block_cache_hit_rate(), 0.0);
assert_eq!(stats.block_cache_evictions, 0);
assert_eq!(stats.block_cache_capacity_bytes, 0);
assert_eq!(stats.key_cache_hits, 0);
assert_eq!(stats.key_cache_misses, 0);
assert_eq!(stats.key_cache_evictions, 0);
assert_eq!(stats.key_cache_invalidations, 0);
assert_eq!(stats.key_cache_resident_bytes, 0);
assert_eq!(stats.key_cache_capacity_bytes, 0);
assert_eq!(stats.key_cache_hit_rate(), 0.0);
}
#[test]
fn stats_bridge_reports_real_b1_cache_numbers() {
let cache = Arc::new(DecompressedChunkCache::with_budget_bytes(1 << 20));
let manager = MemoryManager::with_chunk_cache(Arc::clone(&cache));
let key = ChunkKey::new(1, 0);
cache.insert(key, vec![0xAB; 4096]); assert!(cache.get(&key).is_some()); assert!(cache.get(&ChunkKey::new(1, 1)).is_none());
let stats = manager.stats().expect("stats");
assert_eq!(stats.block_cache_hits, 1);
assert_eq!(stats.block_cache_misses, 1);
assert_eq!(stats.total_memory_used, cache.resident_bytes());
assert!(
stats.total_memory_used > 0,
"occupancy tracks resident bytes"
);
assert!(
stats.block_cache_hit_rate() > 0.0,
"a hit makes the reported rate non-zero (not structural 0.0)"
);
assert_eq!(stats.block_cache_capacity_bytes, cache.budget_bytes());
assert_eq!(stats.block_cache_evictions, 0);
}
#[test]
fn stats_bridge_reports_real_b1_eviction_count() {
let cache = Arc::new(DecompressedChunkCache::with_budget_and_shards(200, 1));
let manager = MemoryManager::with_chunk_cache(Arc::clone(&cache));
for i in 0..5u64 {
cache.insert(ChunkKey::new(1, i), vec![i as u8; 100]);
}
let stats = manager.stats().expect("stats");
assert_eq!(stats.block_cache_evictions, cache.eviction_count());
assert!(
stats.block_cache_evictions > 0,
"over-budget inserts must have evicted"
);
assert_eq!(stats.block_cache_capacity_bytes, cache.budget_bytes());
}
}