1use std::io::Write;
7use std::path::Path;
8
9use serde::{Deserialize, Serialize};
10
11use super::{MemoryReport, ProfileReport};
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
15pub enum ReportFormat {
16 Json,
18 Text,
20 Markdown,
22 Csv,
24}
25
26pub struct ReportExporter;
28
29impl ReportExporter {
30 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 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 fn to_json(report: &ProfileReport) -> String {
54 serde_json::to_string_pretty(report).unwrap_or_else(|e| format!("Error: {}", e))
55 }
56
57 fn to_text(report: &ProfileReport) -> String {
59 report.to_summary()
60 }
61
62 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 fn to_csv(report: &ProfileReport) -> String {
166 let mut csv = String::new();
167
168 csv.push_str("region,current_bytes,peak_bytes,total_allocated,total_deallocated,allocation_count,deallocation_count\n");
170
171 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 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#[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 pub fn new() -> Self {
215 Self::default()
216 }
217
218 pub fn current_bytes(mut self, bytes: u64) -> Self {
220 self.current_bytes = bytes;
221 self
222 }
223
224 pub fn peak_bytes(mut self, bytes: u64) -> Self {
226 self.peak_bytes = bytes;
227 self
228 }
229
230 pub fn total_allocated(mut self, bytes: u64) -> Self {
232 self.total_allocated = bytes;
233 self
234 }
235
236 pub fn total_deallocated(mut self, bytes: u64) -> Self {
238 self.total_deallocated = bytes;
239 self
240 }
241
242 pub fn allocation_count(mut self, count: u64) -> Self {
244 self.allocation_count = count;
245 self
246 }
247
248 pub fn deallocation_count(mut self, count: u64) -> Self {
250 self.deallocation_count = count;
251 self
252 }
253
254 pub fn growth_rate(mut self, rate: f64) -> Self {
256 self.growth_rate = Some(rate);
257 self
258 }
259
260 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#[derive(Debug, Clone, Serialize, Deserialize)]
278pub struct ReportComparison {
279 pub first_timestamp: chrono::DateTime<chrono::Utc>,
281
282 pub second_timestamp: chrono::DateTime<chrono::Utc>,
284
285 pub time_diff_secs: i64,
287
288 pub memory_diff_bytes: i64,
290
291 pub memory_change_percent: f64,
293
294 pub allocation_diff: i64,
296
297 pub new_warnings: usize,
299
300 pub resolved_warnings: usize,
302}
303
304impl ReportComparison {
305 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 pub fn is_memory_increasing(&self) -> bool {
344 self.memory_diff_bytes > 0
345 }
346
347 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}