Skip to main content

trueno_explain/
output.rs

1//! Output formatters for trueno-explain
2//!
3//! Supports text (terminal), JSON, and future TUI output.
4
5use crate::analyzer::{AnalysisReport, MudaType};
6use colored::{ColoredString, Colorize};
7use std::io::{self, Write};
8
9/// Output format options
10#[derive(Debug, Clone, Copy, Default)]
11pub enum OutputFormat {
12    /// Colored text output for terminal
13    #[default]
14    Text,
15    /// JSON output for tooling and CI
16    Json,
17}
18
19/// Format an analysis report as colored text for terminal
20#[must_use]
21pub fn format_text(report: &AnalysisReport) -> String {
22    let mut output = String::new();
23
24    // Header
25    output.push_str(&format!(
26        "{} Analysis: {}\n",
27        report.target.cyan().bold(),
28        report.name.white().bold()
29    ));
30    output.push_str(&"═".repeat(60));
31    output.push('\n');
32
33    // Register Pressure
34    let reg_status = if report.registers.total() < 64 {
35        "[OK]".green()
36    } else if report.registers.total() < 128 {
37        "[WARN]".yellow()
38    } else {
39        "[HIGH]".red()
40    };
41
42    output.push_str(&format!(
43        "\n{}                                    {}\n",
44        "Register Pressure:".white().bold(),
45        reg_status
46    ));
47    output.push_str(&format!(
48        "  ├── .reg .f32: {} / 255 ({:.1}%)\n",
49        report.registers.f32_regs,
50        report.registers.f32_regs as f32 / 255.0 * 100.0
51    ));
52    output.push_str(&format!(
53        "  ├── .reg .b32: {} / 255 ({:.1}%)\n",
54        report.registers.b32_regs,
55        report.registers.b32_regs as f32 / 255.0 * 100.0
56    ));
57    output.push_str(&format!(
58        "  ├── .reg .b64: {} / 255 ({:.1}%)\n",
59        report.registers.b64_regs,
60        report.registers.b64_regs as f32 / 255.0 * 100.0
61    ));
62    // PTX has 8 predicate registers (p0-p7)
63    output.push_str(&format!(
64        "  ├── .reg .pred: {} / 8 ({:.1}%)\n",
65        report.registers.pred_regs,
66        report.registers.pred_regs as f32 / 8.0 * 100.0
67    ));
68    output.push_str(&format!(
69        "  └── {}: {} registers → {:.0}% occupancy possible\n",
70        "Total".bold(),
71        report.registers.total(),
72        report.estimated_occupancy * 100.0
73    ));
74
75    // Memory Access Pattern
76    let mem_status = if report.memory.coalesced_ratio >= 0.9 {
77        "[OK]".green()
78    } else if report.memory.coalesced_ratio >= 0.7 {
79        "[WARN]".yellow()
80    } else {
81        "[BAD]".red()
82    };
83
84    output.push_str(&format!(
85        "\n{}                                {}\n",
86        "Memory Access Pattern:".white().bold(),
87        mem_status
88    ));
89    output.push_str(&format!(
90        "  ├── Global loads: {} (coalesced: {:.1}%)\n",
91        report.memory.global_loads,
92        report.memory.coalesced_ratio * 100.0
93    ));
94    output.push_str(&format!(
95        "  ├── Global stores: {}\n",
96        report.memory.global_stores
97    ));
98    output.push_str(&format!(
99        "  ├── Shared loads: {}\n",
100        report.memory.shared_loads
101    ));
102    output.push_str(&format!(
103        "  └── Shared stores: {}\n",
104        report.memory.shared_stores
105    ));
106
107    // Roofline
108    output.push_str(&format!("\n{}\n", "Performance Estimate:".white().bold()));
109    output.push_str(&format!(
110        "  ├── Arithmetic Intensity: {:.2} FLOPs/Byte\n",
111        report.roofline.arithmetic_intensity
112    ));
113    output.push_str(&format!(
114        "  └── Bottleneck: {}\n",
115        if report.roofline.memory_bound {
116            "Memory bandwidth".yellow()
117        } else {
118            "Compute".green()
119        }
120    ));
121
122    // Muda Warnings
123    if report.warnings.is_empty() {
124        output.push_str(&format!("\n{} No Muda detected\n", "✓".green()));
125    } else {
126        output.push_str(&format!("\n{}\n", "Muda (Waste) Detection:".white().bold()));
127        for warning in &report.warnings {
128            let icon = match warning.muda_type {
129                MudaType::Transport => "⚠".yellow(),
130                MudaType::Waiting => "⏳".yellow(),
131                MudaType::Overprocessing => "🔄".yellow(),
132            };
133            output.push_str(&format!(
134                "  {} {}: {}\n",
135                icon,
136                muda_name(&warning.muda_type),
137                warning.description
138            ));
139            if let Some(ref suggestion) = warning.suggestion {
140                output.push_str(&format!(
141                    "     └── {}: {}\n",
142                    "Suggestion".cyan(),
143                    suggestion
144                ));
145            }
146        }
147    }
148
149    output
150}
151
152fn muda_name(muda: &MudaType) -> ColoredString {
153    match muda {
154        MudaType::Transport => "Muda of Transport (Spills)".yellow(),
155        MudaType::Waiting => "Muda of Waiting (Stalls)".yellow(),
156        MudaType::Overprocessing => "Muda of Overprocessing".yellow(),
157    }
158}
159
160/// Format an analysis report as JSON
161///
162/// # Errors
163///
164/// Returns `serde_json::Error` if serialization fails.
165pub fn format_json(report: &AnalysisReport) -> serde_json::Result<String> {
166    serde_json::to_string_pretty(report)
167}
168
169/// Write report to stdout in the specified format
170///
171/// # Errors
172///
173/// Returns `io::Error` if writing to stdout fails or JSON serialization fails.
174pub fn write_report(report: &AnalysisReport, format: OutputFormat) -> io::Result<()> {
175    let mut stdout = io::stdout().lock();
176
177    match format {
178        OutputFormat::Text => {
179            write!(stdout, "{}", format_text(report))?;
180        }
181        OutputFormat::Json => {
182            let json = format_json(report).map_err(io::Error::other)?;
183            writeln!(stdout, "{}", json)?;
184        }
185    }
186
187    Ok(())
188}
189
190#[cfg(test)]
191mod tests {
192    use super::*;
193    use crate::analyzer::{MemoryPattern, MudaWarning, RegisterUsage, RooflineMetric};
194
195    fn sample_report() -> AnalysisReport {
196        AnalysisReport {
197            name: "test_kernel".to_string(),
198            target: "PTX".to_string(),
199            registers: RegisterUsage {
200                f32_regs: 24,
201                b32_regs: 18,
202                b64_regs: 12,
203                pred_regs: 4,
204                ..Default::default()
205            },
206            memory: MemoryPattern {
207                global_loads: 100,
208                global_stores: 50,
209                coalesced_ratio: 0.95,
210                ..Default::default()
211            },
212            roofline: RooflineMetric {
213                arithmetic_intensity: 2.5,
214                theoretical_peak_gflops: 15000.0,
215                memory_bound: true,
216            },
217            warnings: vec![],
218            instruction_count: 150,
219            estimated_occupancy: 0.875,
220        }
221    }
222
223    #[test]
224    fn test_format_text_contains_kernel_name() {
225        let report = sample_report();
226        let text = format_text(&report);
227        assert!(text.contains("test_kernel"));
228    }
229
230    #[test]
231    fn test_format_text_contains_registers() {
232        let report = sample_report();
233        let text = format_text(&report);
234        assert!(text.contains("24"));
235        assert!(text.contains("f32"));
236    }
237
238    #[test]
239    fn test_format_text_contains_memory() {
240        let report = sample_report();
241        let text = format_text(&report);
242        assert!(text.contains("Global loads"));
243        assert!(text.contains("100"));
244    }
245
246    #[test]
247    fn test_format_json_valid() {
248        let report = sample_report();
249        let json = format_json(&report).unwrap();
250
251        // Verify it's valid JSON by parsing it
252        let parsed: AnalysisReport = serde_json::from_str(&json).unwrap();
253        assert_eq!(parsed.name, "test_kernel");
254    }
255
256    #[test]
257    fn test_format_text_with_warnings() {
258        let mut report = sample_report();
259        report.warnings.push(MudaWarning {
260            muda_type: MudaType::Transport,
261            description: "5 spills detected".to_string(),
262            impact: "High latency".to_string(),
263            line: None,
264            suggestion: Some("Reduce variables".to_string()),
265        });
266
267        let text = format_text(&report);
268        assert!(text.contains("Muda"));
269        assert!(text.contains("5 spills"));
270        assert!(text.contains("Suggestion"));
271    }
272
273    #[test]
274    fn test_format_text_no_warnings() {
275        let report = sample_report();
276        let text = format_text(&report);
277        assert!(text.contains("No Muda detected"));
278    }
279}