ferric_ai/commands/stats/
mod.rs1use crate::cli::Cli;
2use crate::error::Result;
3use crate::handler::CommandHandler;
4use crate::helpers::{parse_single_svg, resolve_path_to_files};
5use crate::parser::Tree;
6use std::path::PathBuf;
7
8pub mod calculations {
10 pub mod basic;
11 pub mod concentration;
12 pub mod tiers;
13 pub mod statistics;
14 pub mod percentiles;
15 pub mod health;
16 pub mod distribution;
17 pub mod validation;
18}
19
20pub mod core {
21 pub mod data_structures;
22}
23
24pub mod display {
25 pub mod formatting;
26}
27
28pub mod subcommands {
29 pub mod summary;
30 pub mod hotspots;
31 pub mod diagnosis;
32 pub mod distribution;
33 pub mod comprehensive;
34}
35
36use subcommands::{
37 summary::SummaryHandler,
38 hotspots::HotspotsHandler,
39 diagnosis::DiagnosisHandler,
40 distribution::DistributionHandler,
41 comprehensive::ComprehensiveHandler,
42};
43
44#[derive(Debug, Clone, clap::Subcommand)]
46pub enum StatsCommands {
47 Summary {
49 #[arg(long, help = "Show comparison analysis instead of individual stats")]
50 comparison: bool,
51 #[arg(long, help = "Output results in JSON format")]
52 json: bool,
53 },
54 Hotspots {
56 #[arg(long, help = "Show comparison analysis instead of individual stats")]
57 comparison: bool,
58 #[arg(long, help = "Output results in JSON format")]
59 json: bool,
60 },
61 Diagnosis {
63 #[arg(long, help = "Show comparison analysis instead of individual stats")]
64 comparison: bool,
65 #[arg(long, help = "Output results in JSON format")]
66 json: bool,
67 },
68 Distribution {
70 #[arg(long, help = "Show comparison analysis instead of individual stats")]
71 comparison: bool,
72 #[arg(long, help = "Output results in JSON format")]
73 json: bool,
74 },
75 Comprehensive {
77 #[arg(long, help = "Show comparison analysis instead of individual stats")]
78 comparison: bool,
79 #[arg(long, help = "Output results in JSON format")]
80 json: bool,
81 },
82}
83
84pub struct StatsHandler;
86
87impl CommandHandler for StatsHandler {
88 fn execute(&self, cli: &Cli) -> Result<()> {
89 let stats_command = if let crate::cli::Commands::Stats(ref subcommand) = &cli.command {
91 subcommand
92 } else {
93 return Err(crate::error::FerricError::CommandError(
94 "Stats command expected but not found".to_string()
95 ));
96 };
97
98 let (primary_tree, comparison_tree, primary_path, comparison_path) = self.parse_trees_with_paths(cli)?;
100
101 let primary_filename = primary_path.file_name().unwrap_or_default().to_string_lossy();
102 let comparison_filename = comparison_path.as_ref()
103 .and_then(|p| p.file_name())
104 .map(|n| n.to_string_lossy())
105 .unwrap_or_default();
106
107 match stats_command {
109 StatsCommands::Summary { comparison: _, json } => {
110 let handler = SummaryHandler;
111 if let Some(comp_tree) = comparison_tree {
112 handler.execute_comparison(&primary_tree, &comp_tree, &primary_filename, &comparison_filename, *json)
113 } else {
114 handler.execute_single(&primary_tree, &primary_filename, *json)
115 }
116 },
117 StatsCommands::Hotspots { comparison: _, json } => {
118 let handler = HotspotsHandler;
119 if let Some(comp_tree) = comparison_tree {
120 handler.execute_comparison(&primary_tree, &comp_tree, &primary_filename, &comparison_filename, *json)
121 } else {
122 handler.execute_single(&primary_tree, &primary_filename, *json)
123 }
124 },
125 StatsCommands::Diagnosis { comparison: _, json } => {
126 let handler = DiagnosisHandler;
127 if let Some(comp_tree) = comparison_tree {
128 handler.execute_comparison(&primary_tree, &comp_tree, &primary_filename, &comparison_filename, *json)
129 } else {
130 handler.execute_single(&primary_tree, &primary_filename, *json)
131 }
132 },
133 StatsCommands::Distribution { comparison: _, json } => {
134 let handler = DistributionHandler;
135 if let Some(comp_tree) = comparison_tree {
136 handler.execute_comparison(&primary_tree, &comp_tree, &primary_filename, &comparison_filename, *json)
137 } else {
138 handler.execute_single(&primary_tree, &primary_filename, *json)
139 }
140 },
141 StatsCommands::Comprehensive { comparison: _, json } => {
142 let handler = ComprehensiveHandler;
143 if let Some(comp_tree) = comparison_tree {
144 handler.execute_comparison(&primary_tree, &comp_tree, &primary_filename, &comparison_filename, *json)
145 } else {
146 handler.execute_single(&primary_tree, &primary_filename, *json)
147 }
148 },
149 }
150 }
151
152 fn parse_trees(&self, cli: &Cli) -> Result<(Vec<Tree>, Vec<Tree>)> {
154 let (primary_tree, comparison_tree) = self.parse_trees_single_file(cli)?;
155
156 let primary_vec = vec![primary_tree];
157 let comparison_vec = if let Some(tree) = comparison_tree {
158 vec![tree]
159 } else {
160 Vec::new()
161 };
162
163 Ok((primary_vec, comparison_vec))
164 }
165}
166
167impl StatsHandler {
168 fn parse_trees_with_paths(&self, cli: &Cli) -> Result<(Tree, Option<Tree>, PathBuf, Option<PathBuf>)> {
170 let primary_files = resolve_path_to_files(&cli.flamegraph)?;
172 if primary_files.len() != 1 {
173 return Err(crate::error::FerricError::CommandError(
174 format!("Stats command requires exactly one flamegraph file, but found {} files. Please specify a single .svg file or a glob pattern that matches exactly one file.", primary_files.len())
175 ));
176 }
177
178 let primary_path = primary_files.into_iter().next().unwrap();
179 let primary_tree = parse_single_svg(&primary_path)?;
180
181 let (comparison_tree, comparison_path) = if let Some(ref compare_path) = cli.compare {
183 let comparison_files = resolve_path_to_files(compare_path)?;
184 if comparison_files.len() != 1 {
185 return Err(crate::error::FerricError::CommandError(
186 format!("Stats comparison requires exactly one comparison file, but found {} files. Please specify a single .svg file.", comparison_files.len())
187 ));
188 }
189
190 let comp_path = comparison_files.into_iter().next().unwrap();
191 let comp_tree = parse_single_svg(&comp_path)?;
192 (Some(comp_tree), Some(comp_path))
193 } else {
194 (None, None)
195 };
196
197 Ok((primary_tree, comparison_tree, primary_path, comparison_path))
198 }
199
200 fn parse_trees_single_file(&self, cli: &Cli) -> Result<(Tree, Option<Tree>)> {
202 let (primary_tree, comparison_tree, _, _) = self.parse_trees_with_paths(cli)?;
203 Ok((primary_tree, comparison_tree))
204 }
205}
206
207pub const STATS_LONG_ABOUT: &str = "Analyze flamegraph statistics and performance characteristics to provide context for threshold configuration. Generates CPU percentage distributions, function counts, recursion depth analysis, and suggested thresholds for hotspot detection. Designed to help LLMs and users configure appropriate analysis parameters based on the specific performance profile of the application. Output includes both human-readable summaries and structured JSON data for automated threshold setting.
209
210Available subcommands:
211 summary Basic flamegraph inventory and function counts
212 hotspots CPU concentration analysis and performance tiers
213 diagnosis Health assessment and optimization recommendations
214 distribution Statistical distribution patterns and variability analysis
215 comprehensive Complete analysis combining all metrics
216
217Use --comparison flag with --compare to show comparison analysis between two flamegraphs.";