1#[cfg(feature = "schema")]
7use schemars::JsonSchema;
8use serde::{Deserialize, Serialize};
9
10#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
12#[cfg_attr(feature = "schema", derive(JsonSchema))]
13pub struct CacheStats {
14 pub size: usize,
16 pub capacity: usize,
18 pub hits: u64,
20 pub misses: u64,
22 pub hit_rate: f64,
24}
25
26impl CacheStats {
27 #[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 #[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 #[must_use]
77 pub fn is_full(&self) -> bool {
78 self.size >= self.capacity
79 }
80
81 #[must_use]
83 pub fn is_empty(&self) -> bool {
84 self.size == 0
85 }
86
87 #[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 #[must_use]
100 pub fn total_requests(&self) -> u64 {
101 self.hits + self.misses
102 }
103
104 #[must_use]
106 pub fn miss_rate(&self) -> f64 {
107 1.0 - self.hit_rate
108 }
109
110 #[must_use]
132 pub fn efficiency_score(&self) -> f64 {
133 let hit_score = self.hit_rate * 70.0; let util_score = self.fill_percentage() * 30.0; hit_score + util_score
136 }
137}
138
139impl Default for CacheStats {
140 fn default() -> Self {
141 Self::empty(0)
142 }
143}
144
145#[derive(Debug, Clone, Serialize, Deserialize)]
147#[cfg_attr(feature = "schema", derive(JsonSchema))]
148pub struct TieredCacheStats {
149 pub l1_stats: CacheStats,
151 pub l2_stats: CacheStats,
153 pub promotions: u64,
155}
156
157impl TieredCacheStats {
158 #[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 #[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 #[must_use]
185 pub fn total_size(&self) -> usize {
186 self.l1_stats.size + self.l2_stats.size
187 }
188
189 #[must_use]
191 pub fn total_capacity(&self) -> usize {
192 self.l1_stats.capacity + self.l2_stats.capacity
193 }
194}
195
196#[derive(Debug, Clone, Serialize, Deserialize)]
198#[cfg_attr(feature = "schema", derive(JsonSchema))]
199pub struct SizedCacheStats {
200 pub entry_count: usize,
202 pub current_size_bytes: usize,
204 pub max_size_bytes: usize,
206 pub evictions: u64,
208 pub insertions: u64,
210}
211
212impl SizedCacheStats {
213 #[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 #[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 #[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 #[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 #[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#[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 #[must_use]
289 pub fn new() -> Self {
290 Self::default()
291 }
292
293 pub fn size(mut self, size: usize) -> Self {
295 self.size = Some(size);
296 self
297 }
298
299 pub fn capacity(mut self, capacity: usize) -> Self {
301 self.capacity = Some(capacity);
302 self
303 }
304
305 pub fn hits(mut self, hits: u64) -> Self {
307 self.hits = Some(hits);
308 self
309 }
310
311 pub fn misses(mut self, misses: u64) -> Self {
313 self.misses = Some(misses);
314 self
315 }
316
317 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#[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 #[must_use]
341 pub fn new() -> Self {
342 Self::default()
343 }
344
345 pub fn entry_count(mut self, count: usize) -> Self {
347 self.entry_count = Some(count);
348 self
349 }
350
351 pub fn current_size_bytes(mut self, size: usize) -> Self {
353 self.current_size_bytes = Some(size);
354 self
355 }
356
357 pub fn max_size_bytes(mut self, size: usize) -> Self {
359 self.max_size_bytes = Some(size);
360 self
361 }
362
363 pub fn evictions(mut self, evictions: u64) -> Self {
365 self.evictions = Some(evictions);
366 self
367 }
368
369 pub fn insertions(mut self, insertions: u64) -> Self {
371 self.insertions = Some(insertions);
372 self
373 }
374
375 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 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 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}