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}