1use crate::glyph_cache::GlyphCacheKey;
15use crate::shaping_cache::ShapingCacheKey;
16use moka::sync::Cache;
17use parking_lot::RwLock;
18use std::hash::Hash;
19use std::sync::Arc;
20use std::sync::OnceLock;
21use std::time::{Duration, Instant};
22
23static CACHE_MANAGER: OnceLock<CacheManager> = OnceLock::new();
25
26pub fn get_cache_manager() -> &'static CacheManager {
31 CACHE_MANAGER.get_or_init(CacheManager::new)
32}
33
34pub const DEFAULT_CACHE_MAX_BYTES: u64 = 512 * 1024 * 1024;
38
39pub struct MultiLevelCache<K, V>
44where
45 K: Hash + Eq + Send + Sync + Clone + 'static,
46 V: Clone + Send + Sync + 'static,
47{
48 cache: Cache<K, V>,
49 stats: Arc<RwLock<CacheMetrics>>,
50}
51
52impl<K, V> std::fmt::Debug for MultiLevelCache<K, V>
53where
54 K: Hash + Eq + Send + Sync + Clone + 'static,
55 V: Clone + Send + Sync + 'static,
56{
57 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
58 f.debug_struct("MultiLevelCache")
59 .field("entry_count", &self.cache.entry_count())
60 .field("hit_rate", &self.hit_rate())
61 .finish()
62 }
63}
64
65impl<K, V> MultiLevelCache<K, V>
66where
67 K: Hash + Eq + Send + Sync + Clone + 'static,
68 V: Clone + Send + Sync + 'static,
69{
70 pub fn new(l1_size: usize, l2_size: usize) -> Self {
75 let total_capacity = (l1_size + l2_size) as u64;
76 let cache = Cache::builder()
77 .max_capacity(total_capacity)
78 .eviction_policy(moka::policy::EvictionPolicy::tiny_lfu())
80 .time_to_idle(Duration::from_secs(600))
82 .build();
83
84 Self {
85 cache,
86 stats: Arc::new(RwLock::new(CacheMetrics::default())),
87 }
88 }
89
90 pub fn get(&self, key: &K) -> Option<V> {
95 let start = Instant::now();
96 let mut stats = self.stats.write();
97 stats.total_requests += 1;
98
99 if let Some(value) = self.cache.get(key) {
100 stats.l1_hits += 1; stats.total_l1_time += start.elapsed();
102 Some(value)
103 } else {
104 stats.misses += 1;
105 None
106 }
107 }
108
109 pub fn insert(&self, key: K, value: V) {
114 self.cache.insert(key, value);
115 }
116
117 pub fn hit_rate(&self) -> f64 {
119 let stats = self.stats.read();
120 if stats.total_requests == 0 {
121 0.0
122 } else {
123 let hits = stats.l1_hits + stats.l2_hits;
124 hits as f64 / stats.total_requests as f64
125 }
126 }
127
128 pub fn avg_access_time(&self) -> Duration {
130 let stats = self.stats.read();
131 let total_time = stats.total_l1_time + stats.total_l2_time;
132 let total_hits = stats.l1_hits + stats.l2_hits;
133 if total_hits == 0 {
134 Duration::ZERO
135 } else {
136 total_time / total_hits as u32
137 }
138 }
139
140 pub fn metrics(&self) -> CacheMetrics {
142 self.stats.read().clone()
143 }
144
145 pub fn len(&self) -> usize {
147 self.cache.entry_count() as usize
148 }
149
150 pub fn is_empty(&self) -> bool {
152 self.cache.entry_count() == 0
153 }
154
155 pub fn clear(&self) {
157 self.cache.invalidate_all();
158 let mut stats = self.stats.write();
159 *stats = CacheMetrics::default();
160 }
161
162 #[cfg(test)]
164 pub fn sync(&self) {
165 self.cache.run_pending_tasks();
166 }
167}
168
169#[derive(Debug, Clone)]
171pub struct CacheStats {
172 pub size: usize,
173 pub capacity: usize,
174 pub total_hits: u32,
175 pub hit_rate: f32,
176}
177
178#[derive(Debug, Clone, Default)]
180pub struct CacheMetrics {
181 pub total_requests: u64,
182 pub l1_hits: u64,
183 pub l2_hits: u64,
184 pub misses: u64,
185 pub total_l1_time: Duration,
186 pub total_l2_time: Duration,
187}
188
189impl CacheMetrics {
190 pub fn hit_rate(&self) -> f64 {
191 if self.total_requests == 0 {
192 0.0
193 } else {
194 (self.l1_hits + self.l2_hits) as f64 / self.total_requests as f64
195 }
196 }
197
198 pub fn l1_hit_rate(&self) -> f64 {
199 if self.total_requests == 0 {
200 0.0
201 } else {
202 self.l1_hits as f64 / self.total_requests as f64
203 }
204 }
205}
206
207pub fn get_cache_max_bytes() -> u64 {
211 std::env::var("TYPF_CACHE_MAX_BYTES")
212 .ok()
213 .and_then(|s| s.parse().ok())
214 .unwrap_or(DEFAULT_CACHE_MAX_BYTES)
215}
216
217pub struct RenderOutputCache<K>
222where
223 K: Hash + Eq + Send + Sync + Clone + 'static,
224{
225 cache: Cache<K, crate::types::RenderOutput>,
226 stats: Arc<RwLock<CacheMetrics>>,
227 max_bytes: u64,
228}
229
230impl<K> std::fmt::Debug for RenderOutputCache<K>
231where
232 K: Hash + Eq + Send + Sync + Clone + 'static,
233{
234 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
235 f.debug_struct("RenderOutputCache")
236 .field("entry_count", &self.cache.entry_count())
237 .field("weighted_size", &self.cache.weighted_size())
238 .field("max_bytes", &self.max_bytes)
239 .field("hit_rate", &self.hit_rate())
240 .finish()
241 }
242}
243
244impl<K> RenderOutputCache<K>
245where
246 K: Hash + Eq + Send + Sync + Clone + 'static,
247{
248 pub fn new(max_bytes: u64) -> Self {
250 let cache = Cache::builder()
251 .max_capacity(max_bytes)
252 .weigher(|_key: &K, value: &crate::types::RenderOutput| {
253 value.byte_size().max(1) as u32
255 })
256 .eviction_policy(moka::policy::EvictionPolicy::tiny_lfu())
257 .time_to_idle(Duration::from_secs(600))
258 .build();
259
260 Self {
261 cache,
262 stats: Arc::new(RwLock::new(CacheMetrics::default())),
263 max_bytes,
264 }
265 }
266
267 pub fn with_default_limit() -> Self {
269 Self::new(get_cache_max_bytes())
270 }
271
272 pub fn get(&self, key: &K) -> Option<crate::types::RenderOutput> {
274 let start = Instant::now();
275 let mut stats = self.stats.write();
276 stats.total_requests += 1;
277
278 if let Some(value) = self.cache.get(key) {
279 stats.l1_hits += 1;
280 stats.total_l1_time += start.elapsed();
281 Some(value)
282 } else {
283 stats.misses += 1;
284 None
285 }
286 }
287
288 pub fn insert(&self, key: K, value: crate::types::RenderOutput) {
292 self.cache.insert(key, value);
293 }
294
295 pub fn hit_rate(&self) -> f64 {
297 let stats = self.stats.read();
298 if stats.total_requests == 0 {
299 0.0
300 } else {
301 stats.l1_hits as f64 / stats.total_requests as f64
302 }
303 }
304
305 pub fn weighted_size(&self) -> u64 {
307 self.cache.weighted_size()
308 }
309
310 pub fn entry_count(&self) -> u64 {
312 self.cache.entry_count()
313 }
314
315 pub fn metrics(&self) -> CacheMetrics {
317 self.stats.read().clone()
318 }
319
320 pub fn clear(&self) {
322 self.cache.invalidate_all();
323 let mut stats = self.stats.write();
324 *stats = CacheMetrics::default();
325 }
326
327 #[cfg(test)]
329 pub fn sync(&self) {
330 self.cache.run_pending_tasks();
331 }
332}
333
334pub struct CacheManager {
336 pub shaping_cache: MultiLevelCache<ShapingCacheKey, Arc<Vec<u8>>>,
337 pub glyph_cache: MultiLevelCache<GlyphCacheKey, Arc<Vec<u8>>>,
338}
339
340impl CacheManager {
341 pub fn new() -> Self {
347 Self {
348 shaping_cache: MultiLevelCache::new(100, 10_000),
349 glyph_cache: MultiLevelCache::new(1000, 100_000),
350 }
351 }
352
353 pub fn get_shaped(&self, key: &ShapingCacheKey) -> Option<Arc<Vec<u8>>> {
355 self.shaping_cache.get(key)
356 }
357
358 pub fn cache_shaped(&self, key: ShapingCacheKey, data: Arc<Vec<u8>>) {
360 self.shaping_cache.insert(key, data);
361 }
362
363 pub fn get_glyph(&self, key: &GlyphCacheKey) -> Option<Arc<Vec<u8>>> {
365 self.glyph_cache.get(key)
366 }
367
368 pub fn cache_glyph(&self, key: GlyphCacheKey, data: Arc<Vec<u8>>) {
370 self.glyph_cache.insert(key, data);
371 }
372
373 pub fn report_metrics(&self) -> String {
375 let shaping = self.shaping_cache.metrics();
376 let glyph = self.glyph_cache.metrics();
377
378 format!(
379 "Cache Performance (TinyLFU):\n\
380 Shaping Cache:\n\
381 - Hit Rate: {:.2}%\n\
382 - Entries: {}\n\
383 - Average Access: {:?}\n\
384 Glyph Cache:\n\
385 - Hit Rate: {:.2}%\n\
386 - Entries: {}\n\
387 - Average Access: {:?}",
388 shaping.hit_rate() * 100.0,
389 self.shaping_cache.len(),
390 self.shaping_cache.avg_access_time(),
391 glyph.hit_rate() * 100.0,
392 self.glyph_cache.len(),
393 self.glyph_cache.avg_access_time(),
394 )
395 }
396
397 pub fn clear_all(&self) {
399 self.shaping_cache.clear();
400 self.glyph_cache.clear();
401 }
402}
403
404impl Default for CacheManager {
405 fn default() -> Self {
406 Self::new()
407 }
408}
409
410#[cfg(test)]
411mod tests {
412 use super::*;
413
414 #[test]
415 fn test_cache_insert_and_get() {
416 let cache: MultiLevelCache<String, String> = MultiLevelCache::new(10, 100);
417
418 cache.insert("key1".to_string(), "value1".to_string());
419 cache.insert("key2".to_string(), "value2".to_string());
420
421 assert_eq!(cache.get(&"key1".to_string()), Some("value1".to_string()));
422 assert_eq!(cache.get(&"key2".to_string()), Some("value2".to_string()));
423 assert_eq!(cache.get(&"key3".to_string()), None);
424 }
425
426 #[test]
427 fn test_cache_metrics() {
428 let cache: MultiLevelCache<u32, String> = MultiLevelCache::new(10, 100);
429
430 cache.insert(1, "one".to_string());
431 cache.insert(2, "two".to_string());
432
433 assert_eq!(cache.get(&1), Some("one".to_string()));
434 assert_eq!(cache.get(&2), Some("two".to_string()));
435 assert_eq!(cache.get(&3), None);
436
437 let metrics = cache.metrics();
438 assert_eq!(metrics.total_requests, 3);
439 assert_eq!(metrics.l1_hits, 2);
440 assert_eq!(metrics.misses, 1);
441 }
442
443 #[test]
444 fn test_cache_clear() {
445 let cache: MultiLevelCache<u32, String> = MultiLevelCache::new(10, 100);
446
447 cache.insert(1, "one".to_string());
448 cache.sync(); assert!(!cache.is_empty());
450
451 cache.clear();
452 cache.sync(); assert!(cache.is_empty());
454 assert_eq!(cache.get(&1), None);
455 }
456
457 #[test]
458 fn test_scan_resistance_concept() {
459 let cache: MultiLevelCache<u32, String> = MultiLevelCache::new(5, 5);
464
465 cache.insert(1, "hot".to_string());
467 for _ in 0..10 {
468 cache.get(&1);
469 }
470
471 for i in 100..200 {
473 cache.insert(i, format!("scan_{}", i));
474 }
475
476 assert!(cache.len() <= 10, "Cache should respect capacity limit");
480 }
481
482 #[test]
483 fn test_render_output_byte_size() {
484 use crate::types::{BitmapData, BitmapFormat, RenderOutput, VectorData, VectorFormat};
485
486 let bitmap = RenderOutput::Bitmap(BitmapData {
488 width: 100,
489 height: 100,
490 format: BitmapFormat::Rgba8,
491 data: vec![0u8; 40_000], });
493 assert_eq!(bitmap.byte_size(), 40_000);
494
495 let vector = RenderOutput::Vector(VectorData {
497 format: VectorFormat::Svg,
498 data: "<svg>test</svg>".to_string(),
499 });
500 assert_eq!(vector.byte_size(), 15);
501
502 let json = RenderOutput::Json(r#"{"test": true}"#.to_string());
504 assert_eq!(json.byte_size(), 14);
505 }
506
507 #[test]
508 fn test_byte_weighted_cache_respects_limit() {
509 use crate::types::{BitmapData, BitmapFormat, RenderOutput};
510
511 let cache: RenderOutputCache<u32> = RenderOutputCache::new(100_000);
513
514 for i in 0..15 {
516 let output = RenderOutput::Bitmap(BitmapData {
517 width: 50,
518 height: 50,
519 format: BitmapFormat::Rgba8,
520 data: vec![i as u8; 10_000], });
522 cache.insert(i, output);
523 }
524 cache.sync(); assert!(
528 cache.weighted_size() <= 100_000,
529 "Cache weighted size {} should be <= 100KB",
530 cache.weighted_size()
531 );
532 }
533
534 #[test]
535 fn test_byte_weighted_cache_large_item_eviction() {
536 use crate::types::{BitmapData, BitmapFormat, RenderOutput};
537
538 let cache: RenderOutputCache<u32> = RenderOutputCache::new(50_000);
540
541 let small = RenderOutput::Bitmap(BitmapData {
543 width: 10,
544 height: 10,
545 format: BitmapFormat::Gray8,
546 data: vec![0u8; 100], });
548 cache.insert(1, small);
549
550 let large = RenderOutput::Bitmap(BitmapData {
552 width: 100,
553 height: 100,
554 format: BitmapFormat::Rgba8,
555 data: vec![0u8; 40_000], });
557 cache.insert(2, large);
558 cache.sync();
559
560 assert!(cache.weighted_size() <= 50_000);
562 }
563}