use super::lru::LruCache;
use crate::types::{ETypeId, NodeId, PropKeyId, PropValue, PropertyCacheConfig};
use std::collections::{HashMap, HashSet};
type NodePropKey = (NodeId, PropKeyId);
type EdgePropKey = (NodeId, ETypeId, NodeId, PropKeyId);
type EdgeIndexKey = (NodeId, ETypeId, NodeId);
#[derive(Debug, Clone, Default)]
pub struct PropertyCacheStats {
pub hits: u64,
pub misses: u64,
pub node_cache_size: usize,
pub edge_cache_size: usize,
pub max_node_cache_size: usize,
pub max_edge_cache_size: usize,
}
impl PropertyCacheStats {
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 total_size(&self) -> usize {
self.node_cache_size + self.edge_cache_size
}
}
pub struct PropertyCache {
node_cache: LruCache<NodePropKey, Option<PropValue>>,
edge_cache: LruCache<EdgePropKey, Option<PropValue>>,
node_key_index: HashMap<NodeId, HashSet<NodePropKey>>,
edge_key_index: HashMap<EdgeIndexKey, HashSet<EdgePropKey>>,
hits: u64,
misses: u64,
}
impl PropertyCache {
pub fn new(config: PropertyCacheConfig) -> Self {
Self {
node_cache: LruCache::new(config.max_node_props),
edge_cache: LruCache::new(config.max_edge_props),
node_key_index: HashMap::new(),
edge_key_index: HashMap::new(),
hits: 0,
misses: 0,
}
}
pub fn node_prop(
&mut self,
node_id: NodeId,
prop_key_id: PropKeyId,
) -> Option<&Option<PropValue>> {
let key = (node_id, prop_key_id);
let result = self.node_cache.get(&key);
if result.is_some() {
self.hits += 1;
} else {
self.misses += 1;
}
result
}
pub fn peek_node_prop(
&self,
node_id: NodeId,
prop_key_id: PropKeyId,
) -> Option<&Option<PropValue>> {
let key = (node_id, prop_key_id);
self.node_cache.peek(&key)
}
pub fn set_node_prop(
&mut self,
node_id: NodeId,
prop_key_id: PropKeyId,
value: Option<PropValue>,
) {
let key = (node_id, prop_key_id);
self.node_cache.set(key, value);
self.node_key_index.entry(node_id).or_default().insert(key);
}
pub fn invalidate_node(&mut self, node_id: NodeId) {
if let Some(keys) = self.node_key_index.remove(&node_id) {
for key in keys {
self.node_cache.delete(&key);
}
}
}
pub fn edge_prop(
&mut self,
src: NodeId,
etype: ETypeId,
dst: NodeId,
prop_key_id: PropKeyId,
) -> Option<&Option<PropValue>> {
let key = (src, etype, dst, prop_key_id);
let result = self.edge_cache.get(&key);
if result.is_some() {
self.hits += 1;
} else {
self.misses += 1;
}
result
}
pub fn peek_edge_prop(
&self,
src: NodeId,
etype: ETypeId,
dst: NodeId,
prop_key_id: PropKeyId,
) -> Option<&Option<PropValue>> {
let key = (src, etype, dst, prop_key_id);
self.edge_cache.peek(&key)
}
pub fn set_edge_prop(
&mut self,
src: NodeId,
etype: ETypeId,
dst: NodeId,
prop_key_id: PropKeyId,
value: Option<PropValue>,
) {
let key = (src, etype, dst, prop_key_id);
self.edge_cache.set(key, value);
let edge_index_key = (src, etype, dst);
self
.edge_key_index
.entry(edge_index_key)
.or_default()
.insert(key);
}
pub fn invalidate_edge(&mut self, src: NodeId, etype: ETypeId, dst: NodeId) {
let edge_index_key = (src, etype, dst);
if let Some(keys) = self.edge_key_index.remove(&edge_index_key) {
for key in keys {
self.edge_cache.delete(&key);
}
}
}
pub fn clear(&mut self) {
self.node_cache.clear();
self.edge_cache.clear();
self.node_key_index.clear();
self.edge_key_index.clear();
self.hits = 0;
self.misses = 0;
}
pub fn stats(&self) -> PropertyCacheStats {
PropertyCacheStats {
hits: self.hits,
misses: self.misses,
node_cache_size: self.node_cache.len(),
edge_cache_size: self.edge_cache.len(),
max_node_cache_size: self.node_cache.max_size(),
max_edge_cache_size: self.edge_cache.max_size(),
}
}
pub fn node_cache_size(&self) -> usize {
self.node_cache.len()
}
pub fn edge_cache_size(&self) -> usize {
self.edge_cache.len()
}
pub fn is_node_cache_empty(&self) -> bool {
self.node_cache.is_empty()
}
pub fn is_edge_cache_empty(&self) -> bool {
self.edge_cache.is_empty()
}
pub fn reset_stats(&mut self) {
self.hits = 0;
self.misses = 0;
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_cache() -> PropertyCache {
PropertyCache::new(PropertyCacheConfig {
max_node_props: 100,
max_edge_props: 100,
})
}
#[test]
fn test_new_cache() {
let cache = make_cache();
assert!(cache.is_node_cache_empty());
assert!(cache.is_edge_cache_empty());
assert_eq!(cache.stats().hits, 0);
assert_eq!(cache.stats().misses, 0);
}
#[test]
fn test_node_prop_cache_miss() {
let mut cache = make_cache();
assert_eq!(cache.node_prop(1, 10), None);
assert_eq!(cache.stats().misses, 1);
}
#[test]
fn test_node_prop_cache_hit() {
let mut cache = make_cache();
cache.set_node_prop(1, 10, Some(PropValue::I64(42)));
let result = cache.node_prop(1, 10);
assert_eq!(result, Some(&Some(PropValue::I64(42))));
assert_eq!(cache.stats().hits, 1);
assert_eq!(cache.stats().misses, 0);
}
#[test]
fn test_node_prop_null_value() {
let mut cache = make_cache();
cache.set_node_prop(1, 10, None);
let result = cache.node_prop(1, 10);
assert_eq!(result, Some(&None));
assert_eq!(cache.stats().hits, 1);
}
#[test]
fn test_node_prop_update() {
let mut cache = make_cache();
cache.set_node_prop(1, 10, Some(PropValue::I64(42)));
cache.set_node_prop(1, 10, Some(PropValue::I64(100)));
let result = cache.node_prop(1, 10);
assert_eq!(result, Some(&Some(PropValue::I64(100))));
assert_eq!(cache.node_cache_size(), 1);
}
#[test]
fn test_node_invalidation() {
let mut cache = make_cache();
cache.set_node_prop(1, 10, Some(PropValue::I64(42)));
cache.set_node_prop(1, 20, Some(PropValue::String("hello".to_string())));
cache.set_node_prop(1, 30, Some(PropValue::Bool(true)));
cache.set_node_prop(2, 10, Some(PropValue::I64(100)));
assert_eq!(cache.node_cache_size(), 4);
cache.invalidate_node(1);
assert_eq!(cache.node_prop(1, 10), None);
assert_eq!(cache.node_prop(1, 20), None);
assert_eq!(cache.node_prop(1, 30), None);
assert_eq!(cache.node_prop(2, 10), Some(&Some(PropValue::I64(100))));
assert_eq!(cache.node_cache_size(), 1);
}
#[test]
fn test_edge_prop_cache_miss() {
let mut cache = make_cache();
assert_eq!(cache.edge_prop(1, 1, 2, 10), None);
assert_eq!(cache.stats().misses, 1);
}
#[test]
fn test_edge_prop_cache_hit() {
let mut cache = make_cache();
cache.set_edge_prop(1, 1, 2, 10, Some(PropValue::F64(std::f64::consts::PI)));
let result = cache.edge_prop(1, 1, 2, 10);
assert_eq!(result, Some(&Some(PropValue::F64(std::f64::consts::PI))));
assert_eq!(cache.stats().hits, 1);
}
#[test]
fn test_edge_prop_null_value() {
let mut cache = make_cache();
cache.set_edge_prop(1, 1, 2, 10, None);
let result = cache.edge_prop(1, 1, 2, 10);
assert_eq!(result, Some(&None));
}
#[test]
fn test_edge_invalidation() {
let mut cache = make_cache();
cache.set_edge_prop(1, 1, 2, 10, Some(PropValue::I64(42)));
cache.set_edge_prop(1, 1, 2, 20, Some(PropValue::String("weight".to_string())));
cache.set_edge_prop(1, 2, 3, 10, Some(PropValue::I64(100)));
assert_eq!(cache.edge_cache_size(), 3);
cache.invalidate_edge(1, 1, 2);
assert_eq!(cache.edge_prop(1, 1, 2, 10), None);
assert_eq!(cache.edge_prop(1, 1, 2, 20), None);
assert_eq!(
cache.edge_prop(1, 2, 3, 10),
Some(&Some(PropValue::I64(100)))
);
assert_eq!(cache.edge_cache_size(), 1);
}
#[test]
fn test_clear() {
let mut cache = make_cache();
cache.set_node_prop(1, 10, Some(PropValue::I64(42)));
cache.set_edge_prop(1, 1, 2, 10, Some(PropValue::I64(100)));
cache.node_prop(1, 10);
cache.node_prop(999, 10);
cache.clear();
assert!(cache.is_node_cache_empty());
assert!(cache.is_edge_cache_empty());
assert_eq!(cache.stats().hits, 0);
assert_eq!(cache.stats().misses, 0);
}
#[test]
fn test_peek_does_not_update_lru() {
let mut cache = PropertyCache::new(PropertyCacheConfig {
max_node_props: 3,
max_edge_props: 3,
});
cache.set_node_prop(1, 10, Some(PropValue::I64(1)));
cache.set_node_prop(2, 10, Some(PropValue::I64(2)));
cache.set_node_prop(3, 10, Some(PropValue::I64(3)));
cache.peek_node_prop(1, 10);
cache.set_node_prop(4, 10, Some(PropValue::I64(4)));
assert_eq!(cache.peek_node_prop(1, 10), None);
assert!(cache.peek_node_prop(2, 10).is_some());
assert!(cache.peek_node_prop(3, 10).is_some());
assert!(cache.peek_node_prop(4, 10).is_some());
}
#[test]
fn test_updates_lru() {
let mut cache = PropertyCache::new(PropertyCacheConfig {
max_node_props: 3,
max_edge_props: 3,
});
cache.set_node_prop(1, 10, Some(PropValue::I64(1)));
cache.set_node_prop(2, 10, Some(PropValue::I64(2)));
cache.set_node_prop(3, 10, Some(PropValue::I64(3)));
cache.node_prop(1, 10);
cache.set_node_prop(4, 10, Some(PropValue::I64(4)));
assert!(cache.peek_node_prop(1, 10).is_some());
assert_eq!(cache.peek_node_prop(2, 10), None);
assert!(cache.peek_node_prop(3, 10).is_some());
assert!(cache.peek_node_prop(4, 10).is_some());
}
#[test]
fn test_stats() {
let mut cache = make_cache();
cache.set_node_prop(1, 10, Some(PropValue::I64(42)));
cache.set_edge_prop(1, 1, 2, 10, Some(PropValue::I64(100)));
cache.node_prop(1, 10);
cache.edge_prop(1, 1, 2, 10);
cache.node_prop(999, 10);
cache.edge_prop(999, 1, 2, 10);
let stats = cache.stats();
assert_eq!(stats.hits, 2);
assert_eq!(stats.misses, 2);
assert_eq!(stats.hit_rate(), 0.5);
assert_eq!(stats.node_cache_size, 1);
assert_eq!(stats.edge_cache_size, 1);
assert_eq!(stats.total_size(), 2);
}
#[test]
fn test_reset_stats() {
let mut cache = make_cache();
cache.set_node_prop(1, 10, Some(PropValue::I64(42)));
cache.node_prop(1, 10);
cache.node_prop(999, 10);
assert_eq!(cache.stats().hits, 1);
assert_eq!(cache.stats().misses, 1);
cache.reset_stats();
assert_eq!(cache.stats().hits, 0);
assert_eq!(cache.stats().misses, 0);
assert_eq!(cache.node_cache_size(), 1);
}
#[test]
fn test_all_prop_value_types() {
let mut cache = make_cache();
cache.set_node_prop(1, 1, Some(PropValue::Null));
cache.set_node_prop(1, 2, Some(PropValue::Bool(true)));
cache.set_node_prop(1, 3, Some(PropValue::I64(-123)));
cache.set_node_prop(1, 4, Some(PropValue::F64(std::f64::consts::PI)));
cache.set_node_prop(1, 5, Some(PropValue::String("hello world".to_string())));
cache.set_node_prop(1, 6, Some(PropValue::VectorF32(vec![1.0, 2.0, 3.0])));
assert_eq!(cache.node_prop(1, 1), Some(&Some(PropValue::Null)));
assert_eq!(cache.node_prop(1, 2), Some(&Some(PropValue::Bool(true))));
assert_eq!(cache.node_prop(1, 3), Some(&Some(PropValue::I64(-123))));
assert_eq!(
cache.node_prop(1, 4),
Some(&Some(PropValue::F64(std::f64::consts::PI)))
);
assert_eq!(
cache.node_prop(1, 5),
Some(&Some(PropValue::String("hello world".to_string())))
);
assert_eq!(
cache.node_prop(1, 6),
Some(&Some(PropValue::VectorF32(vec![1.0, 2.0, 3.0])))
);
}
#[test]
fn test_invalidate_nonexistent_node() {
let mut cache = make_cache();
cache.set_node_prop(1, 10, Some(PropValue::I64(42)));
cache.invalidate_node(999);
assert_eq!(cache.node_prop(1, 10), Some(&Some(PropValue::I64(42))));
}
#[test]
fn test_invalidate_nonexistent_edge() {
let mut cache = make_cache();
cache.set_edge_prop(1, 1, 2, 10, Some(PropValue::I64(42)));
cache.invalidate_edge(999, 1, 2);
assert_eq!(
cache.edge_prop(1, 1, 2, 10),
Some(&Some(PropValue::I64(42)))
);
}
}