1use std::fmt;
6use std::fs::File;
7use std::io::Write;
8use std::path::Path;
9use serde::{Serialize, Deserialize};
10
11use crate::{Profiler, BottleneckAnalyzer, Bottleneck};
12use crate::memory::MemoryProfiler;
13use crate::error::{ProfileResult, ProfileError};
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub enum ReportFormat {
18 Text,
20 Json,
22 Markdown,
24 Html,
26}
27
28#[derive(Debug, Clone, Serialize, Deserialize)]
30pub struct ProfileReport {
31 pub title: String,
33 pub total_duration_secs: f64,
35 pub memory: MemorySummary,
37 pub compute: ComputeSummary,
39 pub bottlenecks: Vec<Bottleneck>,
41}
42
43#[derive(Debug, Clone, Serialize, Deserialize)]
45pub struct MemorySummary {
46 pub current_usage: usize,
48 pub peak_usage: usize,
50 pub total_allocated: usize,
52 pub total_freed: usize,
54 pub allocation_count: usize,
56}
57
58#[derive(Debug, Clone, Serialize, Deserialize)]
60pub struct ComputeSummary {
61 pub operation_count: usize,
63 pub total_time_ns: u64,
65 pub top_operations: Vec<OperationSummary>,
67}
68
69#[derive(Debug, Clone, Serialize, Deserialize)]
71pub struct OperationSummary {
72 pub name: String,
74 pub total_time_ns: u64,
76 pub call_count: usize,
78 pub avg_time_ns: u64,
80 pub time_percentage: f64,
82}
83
84impl ProfileReport {
85 pub fn generate(profiler: &Profiler) -> Self {
87 let memory_profiler = profiler.memory.read();
88 let compute_profiler = profiler.compute.read();
89 let timeline_profiler = profiler.timeline.read();
90
91 let memory_stats = memory_profiler.stats();
92 let compute_stats = compute_profiler.all_stats();
93
94 let total_time_ns: u64 = compute_stats.values().map(|s| s.total_time_ns).sum();
96
97 let mut top_ops: Vec<_> = compute_stats.values().collect();
99 top_ops.sort_by(|a, b| b.total_time_ns.cmp(&a.total_time_ns));
100 let top_operations: Vec<OperationSummary> = top_ops
101 .into_iter()
102 .take(10)
103 .map(|op| {
104 let time_pct = if total_time_ns > 0 {
105 (op.total_time_ns as f64 / total_time_ns as f64) * 100.0
106 } else {
107 0.0
108 };
109 OperationSummary {
110 name: op.name.clone(),
111 total_time_ns: op.total_time_ns,
112 call_count: op.call_count,
113 avg_time_ns: if op.call_count > 0 {
114 op.total_time_ns / op.call_count as u64
115 } else {
116 0
117 },
118 time_percentage: time_pct,
119 }
120 })
121 .collect();
122
123 let analyzer = BottleneckAnalyzer::new();
125 let bottlenecks = analyzer.analyze(&compute_stats, &memory_stats);
126
127 Self {
128 title: "Axonml Profile Report".to_string(),
129 total_duration_secs: timeline_profiler.total_duration().as_secs_f64(),
130 memory: MemorySummary {
131 current_usage: memory_stats.current_usage,
132 peak_usage: memory_stats.peak_usage,
133 total_allocated: memory_stats.total_allocated,
134 total_freed: memory_stats.total_freed,
135 allocation_count: memory_stats.allocation_count,
136 },
137 compute: ComputeSummary {
138 operation_count: compute_stats.len(),
139 total_time_ns,
140 top_operations,
141 },
142 bottlenecks,
143 }
144 }
145
146 pub fn export(&self, path: &Path, format: ReportFormat) -> ProfileResult<()> {
148 let content = match format {
149 ReportFormat::Text => self.to_text(),
150 ReportFormat::Json => self.to_json()?,
151 ReportFormat::Markdown => self.to_markdown(),
152 ReportFormat::Html => self.to_html(),
153 };
154
155 let mut file = File::create(path)?;
156 file.write_all(content.as_bytes())?;
157
158 Ok(())
159 }
160
161 pub fn to_text(&self) -> String {
163 let mut output = String::new();
164
165 output.push_str(&format!("═══════════════════════════════════════════════════════════════\n"));
166 output.push_str(&format!(" {}\n", self.title));
167 output.push_str(&format!("═══════════════════════════════════════════════════════════════\n\n"));
168
169 output.push_str(&format!("Total Duration: {:.3} seconds\n\n", self.total_duration_secs));
170
171 output.push_str("─── Memory Statistics ─────────────────────────────────────────\n");
173 output.push_str(&format!(" Current Usage: {}\n", MemoryProfiler::format_bytes(self.memory.current_usage)));
174 output.push_str(&format!(" Peak Usage: {}\n", MemoryProfiler::format_bytes(self.memory.peak_usage)));
175 output.push_str(&format!(" Total Allocated: {}\n", MemoryProfiler::format_bytes(self.memory.total_allocated)));
176 output.push_str(&format!(" Total Freed: {}\n", MemoryProfiler::format_bytes(self.memory.total_freed)));
177 output.push_str(&format!(" Allocations: {}\n\n", self.memory.allocation_count));
178
179 output.push_str("─── Compute Statistics ────────────────────────────────────────\n");
181 output.push_str(&format!(" Operations Profiled: {}\n", self.compute.operation_count));
182 output.push_str(&format!(" Total Compute Time: {}\n\n", Self::format_duration_ns(self.compute.total_time_ns)));
183
184 if !self.compute.top_operations.is_empty() {
185 output.push_str(" Top Operations by Time:\n");
186 output.push_str(" ┌─────────────────────────────┬────────────┬──────────┬───────────┐\n");
187 output.push_str(" │ Operation │ Total Time │ Calls │ % Time │\n");
188 output.push_str(" ├─────────────────────────────┼────────────┼──────────┼───────────┤\n");
189
190 for op in &self.compute.top_operations {
191 let name = if op.name.len() > 27 {
192 format!("{}...", &op.name[..24])
193 } else {
194 op.name.clone()
195 };
196 output.push_str(&format!(
197 " │ {:<27} │ {:>10} │ {:>8} │ {:>8.1}% │\n",
198 name,
199 Self::format_duration_ns(op.total_time_ns),
200 op.call_count,
201 op.time_percentage
202 ));
203 }
204 output.push_str(" └─────────────────────────────┴────────────┴──────────┴───────────┘\n\n");
205 }
206
207 if !self.bottlenecks.is_empty() {
209 output.push_str("─── Bottlenecks Detected ──────────────────────────────────────\n");
210 for (i, b) in self.bottlenecks.iter().enumerate() {
211 let severity = match b.severity {
212 crate::bottleneck::Severity::Critical => "CRITICAL",
213 crate::bottleneck::Severity::High => "HIGH",
214 crate::bottleneck::Severity::Medium => "MEDIUM",
215 crate::bottleneck::Severity::Low => "LOW",
216 };
217 output.push_str(&format!("\n {}. [{}] {}\n", i + 1, severity, b.name));
218 output.push_str(&format!(" {}\n", b.description));
219 output.push_str(&format!(" → {}\n", b.suggestion));
220 }
221 output.push_str("\n");
222 } else {
223 output.push_str("─── Bottlenecks ───────────────────────────────────────────────\n");
224 output.push_str(" No bottlenecks detected.\n\n");
225 }
226
227 output.push_str("═══════════════════════════════════════════════════════════════\n");
228
229 output
230 }
231
232 pub fn to_json(&self) -> ProfileResult<String> {
234 serde_json::to_string_pretty(self)
235 .map_err(|e| ProfileError::SerializationError(e.to_string()))
236 }
237
238 pub fn to_markdown(&self) -> String {
240 let mut output = String::new();
241
242 output.push_str(&format!("# {}\n\n", self.title));
243 output.push_str(&format!("**Total Duration:** {:.3} seconds\n\n", self.total_duration_secs));
244
245 output.push_str("## Memory Statistics\n\n");
247 output.push_str("| Metric | Value |\n");
248 output.push_str("|--------|-------|\n");
249 output.push_str(&format!("| Current Usage | {} |\n", MemoryProfiler::format_bytes(self.memory.current_usage)));
250 output.push_str(&format!("| Peak Usage | {} |\n", MemoryProfiler::format_bytes(self.memory.peak_usage)));
251 output.push_str(&format!("| Total Allocated | {} |\n", MemoryProfiler::format_bytes(self.memory.total_allocated)));
252 output.push_str(&format!("| Total Freed | {} |\n", MemoryProfiler::format_bytes(self.memory.total_freed)));
253 output.push_str(&format!("| Allocations | {} |\n\n", self.memory.allocation_count));
254
255 output.push_str("## Compute Statistics\n\n");
257 output.push_str(&format!("- **Operations Profiled:** {}\n", self.compute.operation_count));
258 output.push_str(&format!("- **Total Compute Time:** {}\n\n", Self::format_duration_ns(self.compute.total_time_ns)));
259
260 if !self.compute.top_operations.is_empty() {
261 output.push_str("### Top Operations by Time\n\n");
262 output.push_str("| Operation | Total Time | Calls | % Time |\n");
263 output.push_str("|-----------|------------|-------|--------|\n");
264
265 for op in &self.compute.top_operations {
266 output.push_str(&format!(
267 "| {} | {} | {} | {:.1}% |\n",
268 op.name,
269 Self::format_duration_ns(op.total_time_ns),
270 op.call_count,
271 op.time_percentage
272 ));
273 }
274 output.push_str("\n");
275 }
276
277 output.push_str("## Bottlenecks\n\n");
279 if !self.bottlenecks.is_empty() {
280 for (i, b) in self.bottlenecks.iter().enumerate() {
281 let severity = match b.severity {
282 crate::bottleneck::Severity::Critical => "🔴 CRITICAL",
283 crate::bottleneck::Severity::High => "🟠 HIGH",
284 crate::bottleneck::Severity::Medium => "🟡 MEDIUM",
285 crate::bottleneck::Severity::Low => "🟢 LOW",
286 };
287 output.push_str(&format!("### {}. {} - {}\n\n", i + 1, severity, b.name));
288 output.push_str(&format!("{}\n\n", b.description));
289 output.push_str(&format!("**Suggestion:** {}\n\n", b.suggestion));
290 }
291 } else {
292 output.push_str("No bottlenecks detected.\n");
293 }
294
295 output
296 }
297
298 pub fn to_html(&self) -> String {
300 let mut output = String::new();
301
302 output.push_str("<!DOCTYPE html>\n<html>\n<head>\n");
303 output.push_str("<meta charset=\"UTF-8\">\n");
304 output.push_str(&format!("<title>{}</title>\n", self.title));
305 output.push_str("<style>\n");
306 output.push_str("body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; margin: 40px; }\n");
307 output.push_str("h1 { color: #333; }\n");
308 output.push_str("h2 { color: #555; border-bottom: 1px solid #ddd; padding-bottom: 5px; }\n");
309 output.push_str("table { border-collapse: collapse; width: 100%; margin: 20px 0; }\n");
310 output.push_str("th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }\n");
311 output.push_str("th { background: #f5f5f5; }\n");
312 output.push_str(".bottleneck { margin: 15px 0; padding: 15px; border-radius: 5px; }\n");
313 output.push_str(".critical { background: #fee; border-left: 4px solid #c00; }\n");
314 output.push_str(".high { background: #fff3e0; border-left: 4px solid #f80; }\n");
315 output.push_str(".medium { background: #fff9c4; border-left: 4px solid #fc0; }\n");
316 output.push_str(".low { background: #e8f5e9; border-left: 4px solid #4c4; }\n");
317 output.push_str("</style>\n</head>\n<body>\n");
318
319 output.push_str(&format!("<h1>{}</h1>\n", self.title));
320 output.push_str(&format!("<p><strong>Total Duration:</strong> {:.3} seconds</p>\n", self.total_duration_secs));
321
322 output.push_str("<h2>Memory Statistics</h2>\n");
324 output.push_str("<table>\n<tr><th>Metric</th><th>Value</th></tr>\n");
325 output.push_str(&format!("<tr><td>Current Usage</td><td>{}</td></tr>\n", MemoryProfiler::format_bytes(self.memory.current_usage)));
326 output.push_str(&format!("<tr><td>Peak Usage</td><td>{}</td></tr>\n", MemoryProfiler::format_bytes(self.memory.peak_usage)));
327 output.push_str(&format!("<tr><td>Total Allocated</td><td>{}</td></tr>\n", MemoryProfiler::format_bytes(self.memory.total_allocated)));
328 output.push_str(&format!("<tr><td>Total Freed</td><td>{}</td></tr>\n", MemoryProfiler::format_bytes(self.memory.total_freed)));
329 output.push_str(&format!("<tr><td>Allocations</td><td>{}</td></tr>\n", self.memory.allocation_count));
330 output.push_str("</table>\n");
331
332 output.push_str("<h2>Compute Statistics</h2>\n");
334 if !self.compute.top_operations.is_empty() {
335 output.push_str("<table>\n");
336 output.push_str("<tr><th>Operation</th><th>Total Time</th><th>Calls</th><th>% Time</th></tr>\n");
337 for op in &self.compute.top_operations {
338 output.push_str(&format!(
339 "<tr><td>{}</td><td>{}</td><td>{}</td><td>{:.1}%</td></tr>\n",
340 op.name,
341 Self::format_duration_ns(op.total_time_ns),
342 op.call_count,
343 op.time_percentage
344 ));
345 }
346 output.push_str("</table>\n");
347 }
348
349 output.push_str("<h2>Bottlenecks</h2>\n");
351 if !self.bottlenecks.is_empty() {
352 for b in &self.bottlenecks {
353 let class = match b.severity {
354 crate::bottleneck::Severity::Critical => "critical",
355 crate::bottleneck::Severity::High => "high",
356 crate::bottleneck::Severity::Medium => "medium",
357 crate::bottleneck::Severity::Low => "low",
358 };
359 output.push_str(&format!("<div class=\"bottleneck {}\">\n", class));
360 output.push_str(&format!("<strong>{}</strong>\n", b.name));
361 output.push_str(&format!("<p>{}</p>\n", b.description));
362 output.push_str(&format!("<p><em>Suggestion: {}</em></p>\n", b.suggestion));
363 output.push_str("</div>\n");
364 }
365 } else {
366 output.push_str("<p>No bottlenecks detected.</p>\n");
367 }
368
369 output.push_str("</body>\n</html>\n");
370
371 output
372 }
373
374 fn format_duration_ns(ns: u64) -> String {
376 if ns >= 1_000_000_000 {
377 format!("{:.2}s", ns as f64 / 1e9)
378 } else if ns >= 1_000_000 {
379 format!("{:.2}ms", ns as f64 / 1e6)
380 } else if ns >= 1_000 {
381 format!("{:.2}µs", ns as f64 / 1e3)
382 } else {
383 format!("{}ns", ns)
384 }
385 }
386}
387
388impl fmt::Display for ProfileReport {
389 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
390 write!(f, "{}", self.to_text())
391 }
392}
393
394#[cfg(test)]
395mod tests {
396 use super::*;
397 use crate::Profiler;
398
399 #[test]
400 fn test_report_generation() {
401 let profiler = Profiler::new();
402 profiler.start("test_op");
403 std::thread::sleep(std::time::Duration::from_millis(10));
404 profiler.stop("test_op");
405 profiler.record_alloc("tensor", 1024);
406
407 let report = ProfileReport::generate(&profiler);
408
409 assert_eq!(report.compute.operation_count, 1);
410 assert!(report.memory.total_allocated >= 1024);
411 }
412
413 #[test]
414 fn test_text_format() {
415 let profiler = Profiler::new();
416 profiler.start("op1");
417 profiler.stop("op1");
418
419 let report = ProfileReport::generate(&profiler);
420 let text = report.to_text();
421
422 assert!(text.contains("Axonml Profile Report"));
423 assert!(text.contains("Memory Statistics"));
424 assert!(text.contains("Compute Statistics"));
425 }
426
427 #[test]
428 fn test_json_format() {
429 let profiler = Profiler::new();
430 let report = ProfileReport::generate(&profiler);
431 let json = report.to_json().unwrap();
432
433 assert!(json.contains("title"));
434 assert!(json.contains("memory"));
435 assert!(json.contains("compute"));
436 }
437
438 #[test]
439 fn test_markdown_format() {
440 let profiler = Profiler::new();
441 let report = ProfileReport::generate(&profiler);
442 let md = report.to_markdown();
443
444 assert!(md.contains("# Axonml Profile Report"));
445 assert!(md.contains("## Memory Statistics"));
446 }
447
448 #[test]
449 fn test_html_format() {
450 let profiler = Profiler::new();
451 let report = ProfileReport::generate(&profiler);
452 let html = report.to_html();
453
454 assert!(html.contains("<!DOCTYPE html>"));
455 assert!(html.contains("<title>Axonml Profile Report</title>"));
456 }
457}