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!("| Growth Rate | {:.2} KB/s |\n", rate / 1024.0));
110        }
111
112        if !report.memory.regions.is_empty() {
113            md.push_str("\n## Memory Regions\n\n");
114            md.push_str("| Region | Current | Peak | Allocations |\n");
115            md.push_str("|--------|---------|------|-------------|\n");
116
117            let mut regions: Vec<_> = report.memory.regions.iter().collect();
118            regions.sort_by(|a, b| b.1.current_bytes.cmp(&a.1.current_bytes));
119
120            for (name, region) in regions {
121                md.push_str(&format!(
122                    "| {} | {} KB | {} KB | {} |\n",
123                    name,
124                    region.current_bytes / 1024,
125                    region.peak_bytes / 1024,
126                    region.allocation_count
127                ));
128            }
129        }
130
131        if !report.leak_warnings.is_empty() {
132            md.push_str("\n## âš ī¸ Leak Warnings\n\n");
133            for warning in &report.leak_warnings {
134                let emoji = match warning.severity {
135                    super::LeakSeverity::Low => "â„šī¸",
136                    super::LeakSeverity::Medium => "âš ī¸",
137                    super::LeakSeverity::High => "đŸ”ļ",
138                    super::LeakSeverity::Critical => "🔴",
139                };
140                md.push_str(&format!(
141                    "- {} **{}** ({}): {}\n",
142                    emoji, warning.region, warning.severity, warning.message
143                ));
144                md.push_str(&format!(
145                    "  - Growth rate: {:.1}%/min\n",
146                    warning.growth_rate_per_minute
147                ));
148                md.push_str(&format!(
149                    "  - Confidence: {:.0}%\n",
150                    warning.confidence * 100.0
151                ));
152            }
153        } else {
154            md.push_str("\n## ✅ No Leak Warnings\n\n");
155            md.push_str("No potential memory leaks detected.\n");
156        }
157
158        md
159    }
160
161    /// Convert report to CSV format.
162    fn to_csv(report: &ProfileReport) -> String {
163        let mut csv = String::new();
164
165        // Header
166        csv.push_str("region,current_bytes,peak_bytes,total_allocated,total_deallocated,allocation_count,deallocation_count\n");
167
168        // Global row
169        csv.push_str(&format!(
170            "global,{},{},{},{},{},{}\n",
171            report.memory.current_bytes,
172            report.memory.peak_bytes,
173            report.memory.total_allocated,
174            report.memory.total_deallocated,
175            report.memory.allocation_count,
176            report.memory.deallocation_count
177        ));
178
179        // Per-region rows
180        for (name, region) in &report.memory.regions {
181            csv.push_str(&format!(
182                "{},{},{},{},{},{},{}\n",
183                name,
184                region.current_bytes,
185                region.peak_bytes,
186                region.total_allocated,
187                region.total_deallocated,
188                region.allocation_count,
189                region.deallocation_count
190            ));
191        }
192
193        csv
194    }
195}
196
197/// Memory report builder for custom reports.
198#[derive(Default)]
199pub struct MemoryReportBuilder {
200    current_bytes: u64,
201    peak_bytes: u64,
202    total_allocated: u64,
203    total_deallocated: u64,
204    allocation_count: u64,
205    deallocation_count: u64,
206    growth_rate: Option<f64>,
207}
208
209impl MemoryReportBuilder {
210    /// Create a new builder.
211    pub fn new() -> Self {
212        Self::default()
213    }
214
215    /// Set current bytes.
216    pub fn current_bytes(mut self, bytes: u64) -> Self {
217        self.current_bytes = bytes;
218        self
219    }
220
221    /// Set peak bytes.
222    pub fn peak_bytes(mut self, bytes: u64) -> Self {
223        self.peak_bytes = bytes;
224        self
225    }
226
227    /// Set total allocated.
228    pub fn total_allocated(mut self, bytes: u64) -> Self {
229        self.total_allocated = bytes;
230        self
231    }
232
233    /// Set total deallocated.
234    pub fn total_deallocated(mut self, bytes: u64) -> Self {
235        self.total_deallocated = bytes;
236        self
237    }
238
239    /// Set allocation count.
240    pub fn allocation_count(mut self, count: u64) -> Self {
241        self.allocation_count = count;
242        self
243    }
244
245    /// Set deallocation count.
246    pub fn deallocation_count(mut self, count: u64) -> Self {
247        self.deallocation_count = count;
248        self
249    }
250
251    /// Set growth rate.
252    pub fn growth_rate(mut self, rate: f64) -> Self {
253        self.growth_rate = Some(rate);
254        self
255    }
256
257    /// Build the memory report.
258    pub fn build(self) -> MemoryReport {
259        MemoryReport {
260            current_bytes: self.current_bytes,
261            peak_bytes: self.peak_bytes,
262            total_allocated: self.total_allocated,
263            total_deallocated: self.total_deallocated,
264            allocation_count: self.allocation_count,
265            deallocation_count: self.deallocation_count,
266            regions: std::collections::HashMap::new(),
267            growth_rate_bytes_per_sec: self.growth_rate,
268            snapshot_count: 0,
269        }
270    }
271}
272
273/// Comparison between two profile reports.
274#[derive(Debug, Clone, Serialize, Deserialize)]
275pub struct ReportComparison {
276    /// First report timestamp.
277    pub first_timestamp: chrono::DateTime<chrono::Utc>,
278
279    /// Second report timestamp.
280    pub second_timestamp: chrono::DateTime<chrono::Utc>,
281
282    /// Time difference.
283    pub time_diff_secs: i64,
284
285    /// Memory difference in bytes.
286    pub memory_diff_bytes: i64,
287
288    /// Memory change percentage.
289    pub memory_change_percent: f64,
290
291    /// Allocation count difference.
292    pub allocation_diff: i64,
293
294    /// New leak warnings in second report.
295    pub new_warnings: usize,
296
297    /// Resolved warnings (in first but not in second).
298    pub resolved_warnings: usize,
299}
300
301impl ReportComparison {
302    /// Compare two profile reports.
303    pub fn compare(first: &ProfileReport, second: &ProfileReport) -> Self {
304        let memory_diff = second.memory.current_bytes as i64 - first.memory.current_bytes as i64;
305        let memory_change_percent = if first.memory.current_bytes > 0 {
306            (memory_diff as f64 / first.memory.current_bytes as f64) * 100.0
307        } else {
308            0.0
309        };
310
311        let first_warnings: std::collections::HashSet<_> =
312            first.leak_warnings.iter().map(|w| &w.region).collect();
313        let second_warnings: std::collections::HashSet<_> =
314            second.leak_warnings.iter().map(|w| &w.region).collect();
315
316        let new_warnings = second_warnings.difference(&first_warnings).count();
317        let resolved_warnings = first_warnings.difference(&second_warnings).count();
318
319        Self {
320            first_timestamp: first.generated_at,
321            second_timestamp: second.generated_at,
322            time_diff_secs: (second.generated_at - first.generated_at).num_seconds(),
323            memory_diff_bytes: memory_diff,
324            memory_change_percent,
325            allocation_diff: second.memory.allocation_count as i64
326                - first.memory.allocation_count as i64,
327            new_warnings,
328            resolved_warnings,
329        }
330    }
331
332    /// Check if memory usage is increasing.
333    pub fn is_memory_increasing(&self) -> bool {
334        self.memory_diff_bytes > 0
335    }
336
337    /// Get human-readable summary.
338    pub fn summary(&self) -> String {
339        let direction = if self.memory_diff_bytes > 0 {
340            "increased"
341        } else if self.memory_diff_bytes < 0 {
342            "decreased"
343        } else {
344            "unchanged"
345        };
346
347        format!(
348            "Memory {} by {} bytes ({:.1}%) over {} seconds. {} new warnings, {} resolved.",
349            direction,
350            self.memory_diff_bytes.abs(),
351            self.memory_change_percent.abs(),
352            self.time_diff_secs,
353            self.new_warnings,
354            self.resolved_warnings
355        )
356    }
357}
358
359#[cfg(test)]
360mod tests {
361    use super::*;
362    use crate::profiling::{LeakSeverity, LeakWarning, ProfilerConfig};
363
364    fn create_test_report(current_bytes: u64, warnings: Vec<LeakWarning>) -> ProfileReport {
365        ProfileReport {
366            generated_at: chrono::Utc::now(),
367            config: ProfilerConfig::default(),
368            memory: MemoryReport {
369                current_bytes,
370                peak_bytes: current_bytes + 1000,
371                total_allocated: current_bytes * 2,
372                total_deallocated: current_bytes,
373                allocation_count: 100,
374                deallocation_count: 50,
375                regions: std::collections::HashMap::new(),
376                growth_rate_bytes_per_sec: Some(100.0),
377                snapshot_count: 10,
378            },
379            leak_warnings: warnings,
380            is_running: true,
381        }
382    }
383
384    #[test]
385    fn test_export_json() {
386        let report = create_test_report(1024, vec![]);
387        let json = ReportExporter::export_to_string(&report, ReportFormat::Json);
388        assert!(json.contains("current_bytes"));
389        assert!(json.contains("1024"));
390    }
391
392    #[test]
393    fn test_export_text() {
394        let report = create_test_report(1024 * 1024, vec![]);
395        let text = ReportExporter::export_to_string(&report, ReportFormat::Text);
396        assert!(text.contains("Memory Usage"));
397        assert!(text.contains("MB"));
398    }
399
400    #[test]
401    fn test_export_markdown() {
402        let report = create_test_report(1024 * 1024, vec![]);
403        let md = ReportExporter::export_to_string(&report, ReportFormat::Markdown);
404        assert!(md.contains("# TRAP Simulator"));
405        assert!(md.contains("| Metric | Value |"));
406    }
407
408    #[test]
409    fn test_export_csv() {
410        let report = create_test_report(1024, vec![]);
411        let csv = ReportExporter::export_to_string(&report, ReportFormat::Csv);
412        assert!(csv.contains("region,current_bytes"));
413        assert!(csv.contains("global,1024"));
414    }
415
416    #[test]
417    fn test_memory_report_builder() {
418        let report = MemoryReportBuilder::new()
419            .current_bytes(1000)
420            .peak_bytes(2000)
421            .allocation_count(10)
422            .build();
423
424        assert_eq!(report.current_bytes, 1000);
425        assert_eq!(report.peak_bytes, 2000);
426        assert_eq!(report.allocation_count, 10);
427    }
428
429    #[test]
430    fn test_report_comparison() {
431        let report1 = create_test_report(1000, vec![]);
432        let report2 = create_test_report(1500, vec![]);
433
434        let comparison = ReportComparison::compare(&report1, &report2);
435        assert!(comparison.is_memory_increasing());
436        assert_eq!(comparison.memory_diff_bytes, 500);
437    }
438
439    #[test]
440    fn test_report_comparison_with_warnings() {
441        let warning = LeakWarning {
442            region: "test".to_string(),
443            severity: LeakSeverity::Medium,
444            message: "test warning".to_string(),
445            growth_rate_per_minute: 5.0,
446            current_bytes: 1000,
447            samples_analyzed: 10,
448            confidence: 0.8,
449        };
450
451        let report1 = create_test_report(1000, vec![]);
452        let report2 = create_test_report(1500, vec![warning]);
453
454        let comparison = ReportComparison::compare(&report1, &report2);
455        assert_eq!(comparison.new_warnings, 1);
456        assert_eq!(comparison.resolved_warnings, 0);
457    }
458}