use libdictenstein::{
Dictionary, DictionaryNode, DictionaryValue, MappedDictionary, MappedDictionaryNode,
SyncStrategy,
};
use std::collections::HashMap;
use std::sync::{Arc, RwLock};
#[derive(Debug, Clone)]
struct EntryMetadata {
size_bytes: usize,
hit_count: u32,
access_count: u32,
}
impl EntryMetadata {
fn new(size: usize) -> Self {
Self {
size_bytes: size,
hit_count: 1, access_count: 1,
}
}
fn increment(&mut self) {
self.hit_count = self.hit_count.saturating_add(1);
self.access_count = self.access_count.saturating_add(1);
}
fn hit_rate(&self) -> f64 {
if self.access_count == 0 {
0.0
} else {
self.hit_count as f64 / self.access_count as f64
}
}
fn memory_pressure_score(&self) -> f64 {
let size = self.size_bytes as f64;
let hit_rate = self.hit_rate();
size / (hit_rate + 0.1)
}
}
#[derive(Clone)]
pub struct MemoryPressure<D> {
inner: D,
metadata: Arc<RwLock<HashMap<String, EntryMetadata>>>,
}
impl<D> MemoryPressure<D> {
pub fn new(dict: D) -> Self {
Self {
inner: dict,
metadata: Arc::new(RwLock::new(HashMap::new())),
}
}
#[inline]
pub fn into_inner(self) -> D {
self.inner
}
#[inline]
pub fn inner(&self) -> &D {
&self.inner
}
fn record_access<V: DictionaryValue>(&self, term: &str, _value: &V) {
let size = std::mem::size_of::<V>();
let mut metadata = self
.metadata
.write()
.expect("poisoned RwLock; only fatal if writer panicked");
metadata
.entry(term.to_string())
.and_modify(|m| m.increment())
.or_insert_with(|| EntryMetadata::new(size));
}
pub fn memory_pressure_score(&self, term: &str) -> Option<f64> {
let metadata = self
.metadata
.read()
.expect("poisoned RwLock; only fatal if writer panicked");
metadata.get(term).map(|m| m.memory_pressure_score())
}
pub fn find_highest_pressure(&self, terms: &[&str]) -> Option<String> {
let metadata = self
.metadata
.read()
.expect("poisoned RwLock; only fatal if writer panicked");
terms
.iter()
.filter_map(|&term| {
metadata
.get(term)
.map(|m| (term, m.memory_pressure_score()))
})
.max_by(|(_, score1), (_, score2)| {
score1
.partial_cmp(score2)
.unwrap_or(std::cmp::Ordering::Equal)
})
.map(|(term, _)| term.to_string())
}
pub fn evict_highest_pressure(&self, terms: &[&str]) -> Option<String> {
if let Some(high_pressure_term) = self.find_highest_pressure(terms) {
let mut metadata = self
.metadata
.write()
.expect("poisoned RwLock; only fatal if writer panicked");
metadata.remove(&high_pressure_term);
Some(high_pressure_term)
} else {
None
}
}
pub fn clear_metadata(&self) {
let mut metadata = self
.metadata
.write()
.expect("poisoned RwLock; only fatal if writer panicked");
metadata.clear();
}
}
impl<D> Dictionary for MemoryPressure<D>
where
D: Dictionary,
{
type Node = MemoryPressureNode<D::Node>;
#[inline]
fn root(&self) -> Self::Node {
MemoryPressureNode::new(self.inner.root(), Arc::clone(&self.metadata))
}
#[inline]
fn len(&self) -> Option<usize> {
self.inner.len()
}
#[inline]
fn contains(&self, term: &str) -> bool {
self.inner.contains(term)
}
#[inline]
fn sync_strategy(&self) -> SyncStrategy {
self.inner.sync_strategy()
}
}
impl<D, V> MappedDictionary for MemoryPressure<D>
where
D: MappedDictionary<Value = V>,
V: DictionaryValue,
{
type Value = V;
#[inline]
fn get_value(&self, term: &str) -> Option<Self::Value> {
let value = self.inner.get_value(term)?;
self.record_access(term, &value);
Some(value)
}
#[inline]
fn contains_with_value<F>(&self, term: &str, predicate: F) -> bool
where
F: Fn(&Self::Value) -> bool,
{
self.inner.contains_with_value(term, predicate)
}
}
#[derive(Clone)]
pub struct MemoryPressureNode<N> {
inner: N,
metadata: Arc<RwLock<HashMap<String, EntryMetadata>>>,
}
impl<N> MemoryPressureNode<N> {
fn new(inner: N, metadata: Arc<RwLock<HashMap<String, EntryMetadata>>>) -> Self {
Self { inner, metadata }
}
}
impl<N> DictionaryNode for MemoryPressureNode<N>
where
N: DictionaryNode,
{
type Unit = N::Unit;
#[inline]
fn is_final(&self) -> bool {
self.inner.is_final()
}
#[inline]
fn transition(&self, label: Self::Unit) -> Option<Self> {
self.inner
.transition(label)
.map(|node| MemoryPressureNode::new(node, Arc::clone(&self.metadata)))
}
#[inline]
fn edges(&self) -> Box<dyn Iterator<Item = (Self::Unit, Self)> + '_> {
let metadata = Arc::clone(&self.metadata);
Box::new(self.inner.edges().map(move |(label, node)| {
(label, MemoryPressureNode::new(node, Arc::clone(&metadata)))
}))
}
#[inline]
fn edge_count(&self) -> Option<usize> {
self.inner.edge_count()
}
}
impl<N, V> MappedDictionaryNode for MemoryPressureNode<N>
where
N: MappedDictionaryNode<Value = V>,
V: DictionaryValue,
{
type Value = V;
#[inline]
fn value(&self) -> Option<Self::Value> {
self.inner.value()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(feature = "pathmap-backend")]
use libdictenstein::pathmap::PathMapDictionary;
#[test]
#[cfg(feature = "pathmap-backend")]
fn test_memory_pressure_wrapper_basic() {
let dict = PathMapDictionary::from_terms_with_values([("foo", 42), ("bar", 99)]);
let memory_pressure = MemoryPressure::new(dict);
assert_eq!(memory_pressure.get_value("foo"), Some(42));
assert_eq!(memory_pressure.get_value("bar"), Some(99));
assert!(memory_pressure.contains("foo"));
}
#[test]
#[cfg(feature = "pathmap-backend")]
fn test_memory_pressure_scoring() {
let dict = PathMapDictionary::from_terms_with_values([("foo", 42), ("bar", 99)]);
let memory_pressure = MemoryPressure::new(dict);
assert_eq!(memory_pressure.get_value("foo"), Some(42));
let score1 = memory_pressure
.memory_pressure_score("foo")
.expect("expected Some score in test");
assert_eq!(memory_pressure.get_value("bar"), Some(99));
assert_eq!(memory_pressure.get_value("bar"), Some(99));
assert_eq!(memory_pressure.get_value("bar"), Some(99));
let score2 = memory_pressure
.memory_pressure_score("bar")
.expect("expected Some score in test");
assert!((score1 - score2).abs() < 0.01);
}
#[test]
#[cfg(feature = "pathmap-backend")]
fn test_memory_pressure_find_highest() {
let dict =
PathMapDictionary::from_terms_with_values([("foo", 42), ("bar", 99), ("baz", 123)]);
let memory_pressure = MemoryPressure::new(dict);
assert_eq!(memory_pressure.get_value("foo"), Some(42));
assert_eq!(memory_pressure.get_value("bar"), Some(99));
assert_eq!(memory_pressure.get_value("bar"), Some(99));
assert_eq!(memory_pressure.get_value("bar"), Some(99));
for _ in 0..5 {
assert_eq!(memory_pressure.get_value("baz"), Some(123));
}
let highest = memory_pressure.find_highest_pressure(&["foo", "bar", "baz"]);
assert!(highest.is_some());
}
#[test]
#[cfg(feature = "pathmap-backend")]
fn test_memory_pressure_eviction() {
let dict = PathMapDictionary::from_terms_with_values([("foo", 42), ("bar", 99)]);
let memory_pressure = MemoryPressure::new(dict);
assert_eq!(memory_pressure.get_value("foo"), Some(42));
assert_eq!(memory_pressure.get_value("bar"), Some(99));
let evicted = memory_pressure.evict_highest_pressure(&["foo", "bar"]);
assert!(evicted.is_some());
assert_eq!(
memory_pressure.memory_pressure_score(&evicted.expect("expected Some evicted in test")),
None
);
}
#[test]
#[cfg(feature = "pathmap-backend")]
fn test_memory_pressure_clear_metadata() {
let dict = PathMapDictionary::from_terms_with_values([("foo", 42), ("bar", 99)]);
let memory_pressure = MemoryPressure::new(dict);
assert_eq!(memory_pressure.get_value("foo"), Some(42));
assert_eq!(memory_pressure.get_value("bar"), Some(99));
memory_pressure.clear_metadata();
assert_eq!(memory_pressure.memory_pressure_score("foo"), None);
assert_eq!(memory_pressure.memory_pressure_score("bar"), None);
}
#[test]
#[cfg(feature = "pathmap-backend")]
fn test_memory_pressure_node_traversal() {
let dict = PathMapDictionary::<()>::from_terms(["hello", "help"]);
let memory_pressure = MemoryPressure::new(dict);
let root = memory_pressure.root();
assert!(!root.is_final());
let h = root
.transition(b'h')
.expect("expected Some transition h in test");
let e = h
.transition(b'e')
.expect("expected Some transition e in test");
let l = e
.transition(b'l')
.expect("expected Some transition l in test");
let p = l
.transition(b'p')
.expect("expected Some transition p in test");
assert!(p.is_final()); }
#[test]
#[cfg(feature = "pathmap-backend")]
fn test_memory_pressure_into_inner() {
let dict = PathMapDictionary::from_terms_with_values([("foo", 42)]);
let memory_pressure = MemoryPressure::new(dict);
let original = memory_pressure.into_inner();
assert_eq!(original.len(), Some(1));
assert_eq!(original.get_value("foo"), Some(42));
}
#[test]
#[cfg(feature = "pathmap-backend")]
fn test_memory_pressure_hit_rate_tracking() {
let dict = PathMapDictionary::from_terms_with_values([("foo", 42)]);
let memory_pressure = MemoryPressure::new(dict);
assert_eq!(memory_pressure.get_value("foo"), Some(42));
let score1 = memory_pressure
.memory_pressure_score("foo")
.expect("expected Some score in test");
for _ in 0..5 {
assert_eq!(memory_pressure.get_value("foo"), Some(42));
}
let score2 = memory_pressure
.memory_pressure_score("foo")
.expect("expected Some score in test");
assert!((score1 - score2).abs() < 0.01);
}
}