Skip to main content

mabi_core/profiling/
report.rs

1//! Profiling report generation and export.
2//!
3//! Provides utilities for generating, formatting, and exporting
4//! profiling reports in various formats.
5
6use std::io::Write;
7use std::path::Path;
8
9use serde::{Deserialize, Serialize};
10
11use super::{MemoryReport, ProfileReport};
12
13/// Report format options.
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
15pub enum ReportFormat {
16    /// JSON format.
17    Json,
18    /// Human-readable text format.
19    Text,
20    /// Markdown format.
21    Markdown,
22    /// CSV format (for data analysis).
23    Csv,
24}
25
26/// Report exporter for various formats.
27pub struct ReportExporter;
28
29impl ReportExporter {
30    /// Export a profile report to a string.
31    pub fn export_to_string(report: &ProfileReport, format: ReportFormat) -> String {
32        match format {
33            ReportFormat::Json => Self::to_json(report),
34            ReportFormat::Text => Self::to_text(report),
35            ReportFormat::Markdown => Self::to_markdown(report),
36            ReportFormat::Csv => Self::to_csv(report),
37        }
38    }
39
40    /// Export a profile report to a file.
41    pub fn export_to_file<P: AsRef<Path>>(
42        report: &ProfileReport,
43        path: P,
44        format: ReportFormat,
45    ) -> std::io::Result<()> {
46        let content = Self::export_to_string(report, format);
47        let mut file = std::fs::File::create(path)?;
48        file.write_all(content.as_bytes())?;
49        Ok(())
50    }
51
52    /// Convert report to JSON format.
53    fn to_json(report: &ProfileReport) -> String {
54        serde_json::to_string_pretty(report).unwrap_or_else(|e| format!("Error: {}", e))
55    }
56
57    /// Convert report to human-readable text format.
58    fn to_text(report: &ProfileReport) -> String {
59        report.to_summary()
60    }
61
62    /// Convert report to Markdown format.
63    fn to_markdown(report: &ProfileReport) -> String {
64        let mut md = String::new();
65
66        md.push_str("# TRAP Simulator Profiling Report\n\n");
67        md.push_str(&format!(
68            "**Generated:** {}\n\n",
69            report.generated_at.format("%Y-%m-%d %H:%M:%S UTC")
70        ));
71        md.push_str(&format!(
72            "**Status:** {}\n\n",
73            if report.is_running {
74                "đŸŸĸ Running"
75            } else {
76                "🔴 Stopped"
77            }
78        ));
79
80        md.push_str("## Memory Usage\n\n");
81        md.push_str("| Metric | Value |\n");
82        md.push_str("|--------|-------|\n");
83        md.push_str(&format!(
84            "| Current Memory | {} MB |\n",
85            report.memory.current_bytes / 1024 / 1024
86        ));
87        md.push_str(&format!(
88            "| Peak Memory | {} MB |\n",
89            report.memory.peak_bytes / 1024 / 1024
90        ));
91        md.push_str(&format!(
92            "| Total Allocated | {} MB |\n",
93            report.memory.total_allocated / 1024 / 1024
94        ));
95        md.push_str(&format!(
96            "| Total Deallocated | {} MB |\n",
97            report.memory.total_deallocated / 1024 / 1024
98        ));
99        md.push_str(&format!(
100            "| Allocation Count | {} |\n",
101            report.memory.allocation_count
102        ));
103        md.push_str(&format!(
104            "| Deallocation Count | {} |\n",
105            report.memory.deallocation_count
106        ));
107
108        if let Some(rate) = report.memory.growth_rate_bytes_per_sec {
109            md.push_str(&format!(
110                "| Growth Rate | {:.2} KB/s |\n",
111                rate / 1024.0
112            ));
113        }
114
115        if !report.memory.regions.is_empty() {
116            md.push_str("\n## Memory Regions\n\n");
117            md.push_str("| Region | Current | Peak | Allocations |\n");
118            md.push_str("|--------|---------|------|-------------|\n");
119
120            let mut regions: Vec<_> = report.memory.regions.iter().collect();
121            regions.sort_by(|a, b| b.1.current_bytes.cmp(&a.1.current_bytes));
122
123            for (name, region) in regions {
124                md.push_str(&format!(
125                    "| {} | {} KB | {} KB | {} |\n",
126                    name,
127                    region.current_bytes / 1024,
128                    region.peak_bytes / 1024,
129                    region.allocation_count
130                ));
131            }
132        }
133
134        if !report.leak_warnings.is_empty() {
135            md.push_str("\n## âš ī¸ Leak Warnings\n\n");
136            for warning in &report.leak_warnings {
137                let emoji = match warning.severity {
138                    super::LeakSeverity::Low => "â„šī¸",
139                    super::LeakSeverity::Medium => "âš ī¸",
140                    super::LeakSeverity::High => "đŸ”ļ",
141                    super::LeakSeverity::Critical => "🔴",
142                };
143                md.push_str(&format!(
144                    "- {} **{}** ({}): {}\n",
145                    emoji, warning.region, warning.severity, warning.message
146                ));
147                md.push_str(&format!(
148                    "  - Growth rate: {:.1}%/min\n",
149                    warning.growth_rate_per_minute
150                ));
151                md.push_str(&format!(
152                    "  - Confidence: {:.0}%\n",
153                    warning.confidence * 100.0
154                ));
155            }
156        } else {
157            md.push_str("\n## ✅ No Leak Warnings\n\n");
158            md.push_str("No potential memory leaks detected.\n");
159        }
160
161        md
162    }
163
164    /// Convert report to CSV format.
165    fn to_csv(report: &ProfileReport) -> String {
166        let mut csv = String::new();
167
168        // Header
169        csv.push_str("region,current_bytes,peak_bytes,total_allocated,total_deallocated,allocation_count,deallocation_count\n");
170
171        // Global row
172        csv.push_str(&format!(
173            "global,{},{},{},{},{},{}\n",
174            report.memory.current_bytes,
175            report.memory.peak_bytes,
176            report.memory.total_allocated,
177            report.memory.total_deallocated,
178            report.memory.allocation_count,
179            report.memory.deallocation_count
180        ));
181
182        // Per-region rows
183        for (name, region) in &report.memory.regions {
184            csv.push_str(&format!(
185                "{},{},{},{},{},{},{}\n",
186                name,
187                region.current_bytes,
188                region.peak_bytes,
189                region.total_allocated,
190                region.total_deallocated,
191                region.allocation_count,
192                region.deallocation_count
193            ));
194        }
195
196        csv
197    }
198}
199
200/// Memory report builder for custom reports.
201#[derive(Default)]
202pub struct MemoryReportBuilder {
203    current_bytes: u64,
204    peak_bytes: u64,
205    total_allocated: u64,
206    total_deallocated: u64,
207    allocation_count: u64,
208    deallocation_count: u64,
209    growth_rate: Option<f64>,
210}
211
212impl MemoryReportBuilder {
213    /// Create a new builder.
214    pub fn new() -> Self {
215        Self::default()
216    }
217
218    /// Set current bytes.
219    pub fn current_bytes(mut self, bytes: u64) -> Self {
220        self.current_bytes = bytes;
221        self
222    }
223
224    /// Set peak bytes.
225    pub fn peak_bytes(mut self, bytes: u64) -> Self {
226        self.peak_bytes = bytes;
227        self
228    }
229
230    /// Set total allocated.
231    pub fn total_allocated(mut self, bytes: u64) -> Self {
232        self.total_allocated = bytes;
233        self
234    }
235
236    /// Set total deallocated.
237    pub fn total_deallocated(mut self, bytes: u64) -> Self {
238        self.total_deallocated = bytes;
239        self
240    }
241
242    /// Set allocation count.
243    pub fn allocation_count(mut self, count: u64) -> Self {
244        self.allocation_count = count;
245        self
246    }
247
248    /// Set deallocation count.
249    pub fn deallocation_count(mut self, count: u64) -> Self {
250        self.deallocation_count = count;
251        self
252    }
253
254    /// Set growth rate.
255    pub fn growth_rate(mut self, rate: f64) -> Self {
256        self.growth_rate = Some(rate);
257        self
258    }
259
260    /// Build the memory report.
261    pub fn build(self) -> MemoryReport {
262        MemoryReport {
263            current_bytes: self.current_bytes,
264            peak_bytes: self.peak_bytes,
265            total_allocated: self.total_allocated,
266            total_deallocated: self.total_deallocated,
267            allocation_count: self.allocation_count,
268            deallocation_count: self.deallocation_count,
269            regions: std::collections::HashMap::new(),
270            growth_rate_bytes_per_sec: self.growth_rate,
271            snapshot_count: 0,
272        }
273    }
274}
275
276/// Comparison between two profile reports.
277#[derive(Debug, Clone, Serialize, Deserialize)]
278pub struct ReportComparison {
279    /// First report timestamp.
280    pub first_timestamp: chrono::DateTime<chrono::Utc>,
281
282    /// Second report timestamp.
283    pub second_timestamp: chrono::DateTime<chrono::Utc>,
284
285    /// Time difference.
286    pub time_diff_secs: i64,
287
288    /// Memory difference in bytes.
289    pub memory_diff_bytes: i64,
290
291    /// Memory change percentage.
292    pub memory_change_percent: f64,
293
294    /// Allocation count difference.
295    pub allocation_diff: i64,
296
297    /// New leak warnings in second report.
298    pub new_warnings: usize,
299
300    /// Resolved warnings (in first but not in second).
301    pub resolved_warnings: usize,
302}
303
304impl ReportComparison {
305    /// Compare two profile reports.
306    pub fn compare(first: &ProfileReport, second: &ProfileReport) -> Self {
307        let memory_diff =
308            second.memory.current_bytes as i64 - first.memory.current_bytes as i64;
309        let memory_change_percent = if first.memory.current_bytes > 0 {
310            (memory_diff as f64 / first.memory.current_bytes as f64) * 100.0
311        } else {
312            0.0
313        };
314
315        let first_warnings: std::collections::HashSet<_> = first
316            .leak_warnings
317            .iter()
318            .map(|w| &w.region)
319            .collect();
320        let second_warnings: std::collections::HashSet<_> = second
321            .leak_warnings
322            .iter()
323            .map(|w| &w.region)
324            .collect();
325
326        let new_warnings = second_warnings.difference(&first_warnings).count();
327        let resolved_warnings = first_warnings.difference(&second_warnings).count();
328
329        Self {
330            first_timestamp: first.generated_at,
331            second_timestamp: second.generated_at,
332            time_diff_secs: (second.generated_at - first.generated_at).num_seconds(),
333            memory_diff_bytes: memory_diff,
334            memory_change_percent,
335            allocation_diff: second.memory.allocation_count as i64
336                - first.memory.allocation_count as i64,
337            new_warnings,
338            resolved_warnings,
339        }
340    }
341
342    /// Check if memory usage is increasing.
343    pub fn is_memory_increasing(&self) -> bool {
344        self.memory_diff_bytes > 0
345    }
346
347    /// Get human-readable summary.
348    pub fn summary(&self) -> String {
349        let direction = if self.memory_diff_bytes > 0 {
350            "increased"
351        } else if self.memory_diff_bytes < 0 {
352            "decreased"
353        } else {
354            "unchanged"
355        };
356
357        format!(
358            "Memory {} by {} bytes ({:.1}%) over {} seconds. {} new warnings, {} resolved.",
359            direction,
360            self.memory_diff_bytes.abs(),
361            self.memory_change_percent.abs(),
362            self.time_diff_secs,
363            self.new_warnings,
364            self.resolved_warnings
365        )
366    }
367}
368
369#[cfg(test)]
370mod tests {
371    use super::*;
372    use crate::profiling::{LeakSeverity, LeakWarning, ProfilerConfig};
373
374    fn create_test_report(current_bytes: u64, warnings: Vec<LeakWarning>) -> ProfileReport {
375        ProfileReport {
376            generated_at: chrono::Utc::now(),
377            config: ProfilerConfig::default(),
378            memory: MemoryReport {
379                current_bytes,
380                peak_bytes: current_bytes + 1000,
381                total_allocated: current_bytes * 2,
382                total_deallocated: current_bytes,
383                allocation_count: 100,
384                deallocation_count: 50,
385                regions: std::collections::HashMap::new(),
386                growth_rate_bytes_per_sec: Some(100.0),
387                snapshot_count: 10,
388            },
389            leak_warnings: warnings,
390            is_running: true,
391        }
392    }
393
394    #[test]
395    fn test_export_json() {
396        let report = create_test_report(1024, vec![]);
397        let json = ReportExporter::export_to_string(&report, ReportFormat::Json);
398        assert!(json.contains("current_bytes"));
399        assert!(json.contains("1024"));
400    }
401
402    #[test]
403    fn test_export_text() {
404        let report = create_test_report(1024 * 1024, vec![]);
405        let text = ReportExporter::export_to_string(&report, ReportFormat::Text);
406        assert!(text.contains("Memory Usage"));
407        assert!(text.contains("MB"));
408    }
409
410    #[test]
411    fn test_export_markdown() {
412        let report = create_test_report(1024 * 1024, vec![]);
413        let md = ReportExporter::export_to_string(&report, ReportFormat::Markdown);
414        assert!(md.contains("# TRAP Simulator"));
415        assert!(md.contains("| Metric | Value |"));
416    }
417
418    #[test]
419    fn test_export_csv() {
420        let report = create_test_report(1024, vec![]);
421        let csv = ReportExporter::export_to_string(&report, ReportFormat::Csv);
422        assert!(csv.contains("region,current_bytes"));
423        assert!(csv.contains("global,1024"));
424    }
425
426    #[test]
427    fn test_memory_report_builder() {
428        let report = MemoryReportBuilder::new()
429            .current_bytes(1000)
430            .peak_bytes(2000)
431            .allocation_count(10)
432            .build();
433
434        assert_eq!(report.current_bytes, 1000);
435        assert_eq!(report.peak_bytes, 2000);
436        assert_eq!(report.allocation_count, 10);
437    }
438
439    #[test]
440    fn test_report_comparison() {
441        let report1 = create_test_report(1000, vec![]);
442        let report2 = create_test_report(1500, vec![]);
443
444        let comparison = ReportComparison::compare(&report1, &report2);
445        assert!(comparison.is_memory_increasing());
446        assert_eq!(comparison.memory_diff_bytes, 500);
447    }
448
449    #[test]
450    fn test_report_comparison_with_warnings() {
451        let warning = LeakWarning {
452            region: "test".to_string(),
453            severity: LeakSeverity::Medium,
454            message: "test warning".to_string(),
455            growth_rate_per_minute: 5.0,
456            current_bytes: 1000,
457            samples_analyzed: 10,
458            confidence: 0.8,
459        };
460
461        let report1 = create_test_report(1000, vec![]);
462        let report2 = create_test_report(1500, vec![warning]);
463
464        let comparison = ReportComparison::compare(&report1, &report2);
465        assert_eq!(comparison.new_warnings, 1);
466        assert_eq!(comparison.resolved_warnings, 0);
467    }
468}