1use crate::export::binary::error::BinaryExportError;
8use crate::export::binary::index::BinaryIndex;
9use crate::export::binary::index_builder::BinaryIndexBuilder;
10use serde::{Deserialize, Serialize};
11use std::collections::HashMap;
12use std::fs;
13use std::path::{Path, PathBuf};
14use std::time::{Duration, SystemTime, UNIX_EPOCH};
15
16#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct CacheEntry {
19 pub file_path: PathBuf,
21 pub file_hash: u64,
23 pub file_size: u64,
25 pub file_modified: u64,
27 pub cached_at: u64,
29 pub last_accessed: u64,
31 pub access_count: u64,
33 pub cache_file_path: PathBuf,
35}
36
37impl CacheEntry {
38 pub fn new(
40 file_path: PathBuf,
41 file_hash: u64,
42 file_size: u64,
43 file_modified: u64,
44 cache_file_path: PathBuf,
45 ) -> Self {
46 let now = SystemTime::now()
47 .duration_since(UNIX_EPOCH)
48 .unwrap_or(Duration::ZERO)
49 .as_secs();
50
51 Self {
52 file_path,
53 file_hash,
54 file_size,
55 file_modified,
56 cached_at: now,
57 last_accessed: now,
58 access_count: 0,
59 cache_file_path,
60 }
61 }
62
63 pub fn mark_accessed(&mut self) {
65 self.last_accessed = SystemTime::now()
66 .duration_since(UNIX_EPOCH)
67 .unwrap_or(Duration::ZERO)
68 .as_secs();
69 self.access_count += 1;
70 }
71
72 pub fn is_valid_for_file<P: AsRef<Path>>(
74 &self,
75 file_path: P,
76 ) -> Result<bool, BinaryExportError> {
77 let path = file_path.as_ref();
78
79 if !path.exists() {
81 return Ok(false);
82 }
83
84 let metadata = fs::metadata(path)?;
86 let file_size = metadata.len();
87
88 if file_size != self.file_size {
90 return Ok(false);
91 }
92
93 if let Ok(modified) = metadata.modified() {
95 if let Ok(duration) = modified.duration_since(UNIX_EPOCH) {
96 let file_modified = duration.as_secs();
97 if file_modified != self.file_modified {
98 return Ok(false);
99 }
100 }
101 }
102
103 if !self.cache_file_path.exists() {
105 return Ok(false);
106 }
107
108 Ok(true)
109 }
110}
111
112#[derive(Debug, Clone)]
114pub struct IndexCacheConfig {
115 pub max_entries: usize,
117 pub max_age_seconds: u64,
119 pub cache_directory: PathBuf,
121 pub enable_compression: bool,
123}
124
125impl Default for IndexCacheConfig {
126 fn default() -> Self {
127 let cache_dir = std::env::temp_dir().join("memscope_index_cache");
128 Self {
129 max_entries: 100,
130 max_age_seconds: 7 * 24 * 3600, cache_directory: cache_dir,
132 enable_compression: true,
133 }
134 }
135}
136
137#[derive(Debug, Clone, Default)]
139pub struct CacheStats {
140 pub total_requests: u64,
142 pub cache_hits: u64,
144 pub cache_misses: u64,
146 pub evictions: u64,
148 pub time_saved_ms: u64,
150}
151
152impl CacheStats {
153 pub fn hit_rate(&self) -> f64 {
155 if self.total_requests == 0 {
156 0.0
157 } else {
158 (self.cache_hits as f64 / self.total_requests as f64) * 100.0
159 }
160 }
161
162 pub fn record_hit(&mut self, time_saved_ms: u64) {
164 self.total_requests += 1;
165 self.cache_hits += 1;
166 self.time_saved_ms += time_saved_ms;
167 }
168
169 pub fn record_miss(&mut self) {
171 self.total_requests += 1;
172 self.cache_misses += 1;
173 }
174
175 pub fn record_eviction(&mut self) {
177 self.evictions += 1;
178 }
179}
180
181pub struct IndexCache {
183 config: IndexCacheConfig,
185 entries: HashMap<String, CacheEntry>,
187 stats: CacheStats,
189 metadata_file: PathBuf,
191}
192
193impl IndexCache {
194 pub fn new(config: IndexCacheConfig) -> Result<Self, BinaryExportError> {
196 if !config.cache_directory.exists() {
198 fs::create_dir_all(&config.cache_directory)?;
199 }
200
201 let metadata_file = config.cache_directory.join("cache_metadata.json");
202
203 let mut cache = Self {
204 config,
205 entries: HashMap::new(),
206 stats: CacheStats::default(),
207 metadata_file,
208 };
209
210 cache.load_metadata()?;
212
213 cache.cleanup_expired_entries()?;
215
216 Ok(cache)
217 }
218
219 pub fn with_default_config() -> Result<Self, BinaryExportError> {
221 Self::new(IndexCacheConfig::default())
222 }
223
224 pub fn get_or_build_index<P: AsRef<Path>>(
226 &mut self,
227 file_path: P,
228 builder: &BinaryIndexBuilder,
229 ) -> Result<BinaryIndex, BinaryExportError> {
230 let path = file_path.as_ref();
231 let cache_key = self.generate_cache_key(path);
232
233 if let Some(entry) = self.entries.get(&cache_key) {
235 if entry.is_valid_for_file(path)? {
236 let start_time = std::time::Instant::now();
238 let index = self.load_index_from_cache(entry)?;
239 let load_time = start_time.elapsed().as_millis() as u64;
240
241 if let Some(entry) = self.entries.get_mut(&cache_key) {
243 entry.mark_accessed();
244 }
245 self.stats.record_hit(load_time);
246
247 return Ok(index);
248 } else {
249 self.remove_cache_entry(&cache_key)?;
251 }
252 }
253
254 self.stats.record_miss();
256 let start_time = std::time::Instant::now();
257 let index = builder.build_index(path)?;
258 let build_time = start_time.elapsed().as_millis() as u64;
259
260 self.cache_index(path, &index, build_time)?;
262
263 Ok(index)
264 }
265
266 pub fn get_stats(&self) -> &CacheStats {
268 &self.stats
269 }
270
271 pub fn clear(&mut self) -> Result<(), BinaryExportError> {
273 for entry in self.entries.values() {
275 if entry.cache_file_path.exists() {
276 fs::remove_file(&entry.cache_file_path)?;
277 }
278 }
279
280 self.entries.clear();
282 self.stats = CacheStats::default();
283
284 self.save_metadata()?;
286
287 Ok(())
288 }
289
290 fn generate_cache_key<P: AsRef<Path>>(&self, file_path: P) -> String {
292 use std::collections::hash_map::DefaultHasher;
293 use std::hash::{Hash, Hasher};
294
295 let path = file_path.as_ref();
296 let mut hasher = DefaultHasher::new();
297 path.hash(&mut hasher);
298 format!("index_{:x}", hasher.finish())
299 }
300
301 fn cache_index<P: AsRef<Path>>(
303 &mut self,
304 file_path: P,
305 index: &BinaryIndex,
306 _build_time_ms: u64,
307 ) -> Result<(), BinaryExportError> {
308 let path = file_path.as_ref();
309 let cache_key = self.generate_cache_key(path);
310
311 let metadata = fs::metadata(path)?;
313 let file_size = metadata.len();
314 let file_modified = metadata
315 .modified()?
316 .duration_since(UNIX_EPOCH)
317 .unwrap_or(Duration::ZERO)
318 .as_secs();
319
320 let cache_file_name = format!("{cache_key}.index");
322 let cache_file_path = self.config.cache_directory.join(cache_file_name);
323
324 self.save_index_to_cache(index, &cache_file_path)?;
326
327 let entry = CacheEntry::new(
329 path.to_path_buf(),
330 index.file_hash,
331 file_size,
332 file_modified,
333 cache_file_path,
334 );
335
336 self.entries.insert(cache_key, entry);
338
339 self.enforce_cache_limits()?;
341
342 self.save_metadata()?;
344
345 Ok(())
346 }
347
348 fn load_index_from_cache(&self, entry: &CacheEntry) -> Result<BinaryIndex, BinaryExportError> {
350 let cache_data = fs::read(&entry.cache_file_path)?;
351
352 if self.config.enable_compression {
353 let decompressed = self.decompress_data(&cache_data)?;
355 self.deserialize_index(&decompressed)
356 } else {
357 self.deserialize_index(&cache_data)
358 }
359 }
360
361 fn save_index_to_cache(
363 &self,
364 index: &BinaryIndex,
365 cache_file_path: &Path,
366 ) -> Result<(), BinaryExportError> {
367 let serialized = self.serialize_index(index)?;
368
369 let data_to_write = if self.config.enable_compression {
370 self.compress_data(&serialized)?
371 } else {
372 serialized
373 };
374
375 fs::write(cache_file_path, data_to_write)?;
376 Ok(())
377 }
378
379 fn serialize_index(&self, index: &BinaryIndex) -> Result<Vec<u8>, BinaryExportError> {
381 bincode::serialize(index).map_err(|e| {
382 BinaryExportError::SerializationError(format!("Failed to serialize index: {e}"))
383 })
384 }
385
386 fn deserialize_index(&self, data: &[u8]) -> Result<BinaryIndex, BinaryExportError> {
388 bincode::deserialize(data).map_err(|e| {
389 BinaryExportError::SerializationError(format!("Failed to deserialize index: {e}"))
390 })
391 }
392
393 fn compress_data(&self, data: &[u8]) -> Result<Vec<u8>, BinaryExportError> {
395 use std::io::Write;
397 let mut encoder = flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::default());
398 encoder.write_all(data)?;
399 encoder
400 .finish()
401 .map_err(|e| BinaryExportError::CompressionError(format!("Compression failed: {e}")))
402 }
403
404 fn decompress_data(&self, data: &[u8]) -> Result<Vec<u8>, BinaryExportError> {
406 use std::io::Read;
407 let mut decoder = flate2::read::GzDecoder::new(data);
408 let mut decompressed = Vec::new();
409 decoder.read_to_end(&mut decompressed)?;
410 Ok(decompressed)
411 }
412
413 fn remove_cache_entry(&mut self, cache_key: &str) -> Result<(), BinaryExportError> {
415 if let Some(entry) = self.entries.remove(cache_key) {
416 if entry.cache_file_path.exists() {
417 fs::remove_file(&entry.cache_file_path)?;
418 }
419 }
420 Ok(())
421 }
422
423 fn enforce_cache_limits(&mut self) -> Result<(), BinaryExportError> {
425 let now = SystemTime::now()
426 .duration_since(UNIX_EPOCH)
427 .unwrap_or(Duration::ZERO)
428 .as_secs();
429
430 let expired_keys: Vec<String> = self
432 .entries
433 .iter()
434 .filter(|(_, entry)| (now - entry.cached_at) > self.config.max_age_seconds)
435 .map(|(key, _)| key.clone())
436 .collect();
437
438 for key in expired_keys {
439 self.remove_cache_entry(&key)?;
440 self.stats.record_eviction();
441 }
442
443 while self.entries.len() > self.config.max_entries {
445 let lru_key = self
447 .entries
448 .iter()
449 .min_by_key(|(_, entry)| entry.last_accessed)
450 .map(|(key, _)| key.clone());
451
452 if let Some(key) = lru_key {
453 self.remove_cache_entry(&key)?;
454 self.stats.record_eviction();
455 } else {
456 break;
457 }
458 }
459
460 Ok(())
461 }
462
463 fn cleanup_expired_entries(&mut self) -> Result<(), BinaryExportError> {
465 let now = SystemTime::now()
466 .duration_since(UNIX_EPOCH)
467 .unwrap_or(Duration::ZERO)
468 .as_secs();
469
470 let expired_keys: Vec<String> = self
471 .entries
472 .iter()
473 .filter(|(_, entry)| {
474 (now - entry.cached_at) > self.config.max_age_seconds
475 || !entry.cache_file_path.exists()
476 })
477 .map(|(key, _)| key.clone())
478 .collect();
479
480 for key in expired_keys {
481 self.remove_cache_entry(&key)?;
482 }
483
484 Ok(())
485 }
486
487 fn load_metadata(&mut self) -> Result<(), BinaryExportError> {
489 if !self.metadata_file.exists() {
490 return Ok(());
491 }
492
493 let metadata_content = fs::read_to_string(&self.metadata_file)?;
494 let entries: HashMap<String, CacheEntry> = serde_json::from_str(&metadata_content)
495 .map_err(|e| {
496 BinaryExportError::SerializationError(format!(
497 "Failed to parse cache metadata: {e}",
498 ))
499 })?;
500
501 self.entries = entries;
502 Ok(())
503 }
504
505 fn save_metadata(&self) -> Result<(), BinaryExportError> {
507 let metadata_content = serde_json::to_string_pretty(&self.entries).map_err(|e| {
508 BinaryExportError::SerializationError(format!(
509 "Failed to serialize cache metadata: {e}",
510 ))
511 })?;
512
513 fs::write(&self.metadata_file, metadata_content)?;
514 Ok(())
515 }
516}
517
518impl Drop for IndexCache {
519 fn drop(&mut self) {
520 let _ = self.save_metadata();
522 }
523}
524
525#[cfg(test)]
526mod tests {
527 use super::*;
528 use crate::core::types::AllocationInfo;
529 use crate::export::binary::writer::BinaryWriter;
530 use tempfile::{NamedTempFile, TempDir};
531
532 fn create_test_allocation() -> AllocationInfo {
533 AllocationInfo {
534 ptr: 0x1000,
535 size: 1024,
536 var_name: Some("test_var".to_string()),
537 type_name: Some("i32".to_string()),
538 scope_name: None,
539 timestamp_alloc: 1234567890,
540 timestamp_dealloc: None,
541 thread_id: "main".to_string(),
542 borrow_count: 0,
543 stack_trace: None,
544 is_leaked: false,
545 lifetime_ms: None,
546 borrow_info: None,
547 clone_info: None,
548 ownership_history_available: false,
549 smart_pointer_info: None,
550 memory_layout: None,
551 generic_info: None,
552 dynamic_type_info: None,
553 runtime_state: None,
554 stack_allocation: None,
555 temporary_object: None,
556 fragmentation_analysis: None,
557 generic_instantiation: None,
558 type_relationships: None,
559 type_usage: None,
560 function_call_tracking: None,
561 lifecycle_tracking: None,
562 access_tracking: None,
563 drop_chain_analysis: None,
564 }
565 }
566
567 fn create_test_binary_file() -> NamedTempFile {
568 let temp_file = NamedTempFile::new().expect("Failed to create temp file");
569 let test_allocations = vec![create_test_allocation()];
570
571 {
573 let mut writer =
574 BinaryWriter::new(temp_file.path()).expect("Failed to create temp file");
575 writer
576 .write_header(test_allocations.len() as u32)
577 .expect("Failed to write header");
578 for alloc in &test_allocations {
579 writer
580 .write_allocation(alloc)
581 .expect("Failed to write allocation");
582 }
583 writer.finish().expect("Failed to finish writing");
584 }
585
586 temp_file
587 }
588
589 #[test]
590 fn test_cache_entry_creation() {
591 let entry = CacheEntry::new(
592 PathBuf::from("/test/file.bin"),
593 12345,
594 1024,
595 1000000,
596 PathBuf::from("/cache/entry.index"),
597 );
598
599 assert_eq!(entry.file_path, PathBuf::from("/test/file.bin"));
600 assert_eq!(entry.file_hash, 12345);
601 assert_eq!(entry.file_size, 1024);
602 assert_eq!(entry.file_modified, 1000000);
603 assert_eq!(entry.access_count, 0);
604 }
605
606 #[test]
607 fn test_cache_entry_access_tracking() {
608 let mut entry = CacheEntry::new(
609 PathBuf::from("/test/file.bin"),
610 12345,
611 1024,
612 1000000,
613 PathBuf::from("/cache/entry.index"),
614 );
615
616 let initial_access_time = entry.last_accessed;
617 let initial_count = entry.access_count;
618
619 std::thread::sleep(std::time::Duration::from_millis(100));
621
622 entry.mark_accessed();
623
624 assert!(entry.last_accessed >= initial_access_time);
625 assert_eq!(entry.access_count, initial_count + 1);
626 }
627
628 #[test]
629 fn test_cache_stats() {
630 let mut stats = CacheStats::default();
631
632 assert_eq!(stats.hit_rate(), 0.0);
633
634 stats.record_miss();
635 assert_eq!(stats.hit_rate(), 0.0);
636 assert_eq!(stats.total_requests, 1);
637 assert_eq!(stats.cache_misses, 1);
638
639 stats.record_hit(100);
640 assert_eq!(stats.hit_rate(), 50.0);
641 assert_eq!(stats.total_requests, 2);
642 assert_eq!(stats.cache_hits, 1);
643 assert_eq!(stats.time_saved_ms, 100);
644
645 stats.record_eviction();
646 assert_eq!(stats.evictions, 1);
647 }
648
649 #[test]
650 fn test_index_cache_creation() {
651 let temp_dir = TempDir::new().expect("Failed to get test value");
652 let config = IndexCacheConfig {
653 cache_directory: temp_dir.path().to_path_buf(),
654 ..Default::default()
655 };
656
657 let cache = IndexCache::new(config).expect("Failed to get test value");
658 assert_eq!(cache.entries.len(), 0);
659 assert!(temp_dir.path().exists());
660 }
661
662 #[test]
663 fn test_cache_miss_and_build() {
664 let temp_dir = TempDir::new().expect("Failed to get test value");
665 let config = IndexCacheConfig {
666 cache_directory: temp_dir.path().to_path_buf(),
667 max_entries: 10,
668 ..Default::default()
669 };
670
671 let mut cache = IndexCache::new(config).expect("Failed to create cache");
672 let test_file = create_test_binary_file();
673 let builder = BinaryIndexBuilder::new();
674
675 let index1 = cache
677 .get_or_build_index(test_file.path(), &builder)
678 .expect("Test operation failed");
679 assert_eq!(cache.get_stats().cache_misses, 1);
680 assert_eq!(cache.get_stats().cache_hits, 0);
681 assert_eq!(cache.entries.len(), 1);
682
683 let index2 = cache
685 .get_or_build_index(test_file.path(), &builder)
686 .expect("Test operation failed");
687 assert_eq!(cache.get_stats().cache_misses, 1);
688 assert_eq!(cache.get_stats().cache_hits, 1);
689 assert_eq!(cache.get_stats().hit_rate(), 50.0);
690
691 assert_eq!(index1.file_hash, index2.file_hash);
693 assert_eq!(index1.record_count(), index2.record_count());
694 }
695
696 #[test]
697 fn test_cache_invalidation_on_file_change() {
698 let temp_dir = TempDir::new().expect("Failed to get test value");
699 let config = IndexCacheConfig {
700 cache_directory: temp_dir.path().to_path_buf(),
701 ..Default::default()
702 };
703
704 let mut cache = IndexCache::new(config).expect("Failed to create cache");
705 let test_file = create_test_binary_file();
706 let builder = BinaryIndexBuilder::new();
707
708 let _index1 = cache
710 .get_or_build_index(test_file.path(), &builder)
711 .expect("Test operation failed");
712 assert_eq!(cache.get_stats().cache_misses, 1);
713 assert_eq!(cache.entries.len(), 1);
714
715 std::thread::sleep(std::time::Duration::from_millis(100));
717
718 let test_allocations = vec![{
720 let mut alloc = create_test_allocation();
721 alloc.ptr = 0x2000; alloc.size = 2048; alloc
724 }];
725
726 {
728 let mut writer = BinaryWriter::new(test_file.path()).expect("Failed to create writer");
729 writer
730 .write_header(test_allocations.len() as u32)
731 .expect("Failed to write header");
732 for alloc in &test_allocations {
733 writer
734 .write_allocation(alloc)
735 .expect("Failed to write allocation");
736 }
737 writer.finish().expect("Failed to finish writing");
738 }
739
740 let result = cache.get_or_build_index(test_file.path(), &builder);
742 assert!(result.is_ok());
743
744 assert!(cache.get_stats().cache_misses >= 1);
747 }
748
749 #[test]
750 fn test_cache_size_limit_enforcement() {
751 let temp_dir = TempDir::new().expect("Failed to get test value");
752 let config = IndexCacheConfig {
753 cache_directory: temp_dir.path().to_path_buf(),
754 max_entries: 2, ..Default::default()
756 };
757
758 let mut cache = IndexCache::new(config).expect("Failed to create cache");
759 let builder = BinaryIndexBuilder::new();
760
761 let file1 = create_test_binary_file();
763 let file2 = create_test_binary_file();
764 let file3 = create_test_binary_file();
765
766 let _index1 = cache
768 .get_or_build_index(file1.path(), &builder)
769 .expect("Failed to get or build index");
770 let _index2 = cache
771 .get_or_build_index(file2.path(), &builder)
772 .expect("Failed to get or build index");
773 assert_eq!(cache.entries.len(), 2);
774
775 let _index3 = cache
777 .get_or_build_index(file3.path(), &builder)
778 .expect("Failed to get or build index");
779 assert_eq!(cache.entries.len(), 2);
780 assert!(cache.get_stats().evictions > 0);
781 }
782
783 #[test]
784 fn test_cache_clear() {
785 let temp_dir = TempDir::new().expect("Failed to get test value");
786 let config = IndexCacheConfig {
787 cache_directory: temp_dir.path().to_path_buf(),
788 ..Default::default()
789 };
790
791 let mut cache = IndexCache::new(config).expect("Failed to create cache");
792 let test_file = create_test_binary_file();
793 let builder = BinaryIndexBuilder::new();
794
795 let _index = cache
797 .get_or_build_index(test_file.path(), &builder)
798 .expect("Test operation failed");
799 assert_eq!(cache.entries.len(), 1);
800
801 cache.clear().expect("Test operation failed");
803 assert_eq!(cache.entries.len(), 0);
804 assert_eq!(cache.get_stats().total_requests, 0);
805 }
806}