| //! Build analysis logging infrastructure. |
| |
| use std::cell::RefCell; |
| use std::hash::Hash; |
| use std::io::{BufWriter, Write}; |
| use std::mem::ManuallyDrop; |
| use std::path::Path; |
| use std::sync::mpsc::{self, Sender}; |
| use std::thread::JoinHandle; |
| |
| use anyhow::Context as _; |
| use cargo_util::paths; |
| |
| use crate::CargoResult; |
| use crate::compiler::BuildConfig; |
| use crate::util::log_message::LogMessage; |
| use crate::util::short_hash; |
| use crate::workspace::Workspace; |
| |
| // for newer `cargo report` commands |
| struct FileLogger { |
| tx: ManuallyDrop<Sender<LogMessage>>, |
| handle: Option<JoinHandle<()>>, |
| } |
| |
| impl FileLogger { |
| /// Creates a logger if `-Zbuild-analysis` is enabled |
| fn maybe_new(ws: &Workspace<'_>, run_id: &RunId) -> CargoResult<Option<FileLogger>> { |
| let analysis = ws.gctx().build_config()?.analysis.as_ref(); |
| match (analysis, ws.gctx().cli_unstable().build_analysis) { |
| (Some(analysis), true) if analysis.enabled => { |
| let log_dir = ws.gctx().home().join("log"); |
| paths::create_dir_all(log_dir.as_path_unlocked())?; |
| |
| let filename = format!("{run_id}.jsonl"); |
| let log_file = log_dir.open_rw_exclusive_create( |
| Path::new(&filename), |
| ws.gctx(), |
| "build analysis log", |
| )?; |
| |
| let (tx, rx) = mpsc::channel::<LogMessage>(); |
| |
| let run_id_str = run_id.to_string(); |
| let handle = std::thread::spawn(move || { |
| let mut writer = BufWriter::new(log_file); |
| for msg in rx { |
| let _ = msg.write_json_log(&mut writer, &run_id_str); |
| } |
| let _ = writer.flush(); |
| }); |
| |
| Ok(Some(Self { |
| tx: ManuallyDrop::new(tx), |
| handle: Some(handle), |
| })) |
| } |
| (Some(_), false) => { |
| ws.gctx().shell().warn( |
| "ignoring 'build.analysis' config, pass `-Zbuild-analysis` to enable it", |
| )?; |
| Ok(None) |
| } |
| _ => Ok(None), |
| } |
| } |
| } |
| |
| impl Drop for FileLogger { |
| fn drop(&mut self) { |
| // SAFETY: tx is dropped exactly once here to signal thread shutdown. |
| // ManuallyDrop prevents automatic drop after this impl runs. |
| unsafe { |
| ManuallyDrop::drop(&mut self.tx); |
| } |
| |
| if let Some(handle) = self.handle.take() { |
| let _ = handle.join(); |
| } |
| } |
| } |
| |
| /// For legacy `cargo build --timings` flag |
| struct InMemoryLogger { |
| // using mutex to hide mutability |
| logs: RefCell<Vec<LogMessage>>, |
| } |
| |
| impl InMemoryLogger { |
| fn maybe_new(options: &BuildConfig) -> Option<Self> { |
| if options.timing_report { |
| Some(Self { |
| logs: RefCell::new(Vec::new()), |
| }) |
| } else { |
| None |
| } |
| } |
| } |
| |
| /// Logger for `-Zbuild-analysis`. |
| pub struct BuildLogger { |
| run_id: RunId, |
| file_logger: Option<FileLogger>, |
| in_memory_logger: Option<InMemoryLogger>, |
| } |
| |
| impl BuildLogger { |
| pub fn maybe_new(ws: &Workspace<'_>, options: &BuildConfig) -> CargoResult<Option<Self>> { |
| let run_id = Self::generate_run_id(ws); |
| let file_logger = FileLogger::maybe_new(ws, &run_id)?; |
| let in_memory_logger = InMemoryLogger::maybe_new(options); |
| |
| if file_logger.is_none() && in_memory_logger.is_none() { |
| return Ok(None); |
| } |
| |
| Ok(Some(Self { |
| run_id, |
| file_logger, |
| in_memory_logger, |
| })) |
| } |
| |
| /// Generates a unique run ID. |
| pub fn generate_run_id(ws: &Workspace<'_>) -> RunId { |
| RunId::new(&ws.root(), ws.gctx().invocation_time()) |
| } |
| |
| /// Returns the run ID for this build session. |
| pub fn run_id(&self) -> &RunId { |
| &self.run_id |
| } |
| |
| /// Logs a message. |
| pub fn log(&self, msg: LogMessage) { |
| if let Some(ref logger) = self.in_memory_logger { |
| let mut borrowed = logger.logs.try_borrow_mut().expect( |
| "Unable to get a mutable reference to in-memory logger; please file a bug report", |
| ); |
| borrowed.push(msg.clone()); |
| }; |
| |
| if let Some(ref logger) = self.file_logger { |
| let _ = logger.tx.send(msg); |
| }; |
| } |
| |
| pub fn get_logs(&self) -> Option<Vec<LogMessage>> { |
| self.in_memory_logger.as_ref().map(|l| { |
| l.logs |
| .try_borrow() |
| .expect("Unable to get a reference to in-memory logger; please file a bug report") |
| .clone() |
| }) |
| } |
| } |
| |
| /// A unique identifier for a Cargo invocation. |
| #[derive(Clone)] |
| pub struct RunId { |
| timestamp: jiff::Timestamp, |
| hash: String, |
| } |
| |
| impl RunId { |
| const FORMAT: &str = "%Y%m%dT%H%M%S%3fZ"; |
| |
| pub fn new<H: Hash>(h: &H, timestamp: jiff::Timestamp) -> RunId { |
| RunId { |
| timestamp, |
| hash: short_hash(h), |
| } |
| } |
| |
| pub fn timestamp(&self) -> &jiff::Timestamp { |
| &self.timestamp |
| } |
| |
| /// Checks whether ID was generated from the same workspace. |
| pub fn same_workspace(&self, other: &RunId) -> bool { |
| self.hash == other.hash |
| } |
| } |
| |
| impl std::fmt::Display for RunId { |
| fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
| let hash = &self.hash; |
| let timestamp = self.timestamp.strftime(Self::FORMAT); |
| write!(f, "{timestamp}-{hash}") |
| } |
| } |
| |
| impl std::str::FromStr for RunId { |
| type Err = anyhow::Error; |
| |
| fn from_str(s: &str) -> Result<Self, Self::Err> { |
| let msg = |
| || format!("expect run ID in format `20060724T012128000Z-<16-char-hex>`, got `{s}`"); |
| let Some((timestamp, hash)) = s.rsplit_once('-') else { |
| anyhow::bail!(msg()); |
| }; |
| |
| if hash.len() != 16 || !hash.chars().all(|c| c.is_ascii_hexdigit()) { |
| anyhow::bail!(msg()); |
| } |
| let timestamp = jiff::civil::DateTime::strptime(Self::FORMAT, timestamp) |
| .and_then(|dt| dt.to_zoned(jiff::tz::TimeZone::UTC)) |
| .map(|zoned| zoned.timestamp()) |
| .with_context(msg)?; |
| |
| Ok(RunId { |
| timestamp, |
| hash: hash.into(), |
| }) |
| } |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| use super::*; |
| |
| #[test] |
| fn run_id_round_trip() { |
| let id = "20060724T012128000Z-b0fd440798ab3cfb"; |
| assert_eq!(id, &id.parse::<RunId>().unwrap().to_string()); |
| } |
| } |