horon_engine/
semantic_index.rs1use std::collections::BTreeMap;
17use std::ops::Range;
18use std::sync::atomic::{AtomicU64, Ordering};
19use std::sync::{Arc, RwLock};
20
21use g_math::fixed_point::FixedPoint;
22
23use crate::constants::SEMANTIC_INDEX_MAX_SLICES;
24use crate::metric_tree::{EuclideanMetric, MetricVpTree};
25
26pub struct SliceIndex {
28 epoch: u64,
30 pub tree: MetricVpTree<Vec<FixedPoint>>,
32}
33
34pub struct SemanticIndexCache {
36 epoch: AtomicU64,
38 slices: RwLock<BTreeMap<(usize, usize), Arc<SliceIndex>>>,
40}
41
42impl SemanticIndexCache {
43 pub fn new() -> Self {
45 Self {
46 epoch: AtomicU64::new(0),
47 slices: RwLock::new(BTreeMap::new()),
48 }
49 }
50
51 pub fn bump(&self) {
54 self.epoch.fetch_add(1, Ordering::SeqCst);
55 }
56
57 pub fn epoch(&self) -> u64 {
59 self.epoch.load(Ordering::SeqCst)
60 }
61
62 pub fn get_or_build<F>(&self, dim_range: &Range<usize>, snapshot: F) -> Arc<SliceIndex>
68 where
69 F: FnOnce() -> Vec<(String, Vec<FixedPoint>)>,
70 {
71 let key = (dim_range.start, dim_range.end);
72
73 let current = self.epoch.load(Ordering::SeqCst);
75 {
76 let slices = self.slices.read().unwrap_or_else(|e| e.into_inner());
77 if let Some(idx) = slices.get(&key) {
78 if idx.epoch == current {
79 return Arc::clone(idx);
80 }
81 }
82 }
83
84 let build_epoch = self.epoch.load(Ordering::SeqCst);
87 let entries = snapshot();
88 let index = Arc::new(SliceIndex {
89 epoch: build_epoch,
90 tree: MetricVpTree::build(entries, &EuclideanMetric),
91 });
92
93 let mut slices = self.slices.write().unwrap_or_else(|e| e.into_inner());
94 let now = self.epoch.load(Ordering::SeqCst);
96 slices.retain(|_, idx| idx.epoch == now);
97 let entry = slices.entry(key).or_insert_with(|| Arc::clone(&index));
100 let result = Arc::clone(entry);
101
102 while slices.len() > SEMANTIC_INDEX_MAX_SLICES {
105 let evict = slices
106 .keys()
107 .find(|&&k| k != key)
108 .copied()
109 .expect("cache over capacity implies a second key exists");
110 slices.remove(&evict);
111 }
112
113 result
114 }
115
116 pub fn cached_slice_count(&self) -> usize {
118 self.slices.read().unwrap_or_else(|e| e.into_inner()).len()
119 }
120}
121
122impl Default for SemanticIndexCache {
123 fn default() -> Self {
124 Self::new()
125 }
126}
127
128#[cfg(test)]
129mod tests {
130 use super::*;
131
132 fn fp(v: i32) -> FixedPoint {
133 FixedPoint::from_int(v)
134 }
135
136 fn snapshot_of(n: usize) -> Vec<(String, Vec<FixedPoint>)> {
137 (0..n).map(|i| (format!("n{}", i), vec![fp(i as i32)])).collect()
138 }
139
140 #[test]
141 fn cache_hit_and_epoch_invalidation() {
142 let cache = SemanticIndexCache::new();
143 let range = 16..17;
144
145 let a = cache.get_or_build(&range, || snapshot_of(3));
146 let b = cache.get_or_build(&range, || panic!("must be served from cache"));
147 assert!(Arc::ptr_eq(&a, &b));
148
149 cache.bump();
150 let c = cache.get_or_build(&range, || snapshot_of(4));
151 assert!(!Arc::ptr_eq(&a, &c));
152 assert_eq!(c.tree.len(), 4);
153 }
154
155 #[test]
156 fn distinct_slices_get_distinct_trees() {
157 let cache = SemanticIndexCache::new();
158 let a = cache.get_or_build(&(16..18), || snapshot_of(2));
159 let b = cache.get_or_build(&(16..20), || snapshot_of(5));
160 assert!(!Arc::ptr_eq(&a, &b));
161 assert_eq!(cache.cached_slice_count(), 2);
162 }
163
164 #[test]
165 fn eviction_is_bounded_and_deterministic() {
166 let cache = SemanticIndexCache::new();
167 for i in 0..(SEMANTIC_INDEX_MAX_SLICES + 4) {
168 cache.get_or_build(&(i..(i + 1)), || snapshot_of(1));
169 }
170 assert!(cache.cached_slice_count() <= SEMANTIC_INDEX_MAX_SLICES);
171 let last = SEMANTIC_INDEX_MAX_SLICES + 3;
173 let again = cache.get_or_build(&(last..(last + 1)), || panic!("evicted the hot slice"));
174 assert_eq!(again.tree.len(), 1);
175 }
176}