use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::time::{Duration, Instant};
use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum BuildPhase {
DependencyResolution,
CmakeConfiguration,
Compilation,
Linking,
Archiving,
PostProcessing,
}
impl std::fmt::Display for BuildPhase {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
BuildPhase::DependencyResolution => write!(f, "Dependency Resolution"),
BuildPhase::CmakeConfiguration => write!(f, "CMake Configuration"),
BuildPhase::Compilation => write!(f, "Compilation"),
BuildPhase::Linking => write!(f, "Linking"),
BuildPhase::Archiving => write!(f, "Archiving"),
BuildPhase::PostProcessing => write!(f, "Post-Processing"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PhaseMetrics {
pub phase: BuildPhase,
pub duration_secs: f64,
pub percentage: f64,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct CacheStats {
pub tool: Option<String>,
pub hits: u64,
pub misses: u64,
pub hit_rate: f64,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct FileStats {
pub source_files: usize,
pub header_files: usize,
pub total_lines: usize,
pub artifact_size_bytes: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BuildAnalytics {
pub project: String,
pub platform: String,
pub timestamp: String,
pub total_duration_secs: f64,
pub phases: Vec<PhaseMetrics>,
pub cache_stats: CacheStats,
pub file_stats: FileStats,
pub parallel_jobs: usize,
pub peak_memory_mb: Option<u64>,
pub success: bool,
pub error_count: usize,
pub warning_count: usize,
}
impl BuildAnalytics {
pub fn analytics_dir() -> Result<PathBuf> {
let base_dirs = directories::BaseDirs::new().context("Failed to get base directories")?;
let home = base_dirs.home_dir();
let dir = home.join(".ccgo").join("analytics");
std::fs::create_dir_all(&dir)?;
Ok(dir)
}
pub fn analytics_file(project: &str) -> Result<PathBuf> {
Ok(Self::analytics_dir()?.join(format!("{}.json", project)))
}
pub fn save(&self) -> Result<()> {
let file_path = Self::analytics_file(&self.project)?;
let mut history = Self::load_history(&self.project)?;
history.push(self.clone());
if history.len() > 100 {
history.drain(0..history.len() - 100);
}
let json = serde_json::to_string_pretty(&history)?;
std::fs::write(&file_path, json)?;
Ok(())
}
pub fn load_history(project: &str) -> Result<Vec<BuildAnalytics>> {
let file_path = Self::analytics_file(project)?;
if !file_path.exists() {
return Ok(Vec::new());
}
let content = std::fs::read_to_string(&file_path)?;
let history: Vec<BuildAnalytics> = serde_json::from_str(&content)?;
Ok(history)
}
pub fn clear_history(project: &str) -> Result<()> {
let file_path = Self::analytics_file(project)?;
if file_path.exists() {
std::fs::remove_file(&file_path)?;
}
Ok(())
}
pub fn average_build_time(project: &str) -> Result<Option<f64>> {
let history = Self::load_history(project)?;
if history.is_empty() {
return Ok(None);
}
let total: f64 = history.iter().map(|a| a.total_duration_secs).sum();
Ok(Some(total / history.len() as f64))
}
pub fn print_summary(&self) {
println!("\n{}", "=".repeat(80));
println!("Build Analytics Summary");
println!("{}", "=".repeat(80));
println!();
println!("Project: {}", self.project);
println!("Platform: {}", self.platform);
println!("Timestamp: {}", self.timestamp);
println!("Total Duration: {:.2}s", self.total_duration_secs);
println!("Parallel Jobs: {}", self.parallel_jobs);
println!("Success: {}", if self.success { "✓" } else { "✗" });
println!();
if !self.phases.is_empty() {
println!("Phase Breakdown:");
println!("{}", "-".repeat(80));
for phase in &self.phases {
println!(
" {:.<40} {:>8.2}s ({:>5.1}%)",
format!("{}", phase.phase),
phase.duration_secs,
phase.percentage
);
}
println!();
}
if let Some(tool) = &self.cache_stats.tool {
println!("Cache Statistics (using {}):", tool);
println!("{}", "-".repeat(80));
println!(" Hits: {}", self.cache_stats.hits);
println!(" Misses: {}", self.cache_stats.misses);
println!(" Hit Rate: {:.1}%", self.cache_stats.hit_rate);
println!();
}
println!("File Statistics:");
println!("{}", "-".repeat(80));
println!(" Source Files: {}", self.file_stats.source_files);
println!(" Header Files: {}", self.file_stats.header_files);
println!(" Total Lines: {}", self.file_stats.total_lines);
println!(
" Artifact Size: {:.2} MB",
self.file_stats.artifact_size_bytes as f64 / 1024.0 / 1024.0
);
println!();
if self.error_count > 0 || self.warning_count > 0 {
println!("Diagnostics:");
println!("{}", "-".repeat(80));
if self.error_count > 0 {
println!(" Errors: {}", self.error_count);
}
if self.warning_count > 0 {
println!(" Warnings: {}", self.warning_count);
}
println!();
}
println!("{}", "=".repeat(80));
}
}
pub struct AnalyticsCollector {
project: String,
platform: String,
start_time: Instant,
phase_timers: HashMap<BuildPhase, Instant>,
phase_durations: HashMap<BuildPhase, Duration>,
parallel_jobs: usize,
success: bool,
error_count: usize,
warning_count: usize,
}
impl AnalyticsCollector {
pub fn new(project: String, platform: String, parallel_jobs: usize) -> Self {
Self {
project,
platform,
start_time: Instant::now(),
phase_timers: HashMap::new(),
phase_durations: HashMap::new(),
parallel_jobs,
success: false,
error_count: 0,
warning_count: 0,
}
}
pub fn start_phase(&mut self, phase: BuildPhase) {
self.phase_timers.insert(phase, Instant::now());
}
pub fn end_phase(&mut self, phase: BuildPhase) {
if let Some(start) = self.phase_timers.remove(&phase) {
let duration = start.elapsed();
self.phase_durations.insert(phase, duration);
}
}
pub fn set_success(&mut self, success: bool) {
self.success = success;
}
pub fn add_error(&mut self) {
self.error_count += 1;
}
pub fn add_warning(&mut self) {
self.warning_count += 1;
}
pub fn add_diagnostics(&mut self, errors: usize, warnings: usize) {
self.error_count += errors;
self.warning_count += warnings;
}
pub fn finalize(
self,
cache_stats: Option<CacheStats>,
file_stats: Option<FileStats>,
) -> BuildAnalytics {
let total_duration = self.start_time.elapsed();
let total_secs = total_duration.as_secs_f64();
let mut phases: Vec<PhaseMetrics> = self
.phase_durations
.iter()
.map(|(phase, duration)| {
let duration_secs = duration.as_secs_f64();
let percentage = (duration_secs / total_secs) * 100.0;
PhaseMetrics {
phase: *phase,
duration_secs,
percentage,
}
})
.collect();
phases.sort_by(|a, b| {
b.duration_secs
.partial_cmp(&a.duration_secs)
.unwrap_or(std::cmp::Ordering::Equal)
});
BuildAnalytics {
project: self.project,
platform: self.platform,
timestamp: chrono::Local::now().to_rfc3339(),
total_duration_secs: total_secs,
phases,
cache_stats: cache_stats.unwrap_or_default(),
file_stats: file_stats.unwrap_or_default(),
parallel_jobs: self.parallel_jobs,
peak_memory_mb: None, success: self.success,
error_count: self.error_count,
warning_count: self.warning_count,
}
}
}
pub fn get_cache_stats(cache_tool: Option<&str>) -> Option<CacheStats> {
let tool = cache_tool?;
match tool {
"ccache" => get_ccache_stats(),
"sccache" => get_sccache_stats(),
_ => None,
}
}
fn get_ccache_stats() -> Option<CacheStats> {
use std::process::Command;
let output = Command::new("ccache").arg("-s").output().ok()?;
if !output.status.success() {
return None;
}
let stdout = String::from_utf8_lossy(&output.stdout);
let mut hits = 0u64;
let mut misses = 0u64;
for line in stdout.lines() {
if line.contains("cache hit") {
if let Some(count) = line.split_whitespace().last() {
hits += count.parse::<u64>().unwrap_or(0);
}
} else if line.contains("cache miss") {
if let Some(count) = line.split_whitespace().last() {
misses = count.parse::<u64>().unwrap_or(0);
}
}
}
let total = hits + misses;
let hit_rate = if total > 0 {
(hits as f64 / total as f64) * 100.0
} else {
0.0
};
Some(CacheStats {
tool: Some("ccache".to_string()),
hits,
misses,
hit_rate,
})
}
fn get_sccache_stats() -> Option<CacheStats> {
use std::process::Command;
let output = Command::new("sccache").arg("--show-stats").output().ok()?;
if !output.status.success() {
return None;
}
let stdout = String::from_utf8_lossy(&output.stdout);
let mut hits = 0u64;
let mut misses = 0u64;
for line in stdout.lines() {
let line = line.trim();
if line.starts_with("Cache hits") {
if let Some(count) = line.split_whitespace().last() {
hits = count.parse::<u64>().unwrap_or(0);
}
} else if line.starts_with("Cache misses") {
if let Some(count) = line.split_whitespace().last() {
misses = count.parse::<u64>().unwrap_or(0);
}
}
}
let total = hits + misses;
let hit_rate = if total > 0 {
(hits as f64 / total as f64) * 100.0
} else {
0.0
};
Some(CacheStats {
tool: Some("sccache".to_string()),
hits,
misses,
hit_rate,
})
}
pub fn count_files(dir: &Path) -> Result<(usize, usize)> {
let mut source_files = 0;
let mut header_files = 0;
if !dir.exists() {
return Ok((0, 0));
}
for entry in walkdir::WalkDir::new(dir)
.follow_links(false)
.into_iter()
.filter_map(|e| e.ok())
{
let path = entry.path();
if path.is_file() {
if let Some(ext) = path.extension() {
match ext.to_str() {
Some("c") | Some("cc") | Some("cpp") | Some("cxx") => {
source_files += 1;
}
Some("h") | Some("hh") | Some("hpp") | Some("hxx") => {
header_files += 1;
}
_ => {}
}
}
}
}
Ok((source_files, header_files))
}
pub fn get_artifact_size(path: &Path) -> Result<u64> {
if !path.exists() {
return Ok(0);
}
let metadata = std::fs::metadata(path)?;
Ok(metadata.len())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_phase_display() {
assert_eq!(BuildPhase::Compilation.to_string(), "Compilation");
}
#[test]
fn test_analytics_collector() {
let mut collector = AnalyticsCollector::new("test".to_string(), "linux".to_string(), 4);
collector.start_phase(BuildPhase::Compilation);
std::thread::sleep(std::time::Duration::from_millis(10));
collector.end_phase(BuildPhase::Compilation);
collector.set_success(true);
let analytics = collector.finalize(None, None);
assert_eq!(analytics.project, "test");
assert_eq!(analytics.platform, "linux");
assert_eq!(analytics.parallel_jobs, 4);
assert!(analytics.success);
assert!(analytics.total_duration_secs > 0.0);
}
}