chie_shared/types/
cache.rs

1//! Cache-related types for CHIE Protocol.
2//!
3//! This module provides shared types for cache statistics and metrics
4//! that can be used across different caching implementations.
5
6#[cfg(feature = "schema")]
7use schemars::JsonSchema;
8use serde::{Deserialize, Serialize};
9
10/// Cache statistics.
11#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
12#[cfg_attr(feature = "schema", derive(JsonSchema))]
13pub struct CacheStats {
14    /// Current number of entries.
15    pub size: usize,
16    /// Maximum capacity.
17    pub capacity: usize,
18    /// Total cache hits.
19    pub hits: u64,
20    /// Total cache misses.
21    pub misses: u64,
22    /// Hit rate (0.0 to 1.0).
23    pub hit_rate: f64,
24}
25
26impl CacheStats {
27    /// Create new cache statistics.
28    ///
29    /// # Example
30    ///
31    /// ```
32    /// use chie_shared::types::cache::CacheStats;
33    ///
34    /// let stats = CacheStats::new(750, 1000, 8500, 1500);
35    ///
36    /// assert_eq!(stats.size, 750);
37    /// assert_eq!(stats.capacity, 1000);
38    /// assert_eq!(stats.hits, 8500);
39    /// assert_eq!(stats.misses, 1500);
40    /// assert_eq!(stats.hit_rate, 0.85);
41    /// assert_eq!(stats.total_requests(), 10000);
42    /// assert!((stats.miss_rate() - 0.15).abs() < 1e-10);
43    /// assert_eq!(stats.fill_percentage(), 0.75);
44    /// ```
45    #[must_use]
46    #[allow(clippy::cast_precision_loss)]
47    pub fn new(size: usize, capacity: usize, hits: u64, misses: u64) -> Self {
48        let hit_rate = if hits + misses > 0 {
49            hits as f64 / (hits + misses) as f64
50        } else {
51            0.0
52        };
53
54        Self {
55            size,
56            capacity,
57            hits,
58            misses,
59            hit_rate,
60        }
61    }
62
63    /// Create empty cache statistics.
64    #[must_use]
65    pub fn empty(capacity: usize) -> Self {
66        Self {
67            size: 0,
68            capacity,
69            hits: 0,
70            misses: 0,
71            hit_rate: 0.0,
72        }
73    }
74
75    /// Check if cache is full.
76    #[must_use]
77    pub fn is_full(&self) -> bool {
78        self.size >= self.capacity
79    }
80
81    /// Check if cache is empty.
82    #[must_use]
83    pub fn is_empty(&self) -> bool {
84        self.size == 0
85    }
86
87    /// Get the fill percentage (0.0 to 1.0).
88    #[must_use]
89    #[allow(clippy::cast_precision_loss)]
90    pub fn fill_percentage(&self) -> f64 {
91        if self.capacity == 0 {
92            0.0
93        } else {
94            self.size as f64 / self.capacity as f64
95        }
96    }
97
98    /// Get total requests (hits + misses).
99    #[must_use]
100    pub fn total_requests(&self) -> u64 {
101        self.hits + self.misses
102    }
103
104    /// Get miss rate (0.0 to 1.0).
105    #[must_use]
106    pub fn miss_rate(&self) -> f64 {
107        1.0 - self.hit_rate
108    }
109
110    /// Calculate efficiency score (0.0 to 100.0).
111    ///
112    /// Combines hit rate (70% weight) and capacity utilization (30% weight).
113    ///
114    /// # Example
115    ///
116    /// ```
117    /// use chie_shared::types::cache::CacheStats;
118    ///
119    /// // High efficiency: good hit rate and good utilization
120    /// let stats1 = CacheStats::new(900, 1000, 9000, 1000);
121    /// assert_eq!(stats1.efficiency_score(), 90.0 * 0.7 + 0.9 * 30.0);
122    ///
123    /// // Medium efficiency: good hit rate but low utilization
124    /// let stats2 = CacheStats::new(300, 1000, 900, 100);
125    /// assert_eq!(stats2.efficiency_score(), 0.9 * 70.0 + 0.3 * 30.0);
126    ///
127    /// // Low efficiency: poor hit rate
128    /// let stats3 = CacheStats::new(500, 1000, 300, 700);
129    /// assert_eq!(stats3.efficiency_score(), 0.3 * 70.0 + 0.5 * 30.0);
130    /// ```
131    #[must_use]
132    pub fn efficiency_score(&self) -> f64 {
133        let hit_score = self.hit_rate * 70.0; // 70% weight on hit rate
134        let util_score = self.fill_percentage() * 30.0; // 30% weight on utilization
135        hit_score + util_score
136    }
137}
138
139impl Default for CacheStats {
140    fn default() -> Self {
141        Self::empty(0)
142    }
143}
144
145/// Multi-level cache statistics.
146#[derive(Debug, Clone, Serialize, Deserialize)]
147#[cfg_attr(feature = "schema", derive(JsonSchema))]
148pub struct TieredCacheStats {
149    /// L1 cache statistics.
150    pub l1_stats: CacheStats,
151    /// L2 cache statistics.
152    pub l2_stats: CacheStats,
153    /// L1 to L2 promotion count.
154    pub promotions: u64,
155}
156
157impl TieredCacheStats {
158    /// Create new tiered cache statistics.
159    #[must_use]
160    pub fn new(l1_stats: CacheStats, l2_stats: CacheStats, promotions: u64) -> Self {
161        Self {
162            l1_stats,
163            l2_stats,
164            promotions,
165        }
166    }
167
168    /// Get combined hit rate across both levels.
169    #[must_use]
170    #[allow(clippy::cast_precision_loss)]
171    pub fn combined_hit_rate(&self) -> f64 {
172        let total_hits = self.l1_stats.hits + self.l2_stats.hits;
173        let total_misses = self.l1_stats.misses + self.l2_stats.misses;
174        let total = total_hits + total_misses;
175
176        if total == 0 {
177            0.0
178        } else {
179            total_hits as f64 / total as f64
180        }
181    }
182
183    /// Get total size across both levels.
184    #[must_use]
185    pub fn total_size(&self) -> usize {
186        self.l1_stats.size + self.l2_stats.size
187    }
188
189    /// Get total capacity across both levels.
190    #[must_use]
191    pub fn total_capacity(&self) -> usize {
192        self.l1_stats.capacity + self.l2_stats.capacity
193    }
194}
195
196/// Size-based cache statistics with byte tracking.
197#[derive(Debug, Clone, Serialize, Deserialize)]
198#[cfg_attr(feature = "schema", derive(JsonSchema))]
199pub struct SizedCacheStats {
200    /// Number of entries.
201    pub entry_count: usize,
202    /// Current size in bytes.
203    pub current_size_bytes: usize,
204    /// Maximum size in bytes.
205    pub max_size_bytes: usize,
206    /// Total evictions.
207    pub evictions: u64,
208    /// Total insertions.
209    pub insertions: u64,
210}
211
212impl SizedCacheStats {
213    /// Create new sized cache statistics.
214    #[must_use]
215    pub fn new(
216        entry_count: usize,
217        current_size_bytes: usize,
218        max_size_bytes: usize,
219        evictions: u64,
220        insertions: u64,
221    ) -> Self {
222        Self {
223            entry_count,
224            current_size_bytes,
225            max_size_bytes,
226            evictions,
227            insertions,
228        }
229    }
230
231    /// Get size utilization percentage (0.0 to 1.0).
232    #[must_use]
233    #[allow(clippy::cast_precision_loss)]
234    pub fn utilization(&self) -> f64 {
235        if self.max_size_bytes == 0 {
236            0.0
237        } else {
238            self.current_size_bytes as f64 / self.max_size_bytes as f64
239        }
240    }
241
242    /// Get average entry size in bytes.
243    #[must_use]
244    #[allow(clippy::cast_precision_loss)]
245    pub fn avg_entry_size(&self) -> f64 {
246        if self.entry_count == 0 {
247            0.0
248        } else {
249            self.current_size_bytes as f64 / self.entry_count as f64
250        }
251    }
252
253    /// Get eviction rate (evictions per insertion).
254    #[must_use]
255    #[allow(clippy::cast_precision_loss)]
256    pub fn eviction_rate(&self) -> f64 {
257        if self.insertions == 0 {
258            0.0
259        } else {
260            self.evictions as f64 / self.insertions as f64
261        }
262    }
263
264    /// Check if cache is nearly full (>90% utilization).
265    #[must_use]
266    pub fn is_nearly_full(&self) -> bool {
267        self.utilization() > 0.9
268    }
269}
270
271impl Default for SizedCacheStats {
272    fn default() -> Self {
273        Self::new(0, 0, 0, 0, 0)
274    }
275}
276
277/// Builder for CacheStats with fluent API.
278#[derive(Debug, Default)]
279pub struct CacheStatsBuilder {
280    size: Option<usize>,
281    capacity: Option<usize>,
282    hits: Option<u64>,
283    misses: Option<u64>,
284}
285
286impl CacheStatsBuilder {
287    /// Create a new builder.
288    #[must_use]
289    pub fn new() -> Self {
290        Self::default()
291    }
292
293    /// Set the current size.
294    pub fn size(mut self, size: usize) -> Self {
295        self.size = Some(size);
296        self
297    }
298
299    /// Set the capacity.
300    pub fn capacity(mut self, capacity: usize) -> Self {
301        self.capacity = Some(capacity);
302        self
303    }
304
305    /// Set the hit count.
306    pub fn hits(mut self, hits: u64) -> Self {
307        self.hits = Some(hits);
308        self
309    }
310
311    /// Set the miss count.
312    pub fn misses(mut self, misses: u64) -> Self {
313        self.misses = Some(misses);
314        self
315    }
316
317    /// Build the CacheStats.
318    pub fn build(self) -> CacheStats {
319        CacheStats::new(
320            self.size.unwrap_or(0),
321            self.capacity.unwrap_or(0),
322            self.hits.unwrap_or(0),
323            self.misses.unwrap_or(0),
324        )
325    }
326}
327
328/// Builder for SizedCacheStats with fluent API.
329#[derive(Debug, Default)]
330pub struct SizedCacheStatsBuilder {
331    entry_count: Option<usize>,
332    current_size_bytes: Option<usize>,
333    max_size_bytes: Option<usize>,
334    evictions: Option<u64>,
335    insertions: Option<u64>,
336}
337
338impl SizedCacheStatsBuilder {
339    /// Create a new builder.
340    #[must_use]
341    pub fn new() -> Self {
342        Self::default()
343    }
344
345    /// Set the entry count.
346    pub fn entry_count(mut self, count: usize) -> Self {
347        self.entry_count = Some(count);
348        self
349    }
350
351    /// Set the current size in bytes.
352    pub fn current_size_bytes(mut self, size: usize) -> Self {
353        self.current_size_bytes = Some(size);
354        self
355    }
356
357    /// Set the maximum size in bytes.
358    pub fn max_size_bytes(mut self, size: usize) -> Self {
359        self.max_size_bytes = Some(size);
360        self
361    }
362
363    /// Set the eviction count.
364    pub fn evictions(mut self, evictions: u64) -> Self {
365        self.evictions = Some(evictions);
366        self
367    }
368
369    /// Set the insertion count.
370    pub fn insertions(mut self, insertions: u64) -> Self {
371        self.insertions = Some(insertions);
372        self
373    }
374
375    /// Build the SizedCacheStats.
376    pub fn build(self) -> SizedCacheStats {
377        SizedCacheStats::new(
378            self.entry_count.unwrap_or(0),
379            self.current_size_bytes.unwrap_or(0),
380            self.max_size_bytes.unwrap_or(0),
381            self.evictions.unwrap_or(0),
382            self.insertions.unwrap_or(0),
383        )
384    }
385}
386
387#[cfg(test)]
388mod tests {
389    use super::*;
390
391    #[test]
392    fn test_cache_stats_new() {
393        let stats = CacheStats::new(50, 100, 80, 20);
394        assert_eq!(stats.size, 50);
395        assert_eq!(stats.capacity, 100);
396        assert_eq!(stats.hits, 80);
397        assert_eq!(stats.misses, 20);
398        assert_eq!(stats.hit_rate, 0.8);
399    }
400
401    #[test]
402    fn test_cache_stats_empty() {
403        let stats = CacheStats::empty(100);
404        assert_eq!(stats.size, 0);
405        assert_eq!(stats.capacity, 100);
406        assert_eq!(stats.hit_rate, 0.0);
407        assert!(stats.is_empty());
408        assert!(!stats.is_full());
409    }
410
411    #[test]
412    fn test_cache_stats_is_full() {
413        let stats = CacheStats::new(100, 100, 0, 0);
414        assert!(stats.is_full());
415
416        let stats2 = CacheStats::new(99, 100, 0, 0);
417        assert!(!stats2.is_full());
418    }
419
420    #[test]
421    fn test_cache_stats_fill_percentage() {
422        let stats = CacheStats::new(50, 100, 0, 0);
423        assert_eq!(stats.fill_percentage(), 0.5);
424
425        let stats2 = CacheStats::new(75, 100, 0, 0);
426        assert_eq!(stats2.fill_percentage(), 0.75);
427    }
428
429    #[test]
430    fn test_cache_stats_total_requests() {
431        let stats = CacheStats::new(50, 100, 80, 20);
432        assert_eq!(stats.total_requests(), 100);
433    }
434
435    #[test]
436    fn test_cache_stats_miss_rate() {
437        let stats = CacheStats::new(50, 100, 80, 20);
438        assert!((stats.miss_rate() - 0.2).abs() < 0.0001);
439    }
440
441    #[test]
442    fn test_cache_stats_efficiency_score() {
443        let stats = CacheStats::new(50, 100, 80, 20);
444        // Hit rate 0.8 * 70 + fill 0.5 * 30 = 56 + 15 = 71
445        let expected = 0.8 * 70.0 + 0.5 * 30.0;
446        assert!((stats.efficiency_score() - expected).abs() < 0.001);
447    }
448
449    #[test]
450    fn test_tiered_cache_stats() {
451        let l1 = CacheStats::new(10, 20, 80, 20);
452        let l2 = CacheStats::new(50, 100, 40, 10);
453        let tiered = TieredCacheStats::new(l1, l2, 5);
454
455        assert_eq!(tiered.total_size(), 60);
456        assert_eq!(tiered.total_capacity(), 120);
457        assert_eq!(tiered.promotions, 5);
458
459        // Combined hit rate: (80 + 40) / (80 + 20 + 40 + 10) = 120 / 150 = 0.8
460        assert!((tiered.combined_hit_rate() - 0.8).abs() < 0.001);
461    }
462
463    #[test]
464    fn test_sized_cache_stats() {
465        let stats = SizedCacheStats::new(100, 50_000, 100_000, 20, 120);
466
467        assert_eq!(stats.entry_count, 100);
468        assert_eq!(stats.current_size_bytes, 50_000);
469        assert_eq!(stats.max_size_bytes, 100_000);
470        assert_eq!(stats.utilization(), 0.5);
471        assert_eq!(stats.avg_entry_size(), 500.0);
472        assert!((stats.eviction_rate() - (20.0 / 120.0)).abs() < 0.001);
473        assert!(!stats.is_nearly_full());
474    }
475
476    #[test]
477    fn test_sized_cache_stats_nearly_full() {
478        let stats = SizedCacheStats::new(100, 95_000, 100_000, 0, 100);
479        assert!(stats.is_nearly_full());
480
481        let stats2 = SizedCacheStats::new(100, 89_000, 100_000, 0, 100);
482        assert!(!stats2.is_nearly_full());
483    }
484
485    #[test]
486    fn test_cache_stats_serialization() {
487        let stats = CacheStats::new(50, 100, 80, 20);
488        let json = serde_json::to_string(&stats).unwrap();
489        let deserialized: CacheStats = serde_json::from_str(&json).unwrap();
490        assert_eq!(stats, deserialized);
491    }
492
493    #[test]
494    fn test_cache_stats_default() {
495        let stats = CacheStats::default();
496        assert_eq!(stats.size, 0);
497        assert_eq!(stats.capacity, 0);
498        assert_eq!(stats.hits, 0);
499        assert_eq!(stats.misses, 0);
500        assert_eq!(stats.hit_rate, 0.0);
501    }
502
503    #[test]
504    fn test_cache_stats_builder() {
505        let stats = CacheStatsBuilder::new()
506            .size(50)
507            .capacity(100)
508            .hits(80)
509            .misses(20)
510            .build();
511
512        assert_eq!(stats.size, 50);
513        assert_eq!(stats.capacity, 100);
514        assert_eq!(stats.hits, 80);
515        assert_eq!(stats.misses, 20);
516        assert_eq!(stats.hit_rate, 0.8);
517    }
518
519    #[test]
520    fn test_cache_stats_builder_partial() {
521        let stats = CacheStatsBuilder::new().capacity(100).hits(50).build();
522
523        assert_eq!(stats.size, 0);
524        assert_eq!(stats.capacity, 100);
525        assert_eq!(stats.hits, 50);
526    }
527
528    #[test]
529    fn test_sized_cache_stats_builder() {
530        let stats = SizedCacheStatsBuilder::new()
531            .entry_count(100)
532            .current_size_bytes(50_000)
533            .max_size_bytes(100_000)
534            .evictions(20)
535            .insertions(120)
536            .build();
537
538        assert_eq!(stats.entry_count, 100);
539        assert_eq!(stats.current_size_bytes, 50_000);
540        assert_eq!(stats.max_size_bytes, 100_000);
541        assert_eq!(stats.evictions, 20);
542        assert_eq!(stats.insertions, 120);
543    }
544}