1use crate::cli::Cli;
4use crate::error::Result;
5use crate::parser::{FlamegraphParser, Tree};
6use std::fs;
7use std::path::{Path, PathBuf};
8use glob::glob;
9
10pub fn parse_trees_no_comparison(cli: &Cli) -> Result<Vec<Tree>> {
13 let mut trees = Vec::new();
14
15 let svg_files = resolve_path_to_files(&cli.flamegraph)?;
17 for file in svg_files {
18 let tree = parse_single_svg(&file)?;
19 trees.push(tree);
20 }
21
22 Ok(trees)
23}
24
25pub fn parse_single_svg(path: &std::path::Path) -> Result<Tree> {
27 let svg_content = fs::read_to_string(path)
29 .map_err(|e| crate::error::FerricError::ParserError(
30 format!("Failed to read flamegraph file '{}': {}", path.display(), e)
31 ))?;
32
33 let mut parser = FlamegraphParser::default();
34 let parser_nodes = parser.parse_svg(&svg_content)?;
35 let _total_samples = parser.get_total_samples(); let tree = Tree::from_parser_nodes(parser_nodes, 0); Ok(tree)
39}
40
41pub fn find_svg_files(path: &std::path::Path) -> Result<Vec<std::path::PathBuf>> {
43 let mut svg_files = Vec::new();
44
45 if !path.is_dir() {
46 return Ok(svg_files);
47 }
48
49 let entries = fs::read_dir(path)
50 .map_err(|e| crate::error::FerricError::InvalidInput(
51 format!("Cannot read directory {}: {}", path.display(), e)
52 ))?;
53
54 for entry in entries {
55 let entry = entry.map_err(|e| crate::error::FerricError::InvalidInput(
56 format!("Error reading directory entry: {}", e)
57 ))?;
58
59 let path = entry.path();
60 if path.is_file() && path.extension().and_then(|ext| ext.to_str()) == Some("svg") {
61 svg_files.push(path);
62 }
63 }
64
65 svg_files.sort();
66 Ok(svg_files)
67}
68
69pub fn resolve_path_to_files(path: &Path) -> Result<Vec<PathBuf>> {
76 let path_str = path.to_string_lossy();
77
78 if path_str.contains('*') || path_str.contains('?') || path_str.contains('[') {
80 return resolve_glob_pattern(&path_str);
81 }
82
83 if path.is_dir() {
85 find_svg_files(path)
86 } else if path.is_file() {
87 if path.extension().and_then(|ext| ext.to_str()) == Some("svg") {
89 Ok(vec![path.to_path_buf()])
90 } else {
91 Err(crate::error::FerricError::InvalidInput(
92 format!("File '{}' is not an SVG file", path.display())
93 ))
94 }
95 } else {
96 Err(crate::error::FerricError::InvalidInput(
97 format!("Path '{}' does not exist", path.display())
98 ))
99 }
100}
101
102fn resolve_glob_pattern(pattern: &str) -> Result<Vec<PathBuf>> {
104 let mut svg_files = Vec::new();
105
106 let glob_results = glob(pattern)
107 .map_err(|e| crate::error::FerricError::InvalidInput(
108 format!("Invalid glob pattern '{}': {}", pattern, e)
109 ))?;
110
111 for entry in glob_results {
112 match entry {
113 Ok(path) => {
114 if path.is_file() && path.extension().and_then(|ext| ext.to_str()) == Some("svg") {
115 svg_files.push(path);
116 }
117 }
118 Err(e) => {
119 return Err(crate::error::FerricError::InvalidInput(
120 format!("Error reading glob pattern '{}': {}", pattern, e)
121 ));
122 }
123 }
124 }
125
126 if svg_files.is_empty() {
127 return Err(crate::error::FerricError::InvalidInput(
128 format!("No SVG files found matching pattern '{}'", pattern)
129 ));
130 }
131
132 svg_files.sort();
133 Ok(svg_files)
134}
135
136pub fn format_duration(duration: std::time::Duration) -> String {
140 let total_seconds = duration.as_secs();
141
142 if total_seconds < 60 {
143 format!("{} seconds", total_seconds)
144 } else if total_seconds < 3600 {
145 let minutes = total_seconds / 60;
146 let seconds = total_seconds % 60;
147 if seconds == 0 {
148 format!("{} minutes", minutes)
149 } else {
150 format!("{} minutes {} seconds", minutes, seconds)
151 }
152 } else if total_seconds < 86400 {
153 let hours = total_seconds / 3600;
154 let minutes = (total_seconds % 3600) / 60;
155 if minutes == 0 {
156 format!("{} hours", hours)
157 } else {
158 format!("{} hours {} minutes", hours, minutes)
159 }
160 } else if total_seconds < 604800 {
161 let days = total_seconds / 86400;
162 let hours = (total_seconds % 86400) / 3600;
163 if hours == 0 {
164 format!("{} days", days)
165 } else {
166 format!("{} days {} hours", days, hours)
167 }
168 } else if total_seconds < 31536000 {
169 let weeks = total_seconds / 604800;
170 let days = (total_seconds % 604800) / 86400;
171 if days == 0 {
172 format!("{} weeks", weeks)
173 } else {
174 format!("{} weeks {} days", weeks, days)
175 }
176 } else {
177 let years = total_seconds / 31536000;
178 let weeks = (total_seconds % 31536000) / 604800;
179 if weeks == 0 {
180 format!("{} years", years)
181 } else {
182 format!("{} years {} weeks", years, weeks)
183 }
184 }
185}
186
187pub fn get_magnitude(percent_change: f64) -> &'static str {
189 if percent_change < 5.0 {
190 "SLIGHT"
191 } else if percent_change < 15.0 {
192 "MODERATE"
193 } else if percent_change < 50.0 {
194 "SIGNIFICANT"
195 } else {
196 "MAJOR"
197 }
198}
199
200pub fn get_percentile(sorted_values: &[f64], percentile: f64) -> f64 {
202 if sorted_values.is_empty() {
203 return 0.0;
204 }
205
206 let index = ((sorted_values.len() as f64 - 1.0) * percentile) as usize;
207 sorted_values.get(index).copied().unwrap_or(0.0)
208}
209
210pub fn print_compare_metric<T>(name: &str, primary_val: T, comparison_val: T, unit: &str, higher_is_worse: bool)
212where
213 T: Copy + std::fmt::Display + std::ops::Sub<Output = T> + PartialOrd + Into<f64>
214{
215 let primary_f64: f64 = primary_val.into();
216 let comparison_f64: f64 = comparison_val.into();
217
218 let diff = comparison_f64 - primary_f64;
219 let abs_diff = diff.abs();
220 let percent_change = if primary_f64 != 0.0 { (diff / primary_f64) * 100.0 } else { 0.0 };
221
222 let normalized_percent_change = if percent_change.abs() < 0.05 { 0.0 } else { percent_change };
224
225 let (indicator, magnitude, color) = if abs_diff < 0.01 {
226 ("≈", "SAME", "🟡")
227 } else if diff > 0.0 {
228 if higher_is_worse {
229 ("↗", get_magnitude(percent_change.abs()), "🔴") } else {
231 ("↗", get_magnitude(percent_change.abs()), "🟢") }
233 } else if higher_is_worse {
234 ("↘", get_magnitude(percent_change.abs()), "🟢") } else {
236 ("↘", get_magnitude(percent_change.abs()), "🔴") };
238
239 println!("├─ {}: {} {} vs {} {} {} {} ({:+.1}% change) {}",
240 name, primary_val, unit, comparison_val, unit, indicator, magnitude, normalized_percent_change, color);
241}
242
243pub fn print_compare_classification(name: &str, primary_val: &str, comparison_val: &str) {
245 if primary_val == comparison_val {
246 println!("├─ {}: {} ≈ {} (SAME) 🟡", name, primary_val, comparison_val);
247 } else {
248 println!("├─ {}: {} ➜ {} (CHANGED) 🔄", name, primary_val, comparison_val);
249 }
250}
251
252pub fn aggregate_cpu_by_function(cpu_data: &[(String, f64)]) -> Vec<(String, f64)> {
255 use std::collections::HashMap;
256
257 let mut function_totals: HashMap<String, f64> = HashMap::new();
258 for (function_name, cpu_percent) in cpu_data {
259 *function_totals.entry(function_name.clone()).or_insert(0.0) += cpu_percent;
260 }
261
262 let mut sorted_functions: Vec<(String, f64)> = function_totals.into_iter().collect();
263 sorted_functions.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap());
264
265 sorted_functions
266}