Skip to main content

axonml_profile/
report.rs

1//! Report Generation Module
2//!
3//! Generates formatted profiling reports in various output formats.
4
5use 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/// Output format for profiling reports.
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub enum ReportFormat {
18    /// Plain text format
19    Text,
20    /// JSON format
21    Json,
22    /// Markdown format
23    Markdown,
24    /// HTML format
25    Html,
26}
27
28/// A complete profiling report.
29#[derive(Debug, Clone, Serialize, Deserialize)]
30pub struct ProfileReport {
31    /// Report title
32    pub title: String,
33    /// Total profiling duration in seconds
34    pub total_duration_secs: f64,
35    /// Memory statistics
36    pub memory: MemorySummary,
37    /// Compute statistics
38    pub compute: ComputeSummary,
39    /// Detected bottlenecks
40    pub bottlenecks: Vec<Bottleneck>,
41}
42
43/// Summary of memory profiling.
44#[derive(Debug, Clone, Serialize, Deserialize)]
45pub struct MemorySummary {
46    /// Current memory usage in bytes
47    pub current_usage: usize,
48    /// Peak memory usage in bytes
49    pub peak_usage: usize,
50    /// Total bytes allocated
51    pub total_allocated: usize,
52    /// Total bytes freed
53    pub total_freed: usize,
54    /// Number of allocations
55    pub allocation_count: usize,
56}
57
58/// Summary of compute profiling.
59#[derive(Debug, Clone, Serialize, Deserialize)]
60pub struct ComputeSummary {
61    /// Number of unique operations profiled
62    pub operation_count: usize,
63    /// Total compute time in nanoseconds
64    pub total_time_ns: u64,
65    /// Top operations by time
66    pub top_operations: Vec<OperationSummary>,
67}
68
69/// Summary of a single operation.
70#[derive(Debug, Clone, Serialize, Deserialize)]
71pub struct OperationSummary {
72    /// Operation name
73    pub name: String,
74    /// Total time in nanoseconds
75    pub total_time_ns: u64,
76    /// Number of calls
77    pub call_count: usize,
78    /// Average time in nanoseconds
79    pub avg_time_ns: u64,
80    /// Percentage of total time
81    pub time_percentage: f64,
82}
83
84impl ProfileReport {
85    /// Generates a report from a profiler.
86    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        // Calculate total compute time
95        let total_time_ns: u64 = compute_stats.values().map(|s| s.total_time_ns).sum();
96
97        // Get top operations
98        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        // Analyze bottlenecks
124        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    /// Exports the report to a file.
147    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    /// Converts report to plain text.
162    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        // Memory section
172        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        // Compute section
180        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        // Bottlenecks section
208        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    /// Converts report to JSON.
233    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    /// Converts report to Markdown.
239    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        // Memory section
246        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        // Compute section
256        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        // Bottlenecks section
278        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    /// Converts report to HTML.
299    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        // Memory section
323        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        // Compute section
333        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        // Bottlenecks
350        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    /// Formats nanoseconds into a human-readable string.
375    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}