blob: 50a964bf2fe140fbbee033c886022d31aed64e39 [file] [edit]
//! 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());
}
}