Skip to main content

stylus_trace_core/diff/
output.rs

1//! Terminal output rendering for diff reports.
2//!
3//! Provides human-readable summaries of profile comparisons
4//! with visual cues (emojis) for regressions and improvements.
5
6use super::schema::DiffReport;
7use colored::*;
8
9/// Render a human-readable summary of a diff report for the terminal
10pub fn render_terminal_diff(report: &DiffReport) -> String {
11    let mut out = String::new();
12
13    out.push_str(&render_header(report));
14    out.push_str(&render_gas_delta(report));
15    out.push_str(&render_hostio_summary(report));
16    out.push_str(&render_hostio_details(report));
17    out.push_str(&render_hot_paths(report));
18    out.push_str(&render_insights(report));
19    out.push_str(&render_status(report));
20
21    out
22}
23
24fn render_insights(report: &DiffReport) -> String {
25    let mut out = String::new();
26
27    if !report.insights.is_empty() {
28        out.push_str("\nšŸ’” ");
29        out.push_str(&"Optimization Insights:".bold().to_string());
30        out.push('\n');
31
32        for insight in &report.insights {
33            let color_desc = match insight.severity {
34                super::schema::InsightSeverity::High => insight.description.red().bold(),
35                super::schema::InsightSeverity::Medium => insight.description.yellow().bold(),
36                super::schema::InsightSeverity::Low => insight.description.cyan(),
37                super::schema::InsightSeverity::Info => insight.description.normal(),
38            };
39
40            out.push_str(&format!(
41                "  • [{}] {}\n",
42                insight.category.blue(),
43                color_desc
44            ));
45        }
46    }
47    out
48}
49
50fn render_header(report: &DiffReport) -> String {
51    let mut out = String::new();
52    out.push_str("\nšŸ“Š ");
53    out.push_str(&"Profile Comparison Summary".bold().to_string());
54    out.push_str("\n---------------------------------------------------\n");
55    out.push_str(&format!("Baseline: {}\n", report.baseline.transaction_hash));
56    out.push_str(&format!("Target:   {}\n", report.target.transaction_hash));
57    out.push_str("---------------------------------------------------\n\n");
58    out
59}
60
61fn render_gas_delta(report: &DiffReport) -> String {
62    let gas_delta = &report.deltas.gas;
63    let symbol = get_delta_symbol(gas_delta.absolute_change);
64    format!(
65        "{} Total Gas: {} -> {} ({:+.2}%)\n",
66        symbol, gas_delta.baseline, gas_delta.target, gas_delta.percent_change
67    )
68}
69
70fn render_hostio_summary(report: &DiffReport) -> String {
71    let hostio_delta = &report.deltas.hostio;
72    let symbol = get_delta_symbol(hostio_delta.total_calls_change);
73    format!(
74        "{} HostIO Calls: {} -> {} ({:+.2}%)\n",
75        symbol,
76        hostio_delta.baseline_total_calls,
77        hostio_delta.target_total_calls,
78        hostio_delta.total_calls_percent_change
79    )
80}
81
82fn render_hostio_details(report: &DiffReport) -> String {
83    let mut out = String::new();
84    let hostio_delta = &report.deltas.hostio;
85
86    if !hostio_delta.by_type_changes.is_empty() {
87        out.push_str("\nTop HostIO Changes:\n");
88        let mut changes: Vec<_> = hostio_delta.by_type_changes.iter().collect();
89        changes.sort_by(|a, b| b.1.delta.abs().cmp(&a.1.delta.abs()));
90
91        for (hostio_type, change) in changes.iter().take(5) {
92            let symbol = if change.delta > 0 { "šŸ“ˆ" } else { "šŸ“‰" };
93            out.push_str(&format!(
94                "  {} {}: {} -> {} ({:+})\n",
95                symbol, hostio_type, change.baseline, change.target, change.delta
96            ));
97        }
98    }
99    out
100}
101
102fn render_hot_paths(report: &DiffReport) -> String {
103    let mut out = String::new();
104    let hot_paths = &report.deltas.hot_paths;
105
106    if !hot_paths.common_paths.is_empty() {
107        out.push_str(&render_hot_path_comparison_table(report));
108    }
109    out
110}
111
112fn render_hot_path_comparison_table(report: &DiffReport) -> String {
113    let mut out = String::new();
114    let hot_paths = &report.deltas.hot_paths;
115
116    out.push_str("\n  šŸš€ HOT PATH COMPARISON\n");
117    out.push_str(
118        "  ā”ā”ā”ā”ā”ā”ā”ā”ā”ā”ā”ā”ā”ā”ā”ā”ā”ā”ā”ā”ā”ā”ā”ā”ā”ā”ā”ā”ā”ā”ā”ā”ā”ā”ā”ā”ā”ā”ā”ā”ā”ā”³ā”ā”ā”ā”ā”ā”ā”ā”ā”ā”ā”ā”ā”ā”ā”³ā”ā”ā”ā”ā”ā”ā”ā”ā”ā”ā”ā”ā”ā”ā”³ā”ā”ā”ā”ā”ā”ā”ā”ā”ā”ā”ā”ā”“\n",
119    );
120    out.push_str(&format!(
121        "  ā”ƒ {:<38} ā”ƒ {:^12} ā”ƒ {:^12} ā”ƒ {:^10} ā”ƒ\n",
122        "Execution Stack (Common Changes)", "BASELINE", "TARGET", "DELTA"
123    ));
124    out.push_str(
125        "  ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╋━━━━━━━━━━━━━━╋━━━━━━━━━━━━━━╋━━━━━━━━━━━━┫\n",
126    );
127
128    let mut hp_changes = hot_paths.common_paths.clone();
129    hp_changes.sort_by(|a, b| b.gas_change.abs().cmp(&a.gas_change.abs()));
130
131    for hp in hp_changes.iter().take(10) {
132        let delta_color = if hp.gas_change > 0 {
133            "\x1b[31;1m" // Bold Red
134        } else if hp.gas_change < 0 {
135            "\x1b[32;1m" // Bold Green
136        } else {
137            "\x1b[0m" // Reset
138        };
139        let reset = "\x1b[0m";
140
141        let display_stack = shorten_stack(&hp.stack);
142        let display_stack_fixed = if display_stack.len() > 38 {
143            format!("...{}", &display_stack[display_stack.len() - 35..])
144        } else {
145            format!("{:<38}", display_stack)
146        };
147
148        // Scale to Gas (ink / 10,000) with float precision
149        let baseline_gas = hp.baseline_gas as f64 / 10_000.0;
150        let target_gas = hp.target_gas as f64 / 10_000.0;
151
152        out.push_str(&format!(
153            "  ā”ƒ {} ā”ƒ {:>12.1} ā”ƒ {:>12.1} ā”ƒ {}{:>9.2}%{} ā”ƒ\n",
154            display_stack_fixed, baseline_gas, target_gas, delta_color, hp.percent_change, reset
155        ));
156    }
157
158    out.push_str(
159        "  ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┻━━━━━━━━━━━━━━┻━━━━━━━━━━━━━━┻━━━━━━━━━━━━┛\n",
160    );
161
162    out
163}
164
165fn render_status(report: &DiffReport) -> String {
166    let mut out = String::new();
167    out.push_str("\n---------------------------------------------------\n");
168    let status_msg = match report.summary.status.as_str() {
169        "FAILED" => format!(
170            "āŒ STATUS: REGRESSION DETECTED ({} violations)",
171            report.summary.violation_count
172        )
173        .red()
174        .bold(),
175        "WARNING" => format!(
176            "āš ļø  STATUS: WARNING ({} violations)",
177            report.summary.violation_count
178        )
179        .yellow()
180        .bold(),
181        _ => "āœ… STATUS: PASSED".green().bold(),
182    };
183    out.push_str(&status_msg.to_string());
184    out.push('\n');
185    out
186}
187
188fn get_delta_symbol(change: i64) -> &'static str {
189    if change > 0 {
190        "šŸ“ˆ"
191    } else if change < 0 {
192        "šŸ“‰"
193    } else {
194        "āž”ļø"
195    }
196}
197
198fn shorten_stack(stack: &str) -> String {
199    let parts: Vec<&str> = stack.split(';').collect();
200    if parts.len() <= 2 {
201        stack.to_string()
202    } else {
203        format!("...;{};{}", parts[parts.len() - 2], parts[parts.len() - 1])
204    }
205}