use crate::metamodel::{Aspect, ModelElement, Property};
use crate::query::{ComplexityMetrics, Dependency, ModelQuery};
use std::collections::HashMap;
use std::sync::{Arc, RwLock};
#[derive(Debug, Clone, Hash, Eq, PartialEq)]
enum CacheKey {
ComplexityMetrics,
OptionalProperties,
RequiredProperties,
CollectionProperties,
DependencyGraph,
CircularDependencies,
FindByType(String),
FindByNamespace(String),
}
pub struct CachedModelQuery<'a> {
query: ModelQuery<'a>,
cache: Arc<RwLock<HashMap<CacheKey, CachedValue>>>,
hits: Arc<RwLock<usize>>,
misses: Arc<RwLock<usize>>,
max_cache_size: usize,
}
#[derive(Clone)]
struct CachedValue {
value: CachedResult,
access_count: usize,
last_accessed: std::time::Instant,
}
#[derive(Clone)]
enum CachedResult {
ComplexityMetrics(ComplexityMetrics),
Properties(Vec<String>), Dependencies(Vec<Dependency>),
}
impl<'a> CachedModelQuery<'a> {
pub fn new(aspect: &'a Aspect, max_cache_size: usize) -> Self {
Self {
query: ModelQuery::new(aspect),
cache: Arc::new(RwLock::new(HashMap::new())),
hits: Arc::new(RwLock::new(0)),
misses: Arc::new(RwLock::new(0)),
max_cache_size: max_cache_size.max(1),
}
}
pub fn complexity_metrics(&mut self) -> ComplexityMetrics {
let key = CacheKey::ComplexityMetrics;
if let Some(CachedResult::ComplexityMetrics(metrics)) = self.get_cached(&key) {
return metrics;
}
let metrics = self.query.complexity_metrics();
self.cache_result(key, CachedResult::ComplexityMetrics(metrics.clone()));
metrics
}
pub fn find_optional_properties(&mut self) -> Vec<&Property> {
let key = CacheKey::OptionalProperties;
if let Some(CachedResult::Properties(urns)) = self.get_cached(&key) {
return self
.query
.aspect()
.properties()
.iter()
.filter(|p| urns.contains(&p.urn().to_string()))
.collect();
}
let props = self.query.find_optional_properties();
let urns: Vec<String> = props.iter().map(|p| p.urn().to_string()).collect();
self.cache_result(key, CachedResult::Properties(urns));
props
}
pub fn find_required_properties(&mut self) -> Vec<&Property> {
let key = CacheKey::RequiredProperties;
if let Some(CachedResult::Properties(urns)) = self.get_cached(&key) {
return self
.query
.aspect()
.properties()
.iter()
.filter(|p| urns.contains(&p.urn().to_string()))
.collect();
}
let props = self.query.find_required_properties();
let urns: Vec<String> = props.iter().map(|p| p.urn().to_string()).collect();
self.cache_result(key, CachedResult::Properties(urns));
props
}
pub fn find_properties_with_collection_characteristic(&mut self) -> Vec<&Property> {
let key = CacheKey::CollectionProperties;
if let Some(CachedResult::Properties(urns)) = self.get_cached(&key) {
return self
.query
.aspect()
.properties()
.iter()
.filter(|p| urns.contains(&p.urn().to_string()))
.collect();
}
let props = self.query.find_properties_with_collection_characteristic();
let urns: Vec<String> = props.iter().map(|p| p.urn().to_string()).collect();
self.cache_result(key, CachedResult::Properties(urns));
props
}
pub fn build_dependency_graph(&mut self) -> Vec<Dependency> {
let key = CacheKey::DependencyGraph;
if let Some(CachedResult::Dependencies(deps)) = self.get_cached(&key) {
return deps;
}
let deps = self.query.build_dependency_graph();
self.cache_result(key, CachedResult::Dependencies(deps.clone()));
deps
}
pub fn detect_circular_dependencies(&mut self) -> Vec<Vec<String>> {
let key = CacheKey::CircularDependencies;
self.query.detect_circular_dependencies()
}
pub fn clear_cache(&mut self) {
let mut cache = self
.cache
.write()
.expect("cache mutex should not be poisoned");
cache.clear();
}
pub fn cache_statistics(&self) -> CacheStatistics {
let hits = *self.hits.read().expect("hits mutex should not be poisoned");
let misses = *self
.misses
.read()
.expect("misses mutex should not be poisoned");
let total = hits + misses;
let hit_rate = if total == 0 {
0.0
} else {
hits as f64 / total as f64
};
let cache = self
.cache
.read()
.expect("cache mutex should not be poisoned");
CacheStatistics {
size: cache.len(),
capacity: self.max_cache_size,
hits,
misses,
hit_rate,
}
}
fn get_cached(&self, key: &CacheKey) -> Option<CachedResult> {
let mut cache = self
.cache
.write()
.expect("cache mutex should not be poisoned");
if let Some(entry) = cache.get_mut(key) {
entry.access_count += 1;
entry.last_accessed = std::time::Instant::now();
*self
.hits
.write()
.expect("write lock should not be poisoned") += 1;
Some(entry.value.clone())
} else {
*self
.misses
.write()
.expect("write lock should not be poisoned") += 1;
None
}
}
fn cache_result(&self, key: CacheKey, value: CachedResult) {
let mut cache = self
.cache
.write()
.expect("cache mutex should not be poisoned");
if cache.len() >= self.max_cache_size && !cache.contains_key(&key) {
if let Some((lru_key, _)) = cache
.iter()
.min_by_key(|(_, v)| v.last_accessed)
.map(|(k, v)| (k.clone(), v.clone()))
{
cache.remove(&lru_key);
}
}
cache.insert(
key,
CachedValue {
value,
access_count: 0,
last_accessed: std::time::Instant::now(),
},
);
}
pub fn query(&self) -> &ModelQuery<'a> {
&self.query
}
}
#[derive(Debug, Clone)]
pub struct CacheStatistics {
pub size: usize,
pub capacity: usize,
pub hits: usize,
pub misses: usize,
pub hit_rate: f64,
}
impl CacheStatistics {
pub fn total_accesses(&self) -> usize {
self.hits + self.misses
}
pub fn fill_percentage(&self) -> f64 {
if self.capacity == 0 {
0.0
} else {
(self.size as f64 / self.capacity as f64) * 100.0
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::metamodel::{Characteristic, CharacteristicKind};
fn create_test_aspect() -> Aspect {
let mut aspect = Aspect::new("urn:samm:test:1.0.0#TestAspect".to_string());
let mut prop1 = Property::new("urn:samm:test:1.0.0#prop1".to_string());
prop1.optional = false;
let mut prop2 = Property::new("urn:samm:test:1.0.0#prop2".to_string());
prop2.optional = true;
let mut prop3 = Property::new("urn:samm:test:1.0.0#prop3".to_string());
prop3.is_collection = true;
let collection_char = Characteristic::new(
"urn:samm:test:1.0.0#CollectionChar".to_string(),
CharacteristicKind::Collection {
element_characteristic: None,
},
);
prop3.characteristic = Some(collection_char);
aspect.add_property(prop1);
aspect.add_property(prop2);
aspect.add_property(prop3);
aspect
}
#[test]
fn test_cached_complexity_metrics() {
let aspect = create_test_aspect();
let mut query = CachedModelQuery::new(&aspect, 10);
let metrics1 = query.complexity_metrics();
assert_eq!(metrics1.total_properties, 3);
let stats1 = query.cache_statistics();
assert_eq!(stats1.misses, 1);
assert_eq!(stats1.hits, 0);
let metrics2 = query.complexity_metrics();
assert_eq!(metrics2.total_properties, 3);
let stats2 = query.cache_statistics();
assert_eq!(stats2.misses, 1);
assert_eq!(stats2.hits, 1);
assert!((stats2.hit_rate - 0.5).abs() < 0.01);
}
#[test]
fn test_cached_optional_properties() {
let aspect = create_test_aspect();
let mut query = CachedModelQuery::new(&aspect, 10);
let props1 = query.find_optional_properties();
assert_eq!(props1.len(), 1);
let stats1 = query.cache_statistics();
assert_eq!(stats1.misses, 1);
let props2 = query.find_optional_properties();
assert_eq!(props2.len(), 1);
let stats2 = query.cache_statistics();
assert_eq!(stats2.hits, 1);
}
#[test]
fn test_cached_required_properties() {
let aspect = create_test_aspect();
let mut query = CachedModelQuery::new(&aspect, 10);
let props1 = query.find_required_properties();
assert_eq!(props1.len(), 2);
let props2 = query.find_required_properties();
assert_eq!(props2.len(), 2);
let stats = query.cache_statistics();
assert_eq!(stats.hits, 1);
assert_eq!(stats.misses, 1);
}
#[test]
fn test_cache_clear() {
let aspect = create_test_aspect();
let mut query = CachedModelQuery::new(&aspect, 10);
query.complexity_metrics();
query.complexity_metrics();
let stats_before = query.cache_statistics();
assert_eq!(stats_before.size, 1);
query.clear_cache();
let stats_after = query.cache_statistics();
assert_eq!(stats_after.size, 0);
assert_eq!(stats_after.hits, 1); }
#[test]
fn test_cache_lru_eviction() {
let aspect = create_test_aspect();
let mut query = CachedModelQuery::new(&aspect, 2);
query.complexity_metrics();
query.find_optional_properties();
let stats1 = query.cache_statistics();
assert_eq!(stats1.size, 2);
query.find_required_properties();
let stats2 = query.cache_statistics();
assert_eq!(stats2.size, 2); }
#[test]
fn test_cache_statistics() {
let aspect = create_test_aspect();
let mut query = CachedModelQuery::new(&aspect, 10);
query.complexity_metrics();
query.complexity_metrics();
query.find_optional_properties();
let stats = query.cache_statistics();
assert_eq!(stats.total_accesses(), 3);
assert_eq!(stats.hits, 1);
assert_eq!(stats.misses, 2);
assert!((stats.hit_rate - 0.333).abs() < 0.01);
assert_eq!(stats.size, 2);
}
#[test]
fn test_collection_properties_caching() {
let aspect = create_test_aspect();
let mut query = CachedModelQuery::new(&aspect, 10);
let props1 = query.find_properties_with_collection_characteristic();
assert_eq!(props1.len(), 1);
let props2 = query.find_properties_with_collection_characteristic();
assert_eq!(props2.len(), 1);
let stats = query.cache_statistics();
assert_eq!(stats.hits, 1);
}
#[test]
fn test_dependency_graph_caching() {
let mut aspect = Aspect::new("urn:samm:test:1.0.0#TestAspect".to_string());
let mut prop = Property::new("urn:samm:test:1.0.0#prop1".to_string());
let char = Characteristic::new(
"urn:samm:test:1.0.0#Char1".to_string(),
CharacteristicKind::Trait,
);
prop.characteristic = Some(char);
aspect.add_property(prop);
let mut query = CachedModelQuery::new(&aspect, 10);
let deps1 = query.build_dependency_graph();
assert!(!deps1.is_empty());
let deps2 = query.build_dependency_graph();
assert!(!deps2.is_empty());
let stats = query.cache_statistics();
assert_eq!(stats.hits, 1);
}
}