ferric_ai/commands/stats/
mod.rs

1use 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
8// Module declarations
9pub 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/// Stats subcommands
45#[derive(Debug, Clone, clap::Subcommand)]
46pub enum StatsCommands {
47    /// Show basic flamegraph inventory
48    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    /// Identify CPU hotspots and concentration patterns
55    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    /// Assess performance health and get optimization recommendations
62    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    /// Analyze statistical distribution patterns
69    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    /// Complete analysis (all metrics)
76    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
84/// Handler for the stats command with subcommands
85pub struct StatsHandler;
86
87impl CommandHandler for StatsHandler {
88    fn execute(&self, cli: &Cli) -> Result<()> {
89        // Extract subcommand from CLI
90        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        // Parse trees with restriction to single files only
99        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        // Route to appropriate subcommand handler
108        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    // Override default parse_trees to restrict to single files only
153    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    /// Parse trees with restriction to single files only, returning paths as well
169    fn parse_trees_with_paths(&self, cli: &Cli) -> Result<(Tree, Option<Tree>, PathBuf, Option<PathBuf>)> {
170        // Check primary input - resolve glob and ensure single file
171        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        // Handle comparison input if provided
182        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    /// Parse trees with restriction to single files only (legacy method for parse_trees override)
201    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
207// Long about text for the stats command
208pub 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.";