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!("| 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 fn to_csv(report: &ProfileReport) -> String {
163 let mut csv = String::new();
164
165 csv.push_str("region,current_bytes,peak_bytes,total_allocated,total_deallocated,allocation_count,deallocation_count\n");
167
168 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 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#[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 pub fn new() -> Self {
212 Self::default()
213 }
214
215 pub fn current_bytes(mut self, bytes: u64) -> Self {
217 self.current_bytes = bytes;
218 self
219 }
220
221 pub fn peak_bytes(mut self, bytes: u64) -> Self {
223 self.peak_bytes = bytes;
224 self
225 }
226
227 pub fn total_allocated(mut self, bytes: u64) -> Self {
229 self.total_allocated = bytes;
230 self
231 }
232
233 pub fn total_deallocated(mut self, bytes: u64) -> Self {
235 self.total_deallocated = bytes;
236 self
237 }
238
239 pub fn allocation_count(mut self, count: u64) -> Self {
241 self.allocation_count = count;
242 self
243 }
244
245 pub fn deallocation_count(mut self, count: u64) -> Self {
247 self.deallocation_count = count;
248 self
249 }
250
251 pub fn growth_rate(mut self, rate: f64) -> Self {
253 self.growth_rate = Some(rate);
254 self
255 }
256
257 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#[derive(Debug, Clone, Serialize, Deserialize)]
275pub struct ReportComparison {
276 pub first_timestamp: chrono::DateTime<chrono::Utc>,
278
279 pub second_timestamp: chrono::DateTime<chrono::Utc>,
281
282 pub time_diff_secs: i64,
284
285 pub memory_diff_bytes: i64,
287
288 pub memory_change_percent: f64,
290
291 pub allocation_diff: i64,
293
294 pub new_warnings: usize,
296
297 pub resolved_warnings: usize,
299}
300
301impl ReportComparison {
302 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 pub fn is_memory_increasing(&self) -> bool {
334 self.memory_diff_bytes > 0
335 }
336
337 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}