oxihuman_core/
size_cache.rs1#![allow(dead_code)]
4
5use std::collections::HashMap;
8
9#[allow(dead_code)]
11#[derive(Debug, Clone)]
12pub struct SizeCacheEntry {
13 pub key: String,
14 pub size_bytes: usize,
15 pub access_count: u64,
16 pub last_access: u64,
17}
18
19#[allow(dead_code)]
21pub struct SizeCache {
22 entries: HashMap<String, SizeCacheEntry>,
23 budget_bytes: usize,
24 used_bytes: usize,
25 clock: u64,
26 evictions: u64,
27}
28
29#[allow(dead_code)]
30impl SizeCache {
31 pub fn new(budget_bytes: usize) -> Self {
32 Self {
33 entries: HashMap::new(),
34 budget_bytes,
35 used_bytes: 0,
36 clock: 0,
37 evictions: 0,
38 }
39 }
40
41 pub fn insert(&mut self, key: &str, size_bytes: usize) {
42 if let Some(old) = self.entries.remove(key) {
44 self.used_bytes = self.used_bytes.saturating_sub(old.size_bytes);
45 }
46 while !self.entries.is_empty() && self.used_bytes + size_bytes > self.budget_bytes {
48 self.evict_lru();
49 }
50 self.clock += 1;
51 self.used_bytes += size_bytes;
52 self.entries.insert(
53 key.to_string(),
54 SizeCacheEntry {
55 key: key.to_string(),
56 size_bytes,
57 access_count: 1,
58 last_access: self.clock,
59 },
60 );
61 }
62
63 pub fn get(&mut self, key: &str) -> Option<&SizeCacheEntry> {
64 self.clock += 1;
65 let t = self.clock;
66 if let Some(e) = self.entries.get_mut(key) {
67 e.access_count += 1;
68 e.last_access = t;
69 Some(e)
70 } else {
71 None
72 }
73 }
74
75 pub fn remove(&mut self, key: &str) -> bool {
76 if let Some(e) = self.entries.remove(key) {
77 self.used_bytes = self.used_bytes.saturating_sub(e.size_bytes);
78 true
79 } else {
80 false
81 }
82 }
83
84 pub fn contains(&self, key: &str) -> bool {
85 self.entries.contains_key(key)
86 }
87
88 pub fn used_bytes(&self) -> usize {
89 self.used_bytes
90 }
91
92 pub fn budget_bytes(&self) -> usize {
93 self.budget_bytes
94 }
95
96 pub fn count(&self) -> usize {
97 self.entries.len()
98 }
99
100 pub fn evictions(&self) -> u64 {
101 self.evictions
102 }
103
104 pub fn is_over_budget(&self) -> bool {
105 self.used_bytes > self.budget_bytes
106 }
107
108 pub fn clear(&mut self) {
109 self.entries.clear();
110 self.used_bytes = 0;
111 }
112
113 fn evict_lru(&mut self) {
114 let key = self
115 .entries
116 .values()
117 .min_by_key(|e| e.last_access)
118 .map(|e| e.key.clone());
119 if let Some(k) = key {
120 if let Some(e) = self.entries.remove(&k) {
121 self.used_bytes = self.used_bytes.saturating_sub(e.size_bytes);
122 self.evictions += 1;
123 }
124 }
125 }
126}
127
128impl Default for SizeCache {
129 fn default() -> Self {
130 Self::new(1024 * 1024)
131 }
132}
133
134pub fn new_size_cache(budget_bytes: usize) -> SizeCache {
135 SizeCache::new(budget_bytes)
136}
137
138#[cfg(test)]
139mod tests {
140 use super::*;
141
142 #[test]
143 fn insert_and_get() {
144 let mut c = new_size_cache(1000);
145 c.insert("a", 100);
146 assert!(c.contains("a"));
147 assert_eq!(c.used_bytes(), 100);
148 }
149
150 #[test]
151 fn remove_entry() {
152 let mut c = new_size_cache(1000);
153 c.insert("x", 50);
154 assert!(c.remove("x"));
155 assert!(!c.contains("x"));
156 assert_eq!(c.used_bytes(), 0);
157 }
158
159 #[test]
160 fn eviction_on_overflow() {
161 let mut c = new_size_cache(100);
162 c.insert("a", 60);
163 c.insert("b", 60); assert!(c.evictions() > 0);
165 assert!(c.used_bytes() <= 100);
166 }
167
168 #[test]
169 fn get_updates_access() {
170 let mut c = new_size_cache(1000);
171 c.insert("a", 10);
172 c.get("a");
173 assert_eq!(c.get("a").expect("should succeed").access_count, 3);
174 }
175
176 #[test]
177 fn clear_resets_used() {
178 let mut c = new_size_cache(1000);
179 c.insert("a", 200);
180 c.clear();
181 assert_eq!(c.used_bytes(), 0);
182 assert_eq!(c.count(), 0);
183 }
184
185 #[test]
186 fn over_budget_flag() {
187 let mut c = new_size_cache(10);
188 c.insert("big", 5);
190 assert!(!c.is_over_budget());
191 }
192
193 #[test]
194 fn duplicate_insert_reclaims() {
195 let mut c = new_size_cache(1000);
196 c.insert("k", 100);
197 c.insert("k", 50);
198 assert_eq!(c.used_bytes(), 50);
199 assert_eq!(c.count(), 1);
200 }
201
202 #[test]
203 fn budget_bytes() {
204 let c = new_size_cache(512);
205 assert_eq!(c.budget_bytes(), 512);
206 }
207
208 #[test]
209 fn evictions_counter() {
210 let mut c = new_size_cache(50);
211 c.insert("a", 40);
212 c.insert("b", 40);
213 assert!(c.evictions() >= 1);
214 }
215
216 #[test]
217 fn get_missing_returns_none() {
218 let mut c = new_size_cache(100);
219 assert!(c.get("missing").is_none());
220 }
221}