memscope_rs/async_memory/
profile.rs

1//! Task memory profiling and performance metrics
2//!
3//! Provides data structures for tracking memory usage patterns
4//! and performance characteristics of individual async tasks.
5
6use std::time::{Duration, Instant};
7// HashMap will be used for aggregated statistics in future versions
8
9use crate::async_memory::TaskId;
10
11/// Memory usage profile for a single async task
12#[derive(Debug, Clone)]
13pub struct TaskMemoryProfile {
14    /// Unique task identifier
15    pub task_id: TaskId,
16    /// Task creation timestamp
17    pub created_at: Instant,
18    /// Task completion timestamp (if completed)
19    pub completed_at: Option<Instant>,
20    /// Total bytes allocated by this task
21    pub total_allocated: u64,
22    /// Current memory usage (allocated - deallocated)
23    pub current_usage: u64,
24    /// Peak memory usage observed
25    pub peak_usage: u64,
26    /// Number of allocation operations
27    pub allocation_count: u64,
28    /// Number of deallocation operations
29    pub deallocation_count: u64,
30    /// Average allocation size
31    pub average_allocation_size: f64,
32}
33
34impl TaskMemoryProfile {
35    /// Create new task profile
36    pub fn new(task_id: TaskId) -> Self {
37        Self {
38            task_id,
39            created_at: Instant::now(),
40            completed_at: None,
41            total_allocated: 0,
42            current_usage: 0,
43            peak_usage: 0,
44            allocation_count: 0,
45            deallocation_count: 0,
46            average_allocation_size: 0.0,
47        }
48    }
49
50    /// Record allocation event
51    pub fn record_allocation(&mut self, size: u64) {
52        self.total_allocated += size;
53        self.current_usage += size;
54        self.allocation_count += 1;
55
56        // Update peak usage
57        if self.current_usage > self.peak_usage {
58            self.peak_usage = self.current_usage;
59        }
60
61        // Update average allocation size
62        self.average_allocation_size = self.total_allocated as f64 / self.allocation_count as f64;
63    }
64
65    /// Record deallocation event
66    pub fn record_deallocation(&mut self, size: u64) {
67        self.current_usage = self.current_usage.saturating_sub(size);
68        self.deallocation_count += 1;
69    }
70
71    /// Mark task as completed
72    pub fn mark_completed(&mut self) {
73        self.completed_at = Some(Instant::now());
74    }
75
76    /// Check if task is completed
77    pub fn is_completed(&self) -> bool {
78        self.completed_at.is_some()
79    }
80
81    /// Get task lifetime duration
82    pub fn lifetime(&self) -> Duration {
83        let end = self.completed_at.unwrap_or_else(Instant::now);
84        end.duration_since(self.created_at)
85    }
86
87    /// Calculate memory efficiency (deallocated / allocated)
88    pub fn memory_efficiency(&self) -> f64 {
89        if self.total_allocated == 0 {
90            1.0
91        } else {
92            let deallocated = self.total_allocated - self.current_usage;
93            deallocated as f64 / self.total_allocated as f64
94        }
95    }
96
97    /// Check if task has potential memory leak
98    pub fn has_potential_leak(&self) -> bool {
99        self.is_completed() && self.current_usage > 0
100    }
101}
102
103/// Performance metrics for async task execution
104#[derive(Debug, Clone)]
105pub struct TaskPerformanceMetrics {
106    /// Task execution time
107    pub execution_time: Duration,
108    /// Memory allocation rate (bytes/second)
109    pub allocation_rate: f64,
110    /// Memory efficiency ratio
111    pub efficiency_ratio: f64,
112    /// Peak memory usage
113    pub peak_memory_mb: f64,
114    /// Average allocation size
115    pub avg_allocation_size: f64,
116}
117
118impl TaskPerformanceMetrics {
119    /// Create metrics from task profile
120    pub fn from_profile(profile: &TaskMemoryProfile) -> Self {
121        let lifetime = profile.lifetime();
122        let lifetime_secs = lifetime.as_secs_f64();
123
124        let allocation_rate = if lifetime_secs > 0.0 {
125            profile.total_allocated as f64 / lifetime_secs
126        } else {
127            0.0
128        };
129
130        Self {
131            execution_time: lifetime,
132            allocation_rate,
133            efficiency_ratio: profile.memory_efficiency(),
134            peak_memory_mb: profile.peak_usage as f64 / 1_048_576.0, // Convert to MB
135            avg_allocation_size: profile.average_allocation_size,
136        }
137    }
138
139    /// Get performance rating (0.0 to 1.0, higher is better)
140    pub fn performance_rating(&self) -> f64 {
141        // Combine multiple factors for overall rating
142        let efficiency_score = self.efficiency_ratio;
143        let speed_score = if self.allocation_rate < 1_000_000.0 {
144            1.0
145        } else {
146            0.5
147        }; // Prefer < 1MB/s
148        let memory_score = if self.peak_memory_mb < 10.0 { 1.0 } else { 0.7 }; // Prefer < 10MB peak
149
150        (efficiency_score + speed_score + memory_score) / 3.0
151    }
152}
153
154/// Aggregated statistics across multiple tasks
155#[derive(Debug, Clone)]
156pub struct AggregatedTaskStats {
157    /// Total number of tasks tracked
158    pub total_tasks: usize,
159    /// Number of completed tasks
160    pub completed_tasks: usize,
161    /// Total memory allocated across all tasks
162    pub total_memory_allocated: u64,
163    /// Current memory usage across all active tasks
164    pub current_memory_usage: u64,
165    /// Peak memory usage observed
166    pub peak_memory_usage: u64,
167    /// Average task lifetime
168    pub average_lifetime: Duration,
169    /// Memory efficiency across all tasks
170    pub overall_efficiency: f64,
171    /// Tasks with potential memory leaks
172    pub potential_leaks: usize,
173}
174
175impl AggregatedTaskStats {
176    /// Create empty statistics
177    pub fn new() -> Self {
178        Self {
179            total_tasks: 0,
180            completed_tasks: 0,
181            total_memory_allocated: 0,
182            current_memory_usage: 0,
183            peak_memory_usage: 0,
184            average_lifetime: Duration::ZERO,
185            overall_efficiency: 1.0,
186            potential_leaks: 0,
187        }
188    }
189
190    /// Add task profile to aggregated statistics
191    pub fn add_task(&mut self, profile: &TaskMemoryProfile) {
192        self.total_tasks += 1;
193
194        if profile.is_completed() {
195            self.completed_tasks += 1;
196        }
197
198        self.total_memory_allocated += profile.total_allocated;
199        self.current_memory_usage += profile.current_usage;
200
201        if profile.peak_usage > self.peak_memory_usage {
202            self.peak_memory_usage = profile.peak_usage;
203        }
204
205        if profile.has_potential_leak() {
206            self.potential_leaks += 1;
207        }
208
209        // Recalculate averages (simplified)
210        if self.completed_tasks > 0 {
211            self.overall_efficiency = if self.total_memory_allocated > 0 {
212                let total_deallocated = self.total_memory_allocated - self.current_memory_usage;
213                total_deallocated as f64 / self.total_memory_allocated as f64
214            } else {
215                1.0
216            };
217        }
218    }
219
220    /// Get memory usage summary
221    pub fn memory_summary(&self) -> String {
222        format!(
223            "Tasks: {} ({}% complete), Memory: {:.1}MB allocated, {:.1}MB current, {:.1}% efficiency",
224            self.total_tasks,
225            if self.total_tasks > 0 { self.completed_tasks * 100 / self.total_tasks } else { 0 },
226            self.total_memory_allocated as f64 / 1_048_576.0,
227            self.current_memory_usage as f64 / 1_048_576.0,
228            self.overall_efficiency * 100.0
229        )
230    }
231}
232
233impl Default for AggregatedTaskStats {
234    fn default() -> Self {
235        Self::new()
236    }
237}
238
239#[cfg(test)]
240mod tests {
241    use super::*;
242    use std::thread;
243    use std::time::Duration;
244
245    #[test]
246    fn test_task_memory_profile_basic() {
247        let mut profile = TaskMemoryProfile::new(12345);
248
249        assert_eq!(profile.task_id, 12345);
250        assert!(!profile.is_completed());
251        assert_eq!(profile.current_usage, 0);
252        assert_eq!(profile.total_allocated, 0);
253
254        // Record some allocations
255        profile.record_allocation(1024);
256        assert_eq!(profile.current_usage, 1024);
257        assert_eq!(profile.total_allocated, 1024);
258        assert_eq!(profile.peak_usage, 1024);
259        assert_eq!(profile.allocation_count, 1);
260
261        profile.record_allocation(2048);
262        assert_eq!(profile.current_usage, 3072);
263        assert_eq!(profile.total_allocated, 3072);
264        assert_eq!(profile.peak_usage, 3072);
265        assert_eq!(profile.allocation_count, 2);
266
267        // Record deallocation
268        profile.record_deallocation(1024);
269        assert_eq!(profile.current_usage, 2048);
270        assert_eq!(profile.total_allocated, 3072); // Total doesn't decrease
271        assert_eq!(profile.peak_usage, 3072); // Peak doesn't decrease
272        assert_eq!(profile.deallocation_count, 1);
273    }
274
275    #[test]
276    fn test_memory_efficiency_calculation() {
277        let mut profile = TaskMemoryProfile::new(1);
278
279        // Perfect efficiency (all deallocated)
280        profile.record_allocation(1000);
281        profile.record_deallocation(1000);
282        assert_eq!(profile.memory_efficiency(), 1.0);
283
284        // Partial efficiency
285        profile.record_allocation(1000); // Total now 2000, current 1000
286        assert_eq!(profile.memory_efficiency(), 0.5);
287
288        // Zero efficiency (nothing deallocated)
289        let mut profile2 = TaskMemoryProfile::new(2);
290        profile2.record_allocation(1000);
291        assert_eq!(profile2.memory_efficiency(), 0.0);
292    }
293
294    #[test]
295    fn test_memory_leak_detection() {
296        let mut profile = TaskMemoryProfile::new(1);
297
298        // No leak if not completed
299        profile.record_allocation(1000);
300        assert!(!profile.has_potential_leak());
301
302        // No leak if completed with no current usage
303        profile.record_deallocation(1000);
304        profile.mark_completed();
305        assert!(!profile.has_potential_leak());
306
307        // Potential leak if completed with current usage
308        let mut profile2 = TaskMemoryProfile::new(2);
309        profile2.record_allocation(1000);
310        profile2.mark_completed();
311        assert!(profile2.has_potential_leak());
312    }
313
314    #[test]
315    fn test_task_lifetime() {
316        let profile = TaskMemoryProfile::new(1);
317        thread::sleep(Duration::from_millis(10));
318
319        let lifetime = profile.lifetime();
320        assert!(lifetime >= Duration::from_millis(10));
321        assert!(lifetime < Duration::from_millis(100)); // Should be reasonable
322    }
323
324    #[test]
325    fn test_performance_metrics() {
326        let mut profile = TaskMemoryProfile::new(1);
327
328        // Add some allocations
329        profile.record_allocation(1_000_000); // 1MB
330        profile.record_allocation(500_000); // 500KB
331        profile.record_deallocation(500_000); // Deallocate 500KB
332
333        thread::sleep(Duration::from_millis(10));
334        profile.mark_completed();
335
336        let metrics = TaskPerformanceMetrics::from_profile(&profile);
337
338        assert!(metrics.execution_time >= Duration::from_millis(10));
339        assert!(metrics.allocation_rate > 0.0);
340        assert_eq!(metrics.efficiency_ratio, profile.memory_efficiency());
341        assert!((metrics.peak_memory_mb - 1.4305114746).abs() < 0.0001); // 1,500,000 bytes in MB
342
343        let rating = metrics.performance_rating();
344        assert!((0.0..=1.0).contains(&rating));
345    }
346
347    #[test]
348    fn test_aggregated_stats() {
349        let mut stats = AggregatedTaskStats::new();
350        assert_eq!(stats.total_tasks, 0);
351        assert_eq!(stats.completed_tasks, 0);
352
353        // Add completed task
354        let mut profile1 = TaskMemoryProfile::new(1);
355        profile1.record_allocation(1000);
356        profile1.record_deallocation(500);
357        profile1.mark_completed();
358        stats.add_task(&profile1);
359
360        assert_eq!(stats.total_tasks, 1);
361        assert_eq!(stats.completed_tasks, 1);
362        assert_eq!(stats.total_memory_allocated, 1000);
363        assert_eq!(stats.current_memory_usage, 500);
364        assert_eq!(stats.overall_efficiency, 0.5);
365
366        // Add active task
367        let mut profile2 = TaskMemoryProfile::new(2);
368        profile2.record_allocation(2000);
369        stats.add_task(&profile2);
370
371        assert_eq!(stats.total_tasks, 2);
372        assert_eq!(stats.completed_tasks, 1);
373        assert_eq!(stats.total_memory_allocated, 3000);
374        assert_eq!(stats.current_memory_usage, 2500);
375
376        // Test summary string
377        let summary = stats.memory_summary();
378        assert!(summary.contains("Tasks: 2"));
379        assert!(summary.contains("50% complete"));
380    }
381
382    #[test]
383    fn test_average_allocation_size() {
384        let mut profile = TaskMemoryProfile::new(1);
385
386        profile.record_allocation(100);
387        assert_eq!(profile.average_allocation_size, 100.0);
388
389        profile.record_allocation(200);
390        assert_eq!(profile.average_allocation_size, 150.0); // (100 + 200) / 2
391
392        profile.record_allocation(300);
393        assert_eq!(profile.average_allocation_size, 200.0); // (100 + 200 + 300) / 3
394    }
395}