memscope_rs/export/
data_localizer.rs

1//! data localizer - reduce global state access overhead
2//!
3//! This module implements data localization functionality,
4//! fetching all export data at once to avoid repeated access to global state,
5//! thus significantly improving export performance.
6
7use crate::analysis::unsafe_ffi_tracker::{
8    get_global_unsafe_ffi_tracker, EnhancedAllocationInfo, UnsafeFFIStats,
9};
10use crate::core::scope_tracker::get_global_scope_tracker;
11use crate::core::tracker::get_global_tracker;
12use crate::core::types::MemoryStats;
13use crate::core::types::ScopeInfo;
14use crate::core::types::{AllocationInfo, TrackingError, TrackingResult};
15use std::time::{Duration, Instant};
16
17/// data localizer - fetch all export data at once to avoid repeated access to global state
18pub struct DataLocalizer {
19    /// cached basic allocation data
20    cached_allocations: Option<Vec<AllocationInfo>>,
21    /// cached FFI enhanced data
22    cached_ffi_data: Option<Vec<EnhancedAllocationInfo>>,
23    /// cached stats
24    cached_stats: Option<MemoryStats>,
25    /// cached FFI stats
26    cached_ffi_stats: Option<UnsafeFFIStats>,
27    /// cached scope info
28    cached_scope_info: Option<Vec<ScopeInfo>>,
29    /// last update time
30    last_update: Instant,
31    /// cache ttl
32    cache_ttl: Duration,
33}
34
35/// localized export data, containing all necessary information
36#[derive(Debug, Clone)]
37pub struct LocalizedExportData {
38    /// basic memory allocation info
39    pub allocations: Vec<AllocationInfo>,
40    /// FFI enhanced allocation info
41    pub enhanced_allocations: Vec<EnhancedAllocationInfo>,
42    /// stats
43    pub stats: MemoryStats,
44    /// FFI stats
45    pub ffi_stats: UnsafeFFIStats,
46    /// scope info
47    pub scope_info: Vec<ScopeInfo>,
48    /// timestamp
49    pub timestamp: Instant,
50}
51
52/// data gathering stats
53#[derive(Debug, Clone)]
54pub struct DataGatheringStats {
55    /// total time ms
56    pub total_time_ms: u64,
57    /// basic data time ms
58    pub basic_data_time_ms: u64,
59    /// FFI data time ms
60    pub ffi_data_time_ms: u64,
61    /// scope data time ms
62    pub scope_data_time_ms: u64,
63    /// allocation count
64    pub allocation_count: usize,
65    /// ffi allocation count
66    pub ffi_allocation_count: usize,
67    /// scope count
68    pub scope_count: usize,
69}
70
71impl DataLocalizer {
72    /// create new data localizer
73    pub fn new() -> Self {
74        Self {
75            cached_allocations: None,
76            cached_ffi_data: None,
77            cached_stats: None,
78            cached_ffi_stats: None,
79            cached_scope_info: None,
80            last_update: Instant::now(),
81            cache_ttl: Duration::from_millis(100), // 100ms cache, avoid too frequent data fetching
82        }
83    }
84
85    /// create data localizer with custom cache ttl
86    pub fn with_cache_ttl(cache_ttl: Duration) -> Self {
87        Self {
88            cached_allocations: None,
89            cached_ffi_data: None,
90            cached_stats: None,
91            cached_ffi_stats: None,
92            cached_scope_info: None,
93            last_update: Instant::now(),
94            cache_ttl,
95        }
96    }
97
98    /// gather all export data at once to avoid repeated access to global state
99    pub fn gather_all_export_data(
100        &mut self,
101    ) -> TrackingResult<(LocalizedExportData, DataGatheringStats)> {
102        let total_start = Instant::now();
103
104        tracing::info!("🔄 start data localization to reduce global state access...");
105
106        // check if cache is still valid
107        if self.is_cache_valid() {
108            tracing::info!("✅ using cached data, skipping repeated fetching");
109            return self.get_cached_data();
110        }
111
112        // step 1: get basic memory tracking data with timeout and retry
113        let basic_start = Instant::now();
114        let tracker = get_global_tracker();
115
116        // Use try_lock with timeout to avoid deadlock
117        let allocations = self.get_allocations_with_timeout(&tracker)?;
118        let stats = self.get_stats_with_timeout(&tracker)?;
119        let basic_time = basic_start.elapsed();
120
121        // step 2: get ffi related data with timeout
122        let ffi_start = Instant::now();
123        let ffi_tracker = get_global_unsafe_ffi_tracker();
124        let enhanced_allocations = self.get_ffi_allocations_with_timeout(&ffi_tracker);
125        let ffi_stats = self.get_ffi_stats_with_timeout(&ffi_tracker);
126        let ffi_time = ffi_start.elapsed();
127
128        // step 3: get scope data with timeout
129        let scope_start = Instant::now();
130        let scope_tracker = get_global_scope_tracker();
131        let scope_info = self.get_scope_info_with_timeout(&scope_tracker);
132        let scope_time = scope_start.elapsed();
133
134        let total_time = total_start.elapsed();
135
136        // update cache
137        self.cached_allocations = Some(allocations.clone());
138        self.cached_ffi_data = Some(enhanced_allocations.clone());
139        self.cached_stats = Some(stats.clone());
140        self.cached_ffi_stats = Some(ffi_stats.clone());
141        self.cached_scope_info = Some(scope_info.clone());
142        self.last_update = Instant::now();
143
144        let localized_data = LocalizedExportData {
145            allocations: allocations.clone(),
146            enhanced_allocations: enhanced_allocations.clone(),
147            stats,
148            ffi_stats,
149            scope_info: scope_info.clone(),
150            timestamp: total_start,
151        };
152
153        let gathering_stats = DataGatheringStats {
154            total_time_ms: total_time.as_millis() as u64,
155            basic_data_time_ms: basic_time.as_millis() as u64,
156            ffi_data_time_ms: ffi_time.as_millis() as u64,
157            scope_data_time_ms: scope_time.as_millis() as u64,
158            allocation_count: allocations.len(),
159            ffi_allocation_count: enhanced_allocations.len(),
160            scope_count: scope_info.len(),
161        };
162
163        // print performance stats
164        tracing::info!("✅ data localization completed:");
165        tracing::info!("   total time: {:?}", total_time);
166        tracing::info!(
167            "   basic data: {:?} ({} allocations)",
168            basic_time,
169            gathering_stats.allocation_count
170        );
171        tracing::info!(
172            "   ffi data: {:?} ({} enhanced allocations)",
173            ffi_time,
174            gathering_stats.ffi_allocation_count
175        );
176        tracing::info!(
177            "   scope data: {:?} ({} scopes)",
178            scope_time,
179            gathering_stats.scope_count
180        );
181        tracing::info!(
182            "   data localization avoided {} global state accesses",
183            self.estimate_avoided_global_accesses(&gathering_stats)
184        );
185
186        Ok((localized_data, gathering_stats))
187    }
188
189    /// refresh cache and gather all export data
190    pub fn refresh_cache(&mut self) -> TrackingResult<(LocalizedExportData, DataGatheringStats)> {
191        self.invalidate_cache();
192        self.gather_all_export_data()
193    }
194
195    /// check if cache is still valid
196    fn is_cache_valid(&self) -> bool {
197        self.cached_allocations.is_some()
198            && self.cached_ffi_data.is_some()
199            && self.cached_stats.is_some()
200            && self.cached_ffi_stats.is_some()
201            && self.cached_scope_info.is_some()
202            && self.last_update.elapsed() < self.cache_ttl
203    }
204
205    /// get cached data
206    fn get_cached_data(&self) -> TrackingResult<(LocalizedExportData, DataGatheringStats)> {
207        let localized_data = LocalizedExportData {
208            allocations: self
209                .cached_allocations
210                .as_ref()
211                .ok_or_else(|| {
212                    TrackingError::InternalError("Cached allocations not available".to_string())
213                })?
214                .clone(),
215            enhanced_allocations: self
216                .cached_ffi_data
217                .as_ref()
218                .ok_or_else(|| {
219                    TrackingError::InternalError("Cached FFI data not available".to_string())
220                })?
221                .clone(),
222            stats: self
223                .cached_stats
224                .as_ref()
225                .ok_or_else(|| {
226                    TrackingError::InternalError("Cached stats not available".to_string())
227                })?
228                .clone(),
229            ffi_stats: self
230                .cached_ffi_stats
231                .as_ref()
232                .ok_or_else(|| {
233                    TrackingError::InternalError("Cached FFI stats not available".to_string())
234                })?
235                .clone(),
236            scope_info: self
237                .cached_scope_info
238                .as_ref()
239                .ok_or_else(|| {
240                    TrackingError::InternalError("Cached scope info not available".to_string())
241                })?
242                .clone(),
243            timestamp: self.last_update,
244        };
245
246        let gathering_stats = DataGatheringStats {
247            total_time_ms: 0, // cache hit, no time
248            basic_data_time_ms: 0,
249            ffi_data_time_ms: 0,
250            scope_data_time_ms: 0,
251            allocation_count: localized_data.allocations.len(),
252            ffi_allocation_count: localized_data.enhanced_allocations.len(),
253            scope_count: localized_data.scope_info.len(),
254        };
255
256        Ok((localized_data, gathering_stats))
257    }
258
259    /// invalidate cache
260    pub fn invalidate_cache(&mut self) {
261        self.cached_allocations = None;
262        self.cached_ffi_data = None;
263        self.cached_stats = None;
264        self.cached_ffi_stats = None;
265        self.cached_scope_info = None;
266    }
267
268    /// estimate avoided global accesses
269    fn estimate_avoided_global_accesses(&self, stats: &DataGatheringStats) -> usize {
270        // In the traditional export process, each allocation may need multiple accesses to global state
271        // Here we estimate how many accesses we avoided through data localization
272        let basic_accesses = stats.allocation_count * 2; // Each allocation needs to access tracker 2 times
273        let ffi_accesses = stats.ffi_allocation_count * 3; // FFI allocations need more accesses
274        let scope_accesses = stats.scope_count; // scope access
275
276        basic_accesses + ffi_accesses + scope_accesses
277    }
278
279    /// get cache stats
280    pub fn get_cache_stats(&self) -> CacheStats {
281        CacheStats {
282            is_cached: self.is_cache_valid(),
283            cache_age_ms: self.last_update.elapsed().as_millis() as u64,
284            cache_ttl_ms: self.cache_ttl.as_millis() as u64,
285            cached_allocation_count: self
286                .cached_allocations
287                .as_ref()
288                .map(|v| v.len())
289                .unwrap_or(0),
290            cached_ffi_count: self.cached_ffi_data.as_ref().map(|v| v.len()).unwrap_or(0),
291            cached_scope_count: self
292                .cached_scope_info
293                .as_ref()
294                .map(|v| v.len())
295                .unwrap_or(0),
296        }
297    }
298
299    /// Get allocations with timeout to avoid deadlock
300    fn get_allocations_with_timeout(
301        &self,
302        tracker: &std::sync::Arc<crate::core::tracker::MemoryTracker>,
303    ) -> TrackingResult<Vec<AllocationInfo>> {
304        use std::thread;
305        use std::time::Duration;
306
307        const MAX_RETRIES: u32 = 5;
308        const RETRY_DELAY: Duration = Duration::from_millis(10);
309
310        for attempt in 0..MAX_RETRIES {
311            match tracker.get_active_allocations() {
312                Ok(allocations) => return Ok(allocations),
313                Err(e) => {
314                    if attempt == MAX_RETRIES - 1 {
315                        tracing::warn!(
316                            "Failed to get allocations after {} attempts: {}",
317                            MAX_RETRIES,
318                            e
319                        );
320                        return Ok(Vec::new()); // Return empty vec instead of failing
321                    }
322                    thread::sleep(RETRY_DELAY * (attempt + 1));
323                }
324            }
325        }
326        Ok(Vec::new())
327    }
328
329    /// Get stats with timeout to avoid deadlock
330    fn get_stats_with_timeout(
331        &self,
332        tracker: &std::sync::Arc<crate::core::tracker::MemoryTracker>,
333    ) -> TrackingResult<MemoryStats> {
334        use std::thread;
335        use std::time::Duration;
336
337        const MAX_RETRIES: u32 = 5;
338        const RETRY_DELAY: Duration = Duration::from_millis(10);
339
340        for attempt in 0..MAX_RETRIES {
341            match tracker.get_stats() {
342                Ok(stats) => return Ok(stats),
343                Err(e) => {
344                    if attempt == MAX_RETRIES - 1 {
345                        tracing::warn!("Failed to get stats after {} attempts: {}", MAX_RETRIES, e);
346                        return Ok(MemoryStats::default()); // Return default stats instead of failing
347                    }
348                    thread::sleep(RETRY_DELAY * (attempt + 1));
349                }
350            }
351        }
352        Ok(MemoryStats::default())
353    }
354
355    /// Get FFI allocations with timeout to avoid deadlock
356    fn get_ffi_allocations_with_timeout(
357        &self,
358        ffi_tracker: &std::sync::Arc<crate::analysis::unsafe_ffi_tracker::UnsafeFFITracker>,
359    ) -> Vec<EnhancedAllocationInfo> {
360        use std::thread;
361        use std::time::Duration;
362
363        const MAX_RETRIES: u32 = 3;
364        const RETRY_DELAY: Duration = Duration::from_millis(5);
365
366        for attempt in 0..MAX_RETRIES {
367            match ffi_tracker.get_enhanced_allocations() {
368                Ok(allocations) => return allocations,
369                Err(e) => {
370                    if attempt == MAX_RETRIES - 1 {
371                        tracing::warn!(
372                            "Failed to get FFI allocations after {} attempts: {}, using empty data",
373                            MAX_RETRIES,
374                            e
375                        );
376                        return Vec::new();
377                    }
378                    thread::sleep(RETRY_DELAY * (attempt + 1));
379                }
380            }
381        }
382        Vec::new()
383    }
384
385    /// Get FFI stats with timeout to avoid deadlock
386    fn get_ffi_stats_with_timeout(
387        &self,
388        ffi_tracker: &std::sync::Arc<crate::analysis::unsafe_ffi_tracker::UnsafeFFITracker>,
389    ) -> UnsafeFFIStats {
390        use std::thread;
391        use std::time::Duration;
392
393        const MAX_RETRIES: u32 = 3;
394        const RETRY_DELAY: Duration = Duration::from_millis(5);
395
396        for attempt in 0..MAX_RETRIES {
397            let stats = ffi_tracker.get_stats();
398            if attempt == 0 {
399                return stats; // get_stats() doesn't return Result, so just return it
400            }
401            thread::sleep(RETRY_DELAY * (attempt + 1));
402        }
403        ffi_tracker.get_stats()
404    }
405
406    /// Get scope info with timeout to avoid deadlock
407    fn get_scope_info_with_timeout(
408        &self,
409        scope_tracker: &std::sync::Arc<crate::core::scope_tracker::ScopeTracker>,
410    ) -> Vec<ScopeInfo> {
411        use std::thread;
412        use std::time::Duration;
413
414        const MAX_RETRIES: u32 = 3;
415        const RETRY_DELAY: Duration = Duration::from_millis(5);
416
417        for attempt in 0..MAX_RETRIES {
418            let scope_info = scope_tracker.get_all_scopes();
419            if attempt == 0 {
420                return scope_info; // get_all_scopes() doesn't return Result, so just return it
421            }
422            thread::sleep(RETRY_DELAY * (attempt + 1));
423        }
424        scope_tracker.get_all_scopes()
425    }
426}
427
428/// cache stats
429#[derive(Debug, Clone)]
430pub struct CacheStats {
431    /// whether there is valid cache
432    pub is_cached: bool,
433    /// cache age (milliseconds)
434    pub cache_age_ms: u64,
435    /// cache ttl (milliseconds)
436    pub cache_ttl_ms: u64,
437    /// cached allocation count
438    pub cached_allocation_count: usize,
439    /// cached ffi count
440    pub cached_ffi_count: usize,
441    /// cached scope count
442    pub cached_scope_count: usize,
443}
444
445impl Default for DataLocalizer {
446    fn default() -> Self {
447        Self::new()
448    }
449}
450
451impl LocalizedExportData {
452    /// get data age
453    pub fn age(&self) -> Duration {
454        self.timestamp.elapsed()
455    }
456
457    /// check if data is still fresh
458    pub fn is_fresh(&self, max_age: Duration) -> bool {
459        self.age() < max_age
460    }
461
462    /// get total allocation count (basic + ffi)
463    pub fn total_allocation_count(&self) -> usize {
464        self.allocations.len() + self.enhanced_allocations.len()
465    }
466
467    /// get data summary
468    pub fn get_summary(&self) -> String {
469        format!(
470            "LocalizedExportData {{ allocations: {}, ffi_allocations: {}, scopes: {}, age: {:?} }}",
471            self.allocations.len(),
472            self.enhanced_allocations.len(),
473            self.scope_info.len(),
474            self.age()
475        )
476    }
477}
478
479#[cfg(test)]
480mod tests {
481    use super::*;
482    use crate::analysis::unsafe_ffi_tracker::{
483        AllocationSource, EnhancedAllocationInfo, UnsafeFFIStats,
484    };
485    use crate::core::types::{AllocationInfo, MemoryStats, ScopeInfo};
486    use crate::core::CallStackRef;
487    use std::time::{Duration, Instant};
488
489    // Helper function to create test EnhancedAllocationInfo
490    fn create_test_enhanced_allocation_info(ptr: usize, size: usize) -> EnhancedAllocationInfo {
491        let base = AllocationInfo::new(ptr, size);
492        let call_stack = CallStackRef::new(0, Some(1));
493
494        EnhancedAllocationInfo {
495            base,
496            source: AllocationSource::UnsafeRust {
497                unsafe_block_location: "test_location".to_string(),
498                call_stack: call_stack.clone(),
499                risk_assessment: crate::analysis::unsafe_ffi_tracker::RiskAssessment {
500                    risk_level: crate::analysis::unsafe_ffi_tracker::RiskLevel::Low,
501                    risk_factors: vec![],
502                    mitigation_suggestions: vec![],
503                    confidence_score: 0.8,
504                    assessment_timestamp: 0,
505                },
506            },
507            call_stack,
508            cross_boundary_events: vec![],
509            safety_violations: vec![],
510            ffi_tracked: false,
511            memory_passport: None,
512            ownership_history: None,
513        }
514    }
515
516    // Helper function to create test ScopeInfo
517    fn create_test_scope_info(name: &str) -> ScopeInfo {
518        ScopeInfo {
519            name: name.to_string(),
520            parent: None,
521            children: vec![],
522            depth: 0,
523            variables: vec![],
524            total_memory: 0,
525            peak_memory: 0,
526            allocation_count: 0,
527            lifetime_start: None,
528            lifetime_end: None,
529            is_active: true,
530            start_time: 0,
531            end_time: None,
532            memory_usage: 0,
533            child_scopes: vec![],
534            parent_scope: None,
535        }
536    }
537
538    #[test]
539    fn test_data_localizer_creation() {
540        let localizer = DataLocalizer::new();
541        assert!(!localizer.is_cache_valid());
542
543        let cache_stats = localizer.get_cache_stats();
544        assert!(!cache_stats.is_cached);
545        assert_eq!(cache_stats.cached_allocation_count, 0);
546        assert_eq!(cache_stats.cached_ffi_count, 0);
547        assert_eq!(cache_stats.cached_scope_count, 0);
548        assert_eq!(cache_stats.cache_ttl_ms, 100); // Default TTL
549    }
550
551    #[test]
552    fn test_data_localizer_with_custom_ttl() {
553        let custom_ttl = Duration::from_millis(500);
554        let localizer = DataLocalizer::with_cache_ttl(custom_ttl);
555
556        let cache_stats = localizer.get_cache_stats();
557        assert_eq!(cache_stats.cache_ttl_ms, 500);
558        assert!(!cache_stats.is_cached);
559    }
560
561    #[test]
562    fn test_cache_ttl() {
563        let short_ttl = Duration::from_millis(1);
564        let mut localizer = DataLocalizer::with_cache_ttl(short_ttl);
565
566        // simulate cached data
567        localizer.cached_allocations = Some(vec![]);
568        localizer.cached_ffi_data = Some(vec![]);
569        localizer.cached_stats = Some(MemoryStats::default());
570        localizer.cached_ffi_stats = Some(UnsafeFFIStats::default());
571        localizer.cached_scope_info = Some(vec![]);
572        localizer.last_update = Instant::now();
573
574        assert!(localizer.is_cache_valid());
575
576        // Manually expire cache by setting old timestamp instead of sleeping
577        localizer.last_update = Instant::now() - Duration::from_millis(10);
578        assert!(!localizer.is_cache_valid());
579    }
580
581    #[test]
582    fn test_cache_validity_partial_data() {
583        let mut localizer = DataLocalizer::new();
584
585        // Test with only some cached data - should be invalid
586        localizer.cached_allocations = Some(vec![]);
587        localizer.cached_ffi_data = Some(vec![]);
588        // Missing other cached data
589        localizer.last_update = Instant::now();
590
591        assert!(!localizer.is_cache_valid());
592    }
593
594    #[test]
595    fn test_invalidate_cache() {
596        let mut localizer = DataLocalizer::new();
597
598        // Set up cached data
599        localizer.cached_allocations = Some(vec![]);
600        localizer.cached_ffi_data = Some(vec![]);
601        localizer.cached_stats = Some(MemoryStats::default());
602        localizer.cached_ffi_stats = Some(UnsafeFFIStats::default());
603        localizer.cached_scope_info = Some(vec![]);
604        localizer.last_update = Instant::now();
605
606        assert!(localizer.is_cache_valid());
607
608        localizer.invalidate_cache();
609
610        assert!(!localizer.is_cache_valid());
611        assert!(localizer.cached_allocations.is_none());
612        assert!(localizer.cached_ffi_data.is_none());
613        assert!(localizer.cached_stats.is_none());
614        assert!(localizer.cached_ffi_stats.is_none());
615        assert!(localizer.cached_scope_info.is_none());
616    }
617
618    #[test]
619    fn test_estimate_avoided_global_accesses() {
620        let localizer = DataLocalizer::new();
621
622        let stats = DataGatheringStats {
623            total_time_ms: 100,
624            basic_data_time_ms: 50,
625            ffi_data_time_ms: 30,
626            scope_data_time_ms: 20,
627            allocation_count: 10,
628            ffi_allocation_count: 5,
629            scope_count: 3,
630        };
631
632        let avoided = localizer.estimate_avoided_global_accesses(&stats);
633        // Expected: 10*2 + 5*3 + 3 = 20 + 15 + 3 = 38
634        assert_eq!(avoided, 38);
635    }
636
637    #[test]
638    fn test_estimate_avoided_global_accesses_zero() {
639        let localizer = DataLocalizer::new();
640
641        let stats = DataGatheringStats {
642            total_time_ms: 0,
643            basic_data_time_ms: 0,
644            ffi_data_time_ms: 0,
645            scope_data_time_ms: 0,
646            allocation_count: 0,
647            ffi_allocation_count: 0,
648            scope_count: 0,
649        };
650
651        let avoided = localizer.estimate_avoided_global_accesses(&stats);
652        assert_eq!(avoided, 0);
653    }
654
655    #[test]
656    fn test_get_cache_stats_empty() {
657        let localizer = DataLocalizer::new();
658        let stats = localizer.get_cache_stats();
659
660        assert!(!stats.is_cached);
661        assert_eq!(stats.cached_allocation_count, 0);
662        assert_eq!(stats.cached_ffi_count, 0);
663        assert_eq!(stats.cached_scope_count, 0);
664        assert_eq!(stats.cache_ttl_ms, 100);
665        // Cache age should be reasonable (could be 0 if very fast)
666    }
667
668    #[test]
669    fn test_get_cache_stats_with_data() {
670        let mut localizer = DataLocalizer::new();
671
672        // Set up cached data with different sizes
673        localizer.cached_allocations = Some(vec![
674            AllocationInfo::new(0x1000, 256),
675            AllocationInfo::new(0x2000, 512),
676            AllocationInfo::new(0x3000, 1024),
677        ]);
678        localizer.cached_ffi_data = Some(vec![
679            create_test_enhanced_allocation_info(0x4000, 128),
680            create_test_enhanced_allocation_info(0x5000, 256),
681        ]);
682        localizer.cached_scope_info = Some(vec![create_test_scope_info("test_scope")]);
683        localizer.cached_stats = Some(MemoryStats::default());
684        localizer.cached_ffi_stats = Some(UnsafeFFIStats::default());
685        localizer.last_update = Instant::now();
686
687        let stats = localizer.get_cache_stats();
688        assert!(stats.is_cached);
689        assert_eq!(stats.cached_allocation_count, 3);
690        assert_eq!(stats.cached_ffi_count, 2);
691        assert_eq!(stats.cached_scope_count, 1);
692    }
693
694    #[test]
695    fn test_localized_export_data() {
696        let data = LocalizedExportData {
697            allocations: vec![],
698            enhanced_allocations: vec![],
699            stats: MemoryStats::default(),
700            ffi_stats: UnsafeFFIStats::default(),
701            scope_info: vec![],
702            timestamp: Instant::now(),
703        };
704
705        assert_eq!(data.total_allocation_count(), 0);
706        assert!(data.is_fresh(Duration::from_secs(1)));
707
708        let summary = data.get_summary();
709        assert!(summary.contains("allocations: 0"));
710        assert!(summary.contains("ffi_allocations: 0"));
711        assert!(summary.contains("scopes: 0"));
712    }
713
714    #[test]
715    fn test_localized_export_data_with_data() {
716        let data = LocalizedExportData {
717            allocations: vec![
718                AllocationInfo::new(0x1000, 256),
719                AllocationInfo::new(0x2000, 512),
720            ],
721            enhanced_allocations: vec![create_test_enhanced_allocation_info(0x3000, 128)],
722            stats: MemoryStats::default(),
723            ffi_stats: UnsafeFFIStats::default(),
724            scope_info: vec![
725                create_test_scope_info("scope1"),
726                create_test_scope_info("scope2"),
727                create_test_scope_info("scope3"),
728            ],
729            timestamp: Instant::now(),
730        };
731
732        assert_eq!(data.total_allocation_count(), 3); // 2 + 1
733        assert!(data.is_fresh(Duration::from_secs(1)));
734
735        let summary = data.get_summary();
736        assert!(summary.contains("allocations: 2"));
737        assert!(summary.contains("ffi_allocations: 1"));
738        assert!(summary.contains("scopes: 3"));
739    }
740
741    #[test]
742    fn test_localized_export_data_age() {
743        let old_timestamp = Instant::now() - Duration::from_secs(5);
744        let data = LocalizedExportData {
745            allocations: vec![],
746            enhanced_allocations: vec![],
747            stats: MemoryStats::default(),
748            ffi_stats: UnsafeFFIStats::default(),
749            scope_info: vec![],
750            timestamp: old_timestamp,
751        };
752
753        let age = data.age();
754        assert!(age >= Duration::from_secs(5));
755        assert!(!data.is_fresh(Duration::from_secs(1)));
756        assert!(data.is_fresh(Duration::from_secs(10)));
757    }
758
759    #[test]
760    fn test_data_gathering_stats_debug() {
761        let stats = DataGatheringStats {
762            total_time_ms: 150,
763            basic_data_time_ms: 80,
764            ffi_data_time_ms: 40,
765            scope_data_time_ms: 30,
766            allocation_count: 25,
767            ffi_allocation_count: 10,
768            scope_count: 5,
769        };
770
771        let debug_str = format!("{:?}", stats);
772        assert!(debug_str.contains("total_time_ms: 150"));
773        assert!(debug_str.contains("allocation_count: 25"));
774        assert!(debug_str.contains("ffi_allocation_count: 10"));
775        assert!(debug_str.contains("scope_count: 5"));
776    }
777
778    #[test]
779    fn test_cache_stats_debug() {
780        let stats = CacheStats {
781            is_cached: true,
782            cache_age_ms: 250,
783            cache_ttl_ms: 500,
784            cached_allocation_count: 15,
785            cached_ffi_count: 8,
786            cached_scope_count: 3,
787        };
788
789        let debug_str = format!("{:?}", stats);
790        assert!(debug_str.contains("is_cached: true"));
791        assert!(debug_str.contains("cache_age_ms: 250"));
792        assert!(debug_str.contains("cached_allocation_count: 15"));
793    }
794
795    #[test]
796    fn test_default_implementation() {
797        let localizer1 = DataLocalizer::default();
798        let localizer2 = DataLocalizer::new();
799
800        // Both should have the same initial state
801        assert_eq!(localizer1.cache_ttl, localizer2.cache_ttl);
802        assert_eq!(localizer1.is_cache_valid(), localizer2.is_cache_valid());
803    }
804
805    #[test]
806    fn test_localized_export_data_clone() {
807        let original = LocalizedExportData {
808            allocations: vec![AllocationInfo::new(0x1000, 256)],
809            enhanced_allocations: vec![create_test_enhanced_allocation_info(0x2000, 128)],
810            stats: MemoryStats::default(),
811            ffi_stats: UnsafeFFIStats::default(),
812            scope_info: vec![create_test_scope_info("test_scope")],
813            timestamp: Instant::now(),
814        };
815
816        let cloned = original.clone();
817        assert_eq!(cloned.allocations.len(), original.allocations.len());
818        assert_eq!(
819            cloned.enhanced_allocations.len(),
820            original.enhanced_allocations.len()
821        );
822        assert_eq!(cloned.scope_info.len(), original.scope_info.len());
823    }
824
825    #[test]
826    fn test_data_gathering_stats_clone() {
827        let original = DataGatheringStats {
828            total_time_ms: 100,
829            basic_data_time_ms: 50,
830            ffi_data_time_ms: 30,
831            scope_data_time_ms: 20,
832            allocation_count: 10,
833            ffi_allocation_count: 5,
834            scope_count: 3,
835        };
836
837        let cloned = original.clone();
838        assert_eq!(cloned.total_time_ms, original.total_time_ms);
839        assert_eq!(cloned.allocation_count, original.allocation_count);
840        assert_eq!(cloned.ffi_allocation_count, original.ffi_allocation_count);
841        assert_eq!(cloned.scope_count, original.scope_count);
842    }
843
844    #[test]
845    fn test_cache_stats_clone() {
846        let original = CacheStats {
847            is_cached: true,
848            cache_age_ms: 100,
849            cache_ttl_ms: 200,
850            cached_allocation_count: 5,
851            cached_ffi_count: 3,
852            cached_scope_count: 2,
853        };
854
855        let cloned = original.clone();
856        assert_eq!(cloned.is_cached, original.is_cached);
857        assert_eq!(cloned.cache_age_ms, original.cache_age_ms);
858        assert_eq!(
859            cloned.cached_allocation_count,
860            original.cached_allocation_count
861        );
862    }
863}