use anyhow::Result;
use std::fs::{self, OpenOptions};
use std::io::Write;
use std::path::PathBuf;
use std::time::{Duration, Instant};
#[derive(Debug, Clone)]
pub struct PhaseRecord {
pub name: String,
pub duration: Duration,
pub details: String,
}
pub struct BuildLogger {
start: Instant,
log_path: Option<PathBuf>,
phases: Vec<PhaseRecord>,
}
pub struct PhaseTimer<'a> {
logger: &'a mut BuildLogger,
name: String,
start: Instant,
}
impl<'a> PhaseTimer<'a> {
pub fn finish_with_details(self, details: &str) {
let dur = self.start.elapsed();
self.logger.phases.push(PhaseRecord {
name: self.name.clone(),
duration: dur,
details: details.to_string(),
});
}
}
impl BuildLogger {
pub fn new(log_path: Option<PathBuf>) -> Self {
Self {
start: Instant::now(),
log_path,
phases: Vec::new(),
}
}
pub fn record_phase(&mut self, name: &str, duration: Duration, details: &str) {
self.phases.push(PhaseRecord {
name: name.to_string(),
duration,
details: details.to_string(),
});
}
pub fn measure<F, R>(&mut self, name: &str, details: &str, f: F) -> R
where
F: FnOnce() -> R,
{
let start = Instant::now();
let res = f();
let dur = start.elapsed();
self.record_phase(name, dur, details);
res
}
pub fn start_phase<'a>(&'a mut self, name: &str) -> PhaseTimer<'a> {
PhaseTimer {
logger: self,
name: name.to_string(),
start: Instant::now(),
}
}
pub fn phases(&self) -> &[PhaseRecord] {
&self.phases
}
pub fn total_elapsed(&self) -> Duration {
self.start.elapsed()
}
pub fn write_trace(&self, profile: &str, exit_code: i32, outcome: &str) -> Result<()> {
let Some(ref log_path) = self.log_path else {
return Ok(());
};
if let Some(parent) = log_path.parent() {
fs::create_dir_all(parent)?;
}
let total = self.start.elapsed();
let mut f = OpenOptions::new()
.create(true)
.write(true)
.truncate(true)
.open(log_path)?;
writeln!(f, "=== Mitts Build Execution Trace ===")?;
writeln!(f, "Timestamp: {}", humantime_format())?;
writeln!(f, "Profile: [{}]", profile)?;
writeln!(f, "Exit Code: {}", exit_code)?;
writeln!(f, "Outcome: {}", outcome)?;
writeln!(
f,
"Total Dur: {:.2}s ({:.1}ms)",
total.as_secs_f64(),
total.as_secs_f64() * 1000.0
)?;
writeln!(f)?;
writeln!(f, "Phase Breakdown:")?;
writeln!(
f,
"--------------------------------------------------------------------------------"
)?;
writeln!(
f,
"{:<14} {:>10} {:>8} DETAILS",
"PHASE", "DURATION", "SHARE"
)?;
writeln!(
f,
"--------------------------------------------------------------------------------"
)?;
let total_ms = total.as_secs_f64() * 1000.0;
for p in &self.phases {
let p_ms = p.duration.as_secs_f64() * 1000.0;
let share = if total_ms > 0.0 {
(p_ms / total_ms) * 100.0
} else {
0.0
};
writeln!(
f,
"{:<14} {:>8.1}ms {:>7.1}% {}",
p.name, p_ms, share, p.details
)?;
}
writeln!(
f,
"--------------------------------------------------------------------------------"
)?;
writeln!(f, "Total: {:>17.1}ms", total_ms)?;
writeln!(
f,
"================================================================================"
)?;
Ok(())
}
}
fn humantime_format() -> String {
let now = std::time::SystemTime::now();
let dt = now
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
format!("UNIX:{}", dt)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_build_logger_measures_phases() {
let temp = tempfile::tempdir().unwrap();
let log_file = temp.path().join("build.log");
let mut logger = BuildLogger::new(Some(log_file.clone()));
let val = logger.measure("test_phase", "running test phase", || {
std::thread::sleep(Duration::from_millis(5));
42
});
assert_eq!(val, 42);
logger.write_trace("dev", 0, "All targets clean").unwrap();
let content = fs::read_to_string(&log_file).unwrap();
assert!(content.contains("Mitts Build Execution Trace"));
assert!(content.contains("test_phase"));
}
}