stylus_trace_core/diff/
output.rs1use super::schema::DiffReport;
7use colored::*;
8
9pub 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" } else if hp.gas_change < 0 {
135 "\x1b[32;1m" } else {
137 "\x1b[0m" };
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 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}