Skip to main content

cargo_mate/tools/
bloat_check.rs

1use super::{Tool, ToolError, Result, OutputFormat, parse_output_format};
2use clap::{Arg, ArgMatches, Command};
3use std::path::Path;
4use std::process::Command as ProcessCommand;
5use std::collections::HashMap;
6use colored::*;
7use serde::{Serialize, Deserialize};
8#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct BinarySizeInfo {
10    pub path: String,
11    pub total_size: u64,
12    pub text_size: u64,
13    pub data_size: u64,
14    pub bss_size: u64,
15    pub symbol_count: usize,
16}
17#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct SizeComparison {
19    pub current: BinarySizeInfo,
20    pub baseline: BinarySizeInfo,
21    pub size_diff: i64,
22    pub text_diff: i64,
23    pub data_diff: i64,
24    pub bss_diff: i64,
25}
26#[derive(Debug, Clone, Serialize, Deserialize)]
27pub struct SymbolSize {
28    pub name: String,
29    pub size: u64,
30    pub symbol_type: String,
31}
32#[derive(Debug, Clone, Serialize, Deserialize)]
33pub struct OptimizationSuggestion {
34    pub category: String,
35    pub description: String,
36    pub impact: String,
37    pub suggestion: String,
38}
39#[derive(Debug, Clone, Serialize, Deserialize)]
40pub struct BuildComparison {
41    pub debug_size: u64,
42    pub release_size: u64,
43    pub ratio: f64,
44    pub savings: u64,
45}
46pub struct BloatCheckTool;
47impl BloatCheckTool {
48    pub fn new() -> Self {
49        Self
50    }
51    fn analyze_binary_size(&self, binary_path: &str) -> Result<BinarySizeInfo> {
52        if !Path::new(binary_path).exists() {
53            return Err(
54                ToolError::InvalidArguments(format!("Binary not found: {}", binary_path)),
55            );
56        }
57        let metadata = std::fs::metadata(binary_path)?;
58        let total_size = metadata.len();
59        let size_output = match ProcessCommand::new("size")
60            .arg("-A")
61            .arg("-d")
62            .arg(binary_path)
63            .output()
64        {
65            Ok(output) if output.status.success() => {
66                String::from_utf8_lossy(&output.stdout).to_string()
67            }
68            _ => {
69                match ProcessCommand::new("size").arg("-B").arg(binary_path).output() {
70                    Ok(output) if output.status.success() => {
71                        String::from_utf8_lossy(&output.stdout).to_string()
72                    }
73                    _ => format!("{} {} {} {}", total_size, 0, 0, 0),
74                }
75            }
76        };
77        let mut text_size = 0u64;
78        let mut data_size = 0u64;
79        let mut bss_size = 0u64;
80        for line in size_output.lines() {
81            let parts: Vec<&str> = line.split_whitespace().collect();
82            if parts.len() >= 4 {
83                if let (Ok(t), Ok(d), Ok(b)) = (
84                    parts[0].parse::<u64>(),
85                    parts[1].parse::<u64>(),
86                    parts[2].parse::<u64>(),
87                ) {
88                    text_size = t;
89                    data_size = d;
90                    bss_size = b;
91                    break;
92                }
93            }
94        }
95        let symbol_count = match ProcessCommand::new("nm")
96            .arg("-C")
97            .arg("--print-size")
98            .arg("--size-sort")
99            .arg("-t")
100            .arg("d")
101            .arg(binary_path)
102            .output()
103        {
104            Ok(output) if output.status.success() => {
105                String::from_utf8_lossy(&output.stdout)
106                    .lines()
107                    .filter(|line| !line.trim().is_empty())
108                    .count()
109            }
110            _ => 0,
111        };
112        Ok(BinarySizeInfo {
113            path: binary_path.to_string(),
114            total_size,
115            text_size,
116            data_size,
117            bss_size,
118            symbol_count,
119        })
120    }
121    fn analyze_size_changes(
122        &self,
123        current_path: &str,
124        baseline_path: &str,
125    ) -> Result<SizeComparison> {
126        let current = self.analyze_binary_size(current_path)?;
127        let baseline = self.analyze_binary_size(baseline_path)?;
128        let size_diff = current.total_size as i64 - baseline.total_size as i64;
129        let text_diff = current.text_size as i64 - baseline.text_size as i64;
130        let data_diff = current.data_size as i64 - baseline.data_size as i64;
131        let bss_diff = current.bss_size as i64 - baseline.bss_size as i64;
132        Ok(SizeComparison {
133            current,
134            baseline,
135            size_diff,
136            text_diff,
137            data_diff,
138            bss_diff,
139        })
140    }
141    fn find_largest_symbols(&self, binary_path: &str) -> Result<Vec<SymbolSize>> {
142        let output = ProcessCommand::new("nm")
143            .arg("-C")
144            .arg("--print-size")
145            .arg("--size-sort")
146            .arg("-r")
147            .arg("-t")
148            .arg("d")
149            .arg(binary_path)
150            .output()
151            .map_err(|e| ToolError::ExecutionFailed(
152                format!("nm command failed: {}", e),
153            ))?;
154        if !output.status.success() {
155            return Err(
156                ToolError::ExecutionFailed(
157                    String::from_utf8_lossy(&output.stderr).to_string(),
158                ),
159            );
160        }
161        let mut symbols = Vec::new();
162        for line in String::from_utf8_lossy(&output.stdout).lines() {
163            let parts: Vec<&str> = line.split_whitespace().collect();
164            if parts.len() >= 3 {
165                if let Ok(size) = parts[0].parse::<u64>() {
166                    let symbol_type = parts[1].to_string();
167                    let name = parts[2..].join(" ");
168                    symbols
169                        .push(SymbolSize {
170                            name,
171                            size,
172                            symbol_type,
173                        });
174                    if symbols.len() >= 20 {
175                        break;
176                    }
177                }
178            }
179        }
180        Ok(symbols)
181    }
182    fn generate_optimization_suggestions(
183        &self,
184        analysis: &BinarySizeInfo,
185    ) -> Vec<OptimizationSuggestion> {
186        let mut suggestions = Vec::new();
187        if analysis.total_size > 50 * 1024 * 1024 {
188            suggestions
189                .push(OptimizationSuggestion {
190                    category: "Binary Size".to_string(),
191                    description: "Large binary detected".to_string(),
192                    impact: "High".to_string(),
193                    suggestion: "Consider enabling link-time optimization (LTO) in release builds"
194                        .to_string(),
195                });
196        }
197        if analysis.text_size > 20 * 1024 * 1024 {
198            suggestions
199                .push(OptimizationSuggestion {
200                    category: "Code Size".to_string(),
201                    description: "Large text section".to_string(),
202                    impact: "Medium".to_string(),
203                    suggestion: "Review inlining decisions and consider #[inline(never)] for large functions"
204                        .to_string(),
205                });
206        }
207        if analysis.data_size > 10 * 1024 * 1024 {
208            suggestions
209                .push(OptimizationSuggestion {
210                    category: "Data Size".to_string(),
211                    description: "Large data section".to_string(),
212                    impact: "Medium".to_string(),
213                    suggestion: "Review static data usage and consider lazy initialization"
214                        .to_string(),
215                });
216        }
217        if analysis.bss_size > 5 * 1024 * 1024 {
218            suggestions
219                .push(OptimizationSuggestion {
220                    category: "Memory Usage".to_string(),
221                    description: "Large uninitialized data section".to_string(),
222                    impact: "Low".to_string(),
223                    suggestion: "Review large static arrays and consider dynamic allocation"
224                        .to_string(),
225                });
226        }
227        suggestions
228            .push(OptimizationSuggestion {
229                category: "Build Optimization".to_string(),
230                description: "General size optimizations".to_string(),
231                impact: "Low".to_string(),
232                suggestion: "Use cargo build --release with strip = true in Cargo.toml"
233                    .to_string(),
234            });
235        suggestions
236            .push(OptimizationSuggestion {
237                category: "Dependency Analysis".to_string(),
238                description: "Check for unused dependencies".to_string(),
239                impact: "Medium".to_string(),
240                suggestion: "Run cargo-udeps to find unused dependencies".to_string(),
241            });
242        suggestions
243    }
244    fn analyze_debug_vs_release(
245        &self,
246        debug_path: &str,
247        release_path: &str,
248    ) -> Result<BuildComparison> {
249        let debug_info = self.analyze_binary_size(debug_path)?;
250        let release_info = self.analyze_binary_size(release_path)?;
251        let debug_size = debug_info.total_size;
252        let release_size = release_info.total_size;
253        let ratio = if release_size > 0 {
254            debug_size as f64 / release_size as f64
255        } else {
256            1.0
257        };
258        let savings = debug_size.saturating_sub(release_size);
259        Ok(BuildComparison {
260            debug_size,
261            release_size,
262            ratio,
263            savings,
264        })
265    }
266    fn format_size(&self, bytes: u64) -> String {
267        const UNITS: &[&str] = &["B", "KB", "MB", "GB"];
268        let mut size = bytes as f64;
269        let mut unit_index = 0;
270        while size >= 1024.0 && unit_index < UNITS.len() - 1 {
271            size /= 1024.0;
272            unit_index += 1;
273        }
274        format!("{:.1} {}", size, UNITS[unit_index])
275    }
276    fn format_diff(&self, diff: i64) -> String {
277        if diff == 0 {
278            "±0 B".to_string()
279        } else if diff > 0 {
280            format!("+{}", self.format_size(diff as u64))
281        } else {
282            format!("-{}", self.format_size((- diff) as u64))
283        }
284    }
285    fn colorize_diff(&self, diff: i64, threshold: f64) -> ColoredString {
286        let abs_diff = diff.abs() as f64;
287        let color = if abs_diff > threshold as f64 {
288            diff.to_string().red()
289        } else if abs_diff > threshold * 0.7 {
290            diff.to_string().yellow()
291        } else {
292            diff.to_string().green()
293        };
294        color
295    }
296}
297impl Tool for BloatCheckTool {
298    fn name(&self) -> &'static str {
299        "bloat-check"
300    }
301    fn description(&self) -> &'static str {
302        "Analyze binary size and suggest optimizations"
303    }
304    fn command(&self) -> Command {
305        Command::new(self.name())
306            .about(self.description())
307            .long_about(
308                "Analyze binary size and suggest optimizations.\n\
309                 \n\
310                 This tool helps you understand what's contributing to your binary size:\n\
311                 • Track size changes between builds\n\
312                 • Identify largest functions and data structures\n\
313                 • Compare debug vs release builds\n\
314                 • Generate optimization recommendations\n\
315                 \n\
316                 EXAMPLES:\n\
317                 cm tool bloat-check --binary target/release/myapp --symbols\n\
318                 cm tool bloat-check --binary target/release/myapp --baseline old-build/myapp\n\
319                 cm tool bloat-check --debug-compare --optimize",
320            )
321            .args(
322                &[
323                    Arg::new("binary")
324                        .long("binary")
325                        .short('b')
326                        .help("Path to binary to analyze")
327                        .default_value("target/release/cargo-mate"),
328                    Arg::new("baseline")
329                        .long("baseline")
330                        .help("Path to baseline binary for comparison"),
331                    Arg::new("threshold")
332                        .long("threshold")
333                        .short('t')
334                        .help("Size change threshold percentage")
335                        .default_value("5.0"),
336                    Arg::new("symbols")
337                        .long("symbols")
338                        .short('s')
339                        .help("Show largest symbols")
340                        .action(clap::ArgAction::SetTrue),
341                    Arg::new("debug-compare")
342                        .long("debug-compare")
343                        .help("Compare debug vs release builds")
344                        .action(clap::ArgAction::SetTrue),
345                    Arg::new("optimize")
346                        .long("optimize")
347                        .short('o')
348                        .help("Generate optimization suggestions")
349                        .action(clap::ArgAction::SetTrue),
350                    Arg::new("report")
351                        .long("report")
352                        .help("Generate detailed size report")
353                        .action(clap::ArgAction::SetTrue),
354                ],
355            )
356            .args(&super::common_options())
357    }
358    fn execute(&self, matches: &ArgMatches) -> Result<()> {
359        let binary_path = matches.get_one::<String>("binary").unwrap();
360        let baseline_path = matches.get_one::<String>("baseline");
361        let threshold = matches
362            .get_one::<String>("threshold")
363            .unwrap()
364            .parse::<f64>()
365            .unwrap_or(5.0);
366        let show_symbols = matches.get_flag("symbols");
367        let debug_compare = matches.get_flag("debug-compare");
368        let optimize = matches.get_flag("optimize");
369        let report = matches.get_flag("report");
370        let verbose = matches.get_flag("verbose");
371        let dry_run = matches.get_flag("dry-run");
372        let output_format = parse_output_format(matches);
373        if dry_run {
374            println!("🔍 Would analyze binary: {}", binary_path);
375            return Ok(());
376        }
377        match output_format {
378            OutputFormat::Human => {
379                println!(
380                    "📊 {} - {}", "Binary Size Analysis".bold(), self.description()
381                    .cyan()
382                );
383                match self.analyze_binary_size(binary_path) {
384                    Ok(analysis) => {
385                        println!("\n📁 Binary: {}", analysis.path.bold());
386                        println!(
387                            "📏 Size: {}", self.format_size(analysis.total_size)
388                            .green().bold()
389                        );
390                        println!(
391                            "🔢 Symbols: {}", analysis.symbol_count.to_string().cyan()
392                        );
393                        if analysis.text_size > 0 || analysis.data_size > 0
394                            || analysis.bss_size > 0
395                        {
396                            println!("\n📈 Section Sizes:");
397                            if analysis.text_size > 0 {
398                                println!(
399                                    "  Text (code): {}", self.format_size(analysis.text_size)
400                                );
401                            }
402                            if analysis.data_size > 0 {
403                                println!(
404                                    "  Data (initialized): {}", self.format_size(analysis
405                                    .data_size)
406                                );
407                            }
408                            if analysis.bss_size > 0 {
409                                println!(
410                                    "  BSS (uninitialized): {}", self.format_size(analysis
411                                    .bss_size)
412                                );
413                            }
414                        }
415                        if let Some(baseline) = baseline_path {
416                            match self.analyze_size_changes(binary_path, baseline) {
417                                Ok(comparison) => {
418                                    println!("\n📊 Size Changes (compared to {}):", baseline);
419                                    println!(
420                                        "  Total size: {} ({:.1}%)", self.colorize_diff(comparison
421                                        .size_diff, threshold * analysis.total_size as f64 / 100.0),
422                                        (comparison.size_diff as f64 / comparison.baseline
423                                        .total_size as f64 * 100.0)
424                                    );
425                                    if comparison.text_diff != 0 {
426                                        println!(
427                                            "  Text section: {} ({:.1}%)", self.colorize_diff(comparison
428                                            .text_diff, threshold * analysis.text_size as f64 / 100.0),
429                                            (comparison.text_diff as f64 / comparison.baseline.text_size
430                                            as f64 * 100.0)
431                                        );
432                                    }
433                                    if comparison.data_diff != 0 {
434                                        println!(
435                                            "  Data section: {} ({:.1}%)", self.colorize_diff(comparison
436                                            .data_diff, threshold * analysis.data_size as f64 / 100.0),
437                                            (comparison.data_diff as f64 / comparison.baseline.data_size
438                                            as f64 * 100.0)
439                                        );
440                                    }
441                                    if comparison.bss_diff != 0 {
442                                        println!(
443                                            "  BSS section: {} ({:.1}%)", self.colorize_diff(comparison
444                                            .bss_diff, threshold * analysis.bss_size as f64 / 100.0),
445                                            (comparison.bss_diff as f64 / comparison.baseline.bss_size
446                                            as f64 * 100.0)
447                                        );
448                                    }
449                                }
450                                Err(e) => {
451                                    if verbose {
452                                        println!("⚠️  Could not analyze baseline: {}", e);
453                                    }
454                                }
455                            }
456                        }
457                        if show_symbols {
458                            match self.find_largest_symbols(binary_path) {
459                                Ok(symbols) if !symbols.is_empty() => {
460                                    println!("\n🔍 Largest Symbols:");
461                                    for (i, symbol) in symbols.iter().enumerate() {
462                                        println!(
463                                            "  {}. {} ({} bytes) - {}", i + 1, symbol.name.cyan(),
464                                            symbol.size.to_string().yellow(), symbol.symbol_type
465                                        );
466                                    }
467                                }
468                                Ok(_) => {
469                                    if verbose {
470                                        println!("\n⚠️  No symbol information available");
471                                    }
472                                }
473                                Err(e) => {
474                                    if verbose {
475                                        println!("\n⚠️  Could not analyze symbols: {}", e);
476                                    }
477                                }
478                            }
479                        }
480                        if debug_compare {
481                            let debug_path = binary_path.replace("release", "debug");
482                            if Path::new(&debug_path).exists() {
483                                match self
484                                    .analyze_debug_vs_release(&debug_path, binary_path)
485                                {
486                                    Ok(comparison) => {
487                                        println!("\n🔧 Debug vs Release Comparison:");
488                                        println!(
489                                            "  Debug build: {}", self.format_size(comparison.debug_size)
490                                        );
491                                        println!(
492                                            "  Release build: {}", self.format_size(comparison
493                                            .release_size)
494                                        );
495                                        println!("  Size ratio: {:.1}x", comparison.ratio);
496                                        println!(
497                                            "  Space savings: {}", self.format_size(comparison.savings)
498                                            .green()
499                                        );
500                                    }
501                                    Err(e) => {
502                                        if verbose {
503                                            println!("\n⚠️  Could not compare builds: {}", e);
504                                        }
505                                    }
506                                }
507                            } else if verbose {
508                                println!(
509                                    "\n⚠️  Debug build not found at: {}", debug_path
510                                );
511                            }
512                        }
513                        if optimize {
514                            let suggestions = self
515                                .generate_optimization_suggestions(&analysis);
516                            if !suggestions.is_empty() {
517                                println!("\n💡 Optimization Suggestions:");
518                                for suggestion in suggestions {
519                                    let impact_color = match suggestion.impact.as_str() {
520                                        "High" => suggestion.impact.red().bold(),
521                                        "Medium" => suggestion.impact.yellow().bold(),
522                                        _ => suggestion.impact.green().bold(),
523                                    };
524                                    println!(
525                                        "  • [{}] {}: {}", impact_color, suggestion.category
526                                        .bold(), suggestion.suggestion
527                                    );
528                                }
529                            }
530                        }
531                        if report {
532                            println!("\n📋 Detailed Analysis Report:");
533                            println!(
534                                "═══════════════════════════════════════════════"
535                            );
536                            println!("Binary Path: {}", analysis.path);
537                            println!(
538                                "Total Size: {}", self.format_size(analysis.total_size)
539                            );
540                            println!("Symbol Count: {}", analysis.symbol_count);
541                            println!(
542                                "Text Section: {}", self.format_size(analysis.text_size)
543                            );
544                            println!(
545                                "Data Section: {}", self.format_size(analysis.data_size)
546                            );
547                            println!(
548                                "BSS Section: {}", self.format_size(analysis.bss_size)
549                            );
550                            if let Some(baseline) = baseline_path {
551                                if let Ok(comparison) = self
552                                    .analyze_size_changes(binary_path, baseline)
553                                {
554                                    println!("\nSize Changes:");
555                                    println!(
556                                        "Total: {}", self.format_diff(comparison.size_diff)
557                                    );
558                                    println!(
559                                        "Text: {}", self.format_diff(comparison.text_diff)
560                                    );
561                                    println!(
562                                        "Data: {}", self.format_diff(comparison.data_diff)
563                                    );
564                                    println!("BSS: {}", self.format_diff(comparison.bss_diff));
565                                }
566                            }
567                        }
568                    }
569                    Err(e) => {
570                        return Err(
571                            ToolError::ExecutionFailed(
572                                format!("Failed to analyze binary: {}", e),
573                            ),
574                        );
575                    }
576                }
577            }
578            OutputFormat::Json => {
579                let analysis = self.analyze_binary_size(binary_path)?;
580                let mut json_output = serde_json::json!(
581                    { "binary" : analysis.path, "total_size" : analysis.total_size,
582                    "text_size" : analysis.text_size, "data_size" : analysis.data_size,
583                    "bss_size" : analysis.bss_size, "symbol_count" : analysis
584                    .symbol_count, }
585                );
586                if let Some(baseline) = baseline_path {
587                    if let Ok(comparison) = self
588                        .analyze_size_changes(binary_path, baseline)
589                    {
590                        json_output["size_changes"] = serde_json::json!(
591                            { "total_diff" : comparison.size_diff, "text_diff" :
592                            comparison.text_diff, "data_diff" : comparison.data_diff,
593                            "bss_diff" : comparison.bss_diff, }
594                        );
595                    }
596                }
597                if show_symbols {
598                    if let Ok(symbols) = self.find_largest_symbols(binary_path) {
599                        json_output["largest_symbols"] = serde_json::to_value(&symbols)
600                            .unwrap();
601                    }
602                }
603                if optimize {
604                    let suggestions = self.generate_optimization_suggestions(&analysis);
605                    json_output["optimization_suggestions"] = serde_json::to_value(
606                            &suggestions,
607                        )
608                        .unwrap();
609                }
610                println!("{}", serde_json::to_string_pretty(& json_output).unwrap());
611            }
612            OutputFormat::Table => {
613                let analysis = self.analyze_binary_size(binary_path)?;
614                println!(
615                    "┌─ Binary Size Analysis ──────────────────────┐"
616                );
617                println!("│ Binary: {:<35} │", analysis.path);
618                println!("│ Size: {:<37} │", self.format_size(analysis.total_size));
619                println!("│ Symbols: {:<34} │", analysis.symbol_count.to_string());
620                println!("│ Text: {:<37} │", self.format_size(analysis.text_size));
621                println!("│ Data: {:<37} │", self.format_size(analysis.data_size));
622                println!("│ BSS: {:<38} │", self.format_size(analysis.bss_size));
623                println!(
624                    "└─────────────────────────────────────────────┘"
625                );
626            }
627        }
628        Ok(())
629    }
630}