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::encode_to_vec(index, bincode::config::standard()).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::decode_from_slice(data, bincode::config::standard())
389 .map(|(result, _)| result)
390 .map_err(|e| {
391 BinaryExportError::SerializationError(format!("Failed to deserialize index: {e}"))
392 })
393 }
394
395 fn compress_data(&self, data: &[u8]) -> Result<Vec<u8>, BinaryExportError> {
397 use std::io::Write;
399 let mut encoder = flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::default());
400 encoder.write_all(data)?;
401 encoder
402 .finish()
403 .map_err(|e| BinaryExportError::CompressionError(format!("Compression failed: {e}")))
404 }
405
406 fn decompress_data(&self, data: &[u8]) -> Result<Vec<u8>, BinaryExportError> {
408 use std::io::Read;
409 let mut decoder = flate2::read::GzDecoder::new(data);
410 let mut decompressed = Vec::new();
411 decoder.read_to_end(&mut decompressed)?;
412 Ok(decompressed)
413 }
414
415 fn remove_cache_entry(&mut self, cache_key: &str) -> Result<(), BinaryExportError> {
417 if let Some(entry) = self.entries.remove(cache_key) {
418 if entry.cache_file_path.exists() {
419 fs::remove_file(&entry.cache_file_path)?;
420 }
421 }
422 Ok(())
423 }
424
425 fn enforce_cache_limits(&mut self) -> Result<(), BinaryExportError> {
427 let now = SystemTime::now()
428 .duration_since(UNIX_EPOCH)
429 .unwrap_or(Duration::ZERO)
430 .as_secs();
431
432 let expired_keys: Vec<String> = self
434 .entries
435 .iter()
436 .filter(|(_, entry)| (now - entry.cached_at) > self.config.max_age_seconds)
437 .map(|(key, _)| key.clone())
438 .collect();
439
440 for key in expired_keys {
441 self.remove_cache_entry(&key)?;
442 self.stats.record_eviction();
443 }
444
445 while self.entries.len() > self.config.max_entries {
447 let lru_key = self
449 .entries
450 .iter()
451 .min_by_key(|(_, entry)| entry.last_accessed)
452 .map(|(key, _)| key.clone());
453
454 if let Some(key) = lru_key {
455 self.remove_cache_entry(&key)?;
456 self.stats.record_eviction();
457 } else {
458 break;
459 }
460 }
461
462 Ok(())
463 }
464
465 fn cleanup_expired_entries(&mut self) -> Result<(), BinaryExportError> {
467 let now = SystemTime::now()
468 .duration_since(UNIX_EPOCH)
469 .unwrap_or(Duration::ZERO)
470 .as_secs();
471
472 let expired_keys: Vec<String> = self
473 .entries
474 .iter()
475 .filter(|(_, entry)| {
476 (now - entry.cached_at) > self.config.max_age_seconds
477 || !entry.cache_file_path.exists()
478 })
479 .map(|(key, _)| key.clone())
480 .collect();
481
482 for key in expired_keys {
483 self.remove_cache_entry(&key)?;
484 }
485
486 Ok(())
487 }
488
489 fn load_metadata(&mut self) -> Result<(), BinaryExportError> {
491 if !self.metadata_file.exists() {
492 return Ok(());
493 }
494
495 let metadata_content = fs::read_to_string(&self.metadata_file)?;
496 let entries: HashMap<String, CacheEntry> = serde_json::from_str(&metadata_content)
497 .map_err(|e| {
498 BinaryExportError::SerializationError(format!(
499 "Failed to parse cache metadata: {e}",
500 ))
501 })?;
502
503 self.entries = entries;
504 Ok(())
505 }
506
507 fn save_metadata(&self) -> Result<(), BinaryExportError> {
509 let metadata_content = serde_json::to_string_pretty(&self.entries).map_err(|e| {
510 BinaryExportError::SerializationError(format!(
511 "Failed to serialize cache metadata: {e}",
512 ))
513 })?;
514
515 fs::write(&self.metadata_file, metadata_content)?;
516 Ok(())
517 }
518}
519
520impl Drop for IndexCache {
521 fn drop(&mut self) {
522 let _ = self.save_metadata();
524 }
525}
526
527#[cfg(test)]
528mod tests {
529 use super::*;
530 use crate::core::types::AllocationInfo;
531 use crate::export::binary::writer::BinaryWriter;
532 use tempfile::{NamedTempFile, TempDir};
533
534 fn create_test_allocation() -> AllocationInfo {
535 AllocationInfo {
536 ptr: 0x1000,
537 size: 1024,
538 var_name: Some("test_var".to_string()),
539 type_name: Some("i32".to_string()),
540 scope_name: None,
541 timestamp_alloc: 1234567890,
542 timestamp_dealloc: None,
543 thread_id: "main".to_string(),
544 borrow_count: 0,
545 stack_trace: None,
546 is_leaked: false,
547 lifetime_ms: None,
548 borrow_info: None,
549 clone_info: None,
550 ownership_history_available: false,
551 smart_pointer_info: None,
552 memory_layout: None,
553 generic_info: None,
554 dynamic_type_info: None,
555 runtime_state: None,
556 stack_allocation: None,
557 temporary_object: None,
558 fragmentation_analysis: None,
559 generic_instantiation: None,
560 type_relationships: None,
561 type_usage: None,
562 function_call_tracking: None,
563 lifecycle_tracking: None,
564 access_tracking: None,
565 drop_chain_analysis: None,
566 }
567 }
568
569 fn create_test_binary_file() -> NamedTempFile {
570 let temp_file = NamedTempFile::new().expect("Failed to create temp file");
571 let test_allocations = vec![create_test_allocation()];
572
573 {
575 let mut writer =
576 BinaryWriter::new(temp_file.path()).expect("Failed to create temp file");
577 writer
578 .write_header(test_allocations.len() as u32)
579 .expect("Failed to write header");
580 for alloc in &test_allocations {
581 writer
582 .write_allocation(alloc)
583 .expect("Failed to write allocation");
584 }
585 writer.finish().expect("Failed to finish writing");
586 }
587
588 temp_file
589 }
590
591 #[test]
592 fn test_cache_entry_creation() {
593 let entry = CacheEntry::new(
594 PathBuf::from("/test/file.bin"),
595 12345,
596 1024,
597 1000000,
598 PathBuf::from("/cache/entry.index"),
599 );
600
601 assert_eq!(entry.file_path, PathBuf::from("/test/file.bin"));
602 assert_eq!(entry.file_hash, 12345);
603 assert_eq!(entry.file_size, 1024);
604 assert_eq!(entry.file_modified, 1000000);
605 assert_eq!(entry.access_count, 0);
606 }
607
608 #[test]
609 fn test_cache_entry_access_tracking() {
610 let mut entry = CacheEntry::new(
611 PathBuf::from("/test/file.bin"),
612 12345,
613 1024,
614 1000000,
615 PathBuf::from("/cache/entry.index"),
616 );
617
618 let initial_access_time = entry.last_accessed;
619 let initial_count = entry.access_count;
620
621 std::thread::sleep(std::time::Duration::from_millis(100));
623
624 entry.mark_accessed();
625
626 assert!(entry.last_accessed >= initial_access_time);
627 assert_eq!(entry.access_count, initial_count + 1);
628 }
629
630 #[test]
631 fn test_cache_stats() {
632 let mut stats = CacheStats::default();
633
634 assert_eq!(stats.hit_rate(), 0.0);
635
636 stats.record_miss();
637 assert_eq!(stats.hit_rate(), 0.0);
638 assert_eq!(stats.total_requests, 1);
639 assert_eq!(stats.cache_misses, 1);
640
641 stats.record_hit(100);
642 assert_eq!(stats.hit_rate(), 50.0);
643 assert_eq!(stats.total_requests, 2);
644 assert_eq!(stats.cache_hits, 1);
645 assert_eq!(stats.time_saved_ms, 100);
646
647 stats.record_eviction();
648 assert_eq!(stats.evictions, 1);
649 }
650
651 #[test]
652 fn test_index_cache_creation() {
653 let temp_dir = TempDir::new().expect("Failed to get test value");
654 let config = IndexCacheConfig {
655 cache_directory: temp_dir.path().to_path_buf(),
656 ..Default::default()
657 };
658
659 let cache = IndexCache::new(config).expect("Failed to get test value");
660 assert_eq!(cache.entries.len(), 0);
661 assert!(temp_dir.path().exists());
662 }
663
664 #[test]
665 fn test_cache_miss_and_build() {
666 let temp_dir = TempDir::new().expect("Failed to get test value");
667 let config = IndexCacheConfig {
668 cache_directory: temp_dir.path().to_path_buf(),
669 max_entries: 10,
670 ..Default::default()
671 };
672
673 let mut cache = IndexCache::new(config).expect("Failed to create cache");
674 let test_file = create_test_binary_file();
675 let builder = BinaryIndexBuilder::new();
676
677 let index1 = cache
679 .get_or_build_index(test_file.path(), &builder)
680 .expect("Test operation failed");
681 assert_eq!(cache.get_stats().cache_misses, 1);
682 assert_eq!(cache.get_stats().cache_hits, 0);
683 assert_eq!(cache.entries.len(), 1);
684
685 let index2 = cache
687 .get_or_build_index(test_file.path(), &builder)
688 .expect("Test operation failed");
689 assert_eq!(cache.get_stats().cache_misses, 1);
690 assert_eq!(cache.get_stats().cache_hits, 1);
691 assert_eq!(cache.get_stats().hit_rate(), 50.0);
692
693 assert_eq!(index1.file_hash, index2.file_hash);
695 assert_eq!(index1.record_count(), index2.record_count());
696 }
697
698 #[test]
699 fn test_cache_invalidation_on_file_change() {
700 let temp_dir = TempDir::new().expect("Failed to get test value");
701 let config = IndexCacheConfig {
702 cache_directory: temp_dir.path().to_path_buf(),
703 ..Default::default()
704 };
705
706 let mut cache = IndexCache::new(config).expect("Failed to create cache");
707 let test_file = create_test_binary_file();
708 let builder = BinaryIndexBuilder::new();
709
710 let _index1 = cache
712 .get_or_build_index(test_file.path(), &builder)
713 .expect("Test operation failed");
714 assert_eq!(cache.get_stats().cache_misses, 1);
715 assert_eq!(cache.entries.len(), 1);
716
717 std::thread::sleep(std::time::Duration::from_millis(100));
719
720 let test_allocations = vec![{
722 let mut alloc = create_test_allocation();
723 alloc.ptr = 0x2000; alloc.size = 2048; alloc
726 }];
727
728 {
730 let mut writer = BinaryWriter::new(test_file.path()).expect("Failed to create writer");
731 writer
732 .write_header(test_allocations.len() as u32)
733 .expect("Failed to write header");
734 for alloc in &test_allocations {
735 writer
736 .write_allocation(alloc)
737 .expect("Failed to write allocation");
738 }
739 writer.finish().expect("Failed to finish writing");
740 }
741
742 let result = cache.get_or_build_index(test_file.path(), &builder);
744 assert!(result.is_ok());
745
746 assert!(cache.get_stats().cache_misses >= 1);
749 }
750
751 #[test]
752 fn test_cache_size_limit_enforcement() {
753 let temp_dir = TempDir::new().expect("Failed to get test value");
754 let config = IndexCacheConfig {
755 cache_directory: temp_dir.path().to_path_buf(),
756 max_entries: 2, ..Default::default()
758 };
759
760 let mut cache = IndexCache::new(config).expect("Failed to create cache");
761 let builder = BinaryIndexBuilder::new();
762
763 let file1 = create_test_binary_file();
765 let file2 = create_test_binary_file();
766 let file3 = create_test_binary_file();
767
768 let _index1 = cache
770 .get_or_build_index(file1.path(), &builder)
771 .expect("Failed to get or build index");
772 let _index2 = cache
773 .get_or_build_index(file2.path(), &builder)
774 .expect("Failed to get or build index");
775 assert_eq!(cache.entries.len(), 2);
776
777 let _index3 = cache
779 .get_or_build_index(file3.path(), &builder)
780 .expect("Failed to get or build index");
781 assert_eq!(cache.entries.len(), 2);
782 assert!(cache.get_stats().evictions > 0);
783 }
784
785 #[test]
786 fn test_cache_clear() {
787 let temp_dir = TempDir::new().expect("Failed to get test value");
788 let config = IndexCacheConfig {
789 cache_directory: temp_dir.path().to_path_buf(),
790 ..Default::default()
791 };
792
793 let mut cache = IndexCache::new(config).expect("Failed to create cache");
794 let test_file = create_test_binary_file();
795 let builder = BinaryIndexBuilder::new();
796
797 let _index = cache
799 .get_or_build_index(test_file.path(), &builder)
800 .expect("Test operation failed");
801 assert_eq!(cache.entries.len(), 1);
802
803 cache.clear().expect("Test operation failed");
805 assert_eq!(cache.entries.len(), 0);
806 assert_eq!(cache.get_stats().total_requests, 0);
807 }
808}