Skip to main content

oxihuman_core/
size_cache.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5//! A cache that tracks byte-size usage and evicts LRU entries when over budget.
6
7use std::collections::HashMap;
8
9/// An entry in the size cache.
10#[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/// Cache with a byte-size budget that evicts LRU entries.
20#[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        // Remove existing to reclaim space.
43        if let Some(old) = self.entries.remove(key) {
44            self.used_bytes = self.used_bytes.saturating_sub(old.size_bytes);
45        }
46        // Evict LRU until there is room.
47        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); // should evict "a"
164        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        // Force insert beyond budget by inserting matching single items.
189        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}