blob: 3649e28a3bbcff8e44514b5b65ee361d88ef7ee8 [file] [edit]
//! Timing tracking.
//!
//! This module implements some simple tracking information for timing of how
//! long it takes for different units to compile.
pub mod report;
use super::CompileMode;
use super::Unit;
use super::UnitIndex;
use crate::compiler::BuildContext;
use crate::compiler::BuildRunner;
use crate::compiler::job_queue::JobId;
use crate::ops::cargo_report::timings::prepare_context;
use crate::util::cpu::State;
use crate::util::log_message::LogMessage;
use crate::util::style;
use crate::util::{CargoResult, GlobalContext};
use crate::util::data_structures::HashMap;
use cargo_util::paths;
use std::io::BufWriter;
use std::time::{Duration, Instant};
/// Tracking information for the entire build.
///
/// Methods on this structure are generally called from the main thread of a
/// running [`JobQueue`] instance (`DrainState` in specific) when the queue
/// receives messages from spawned off threads.
///
/// [`JobQueue`]: super::JobQueue
pub struct Timings<'gctx> {
gctx: &'gctx GlobalContext,
/// Whether or not timings should be captured.
enabled: bool,
/// When Cargo started.
start: Instant,
/// A summary of the root units.
///
/// A map from unit to index.
unit_to_index: HashMap<Unit, UnitIndex>,
/// Units that are in the process of being built.
/// When they finished, they are moved to `unit_times`.
active: HashMap<JobId, Unit>,
/// Last recorded state of the system's CPUs and when it happened
last_cpu_state: Option<State>,
last_cpu_recording: Instant,
/// Recorded CPU states, stored as tuples. First element is when the
/// recording was taken and second element is percentage usage of the
/// system.
cpu_usage: Vec<(f64, f64)>,
}
/// Section of compilation (e.g. frontend, backend, linking).
#[derive(Copy, Clone, serde::Serialize)]
pub struct CompilationSection {
/// Start of the section, as an offset in seconds from `UnitTime::start`.
pub start: f64,
/// End of the section, as an offset in seconds from `UnitTime::start`.
pub end: Option<f64>,
}
/// Data for a single compilation unit, prepared for serialization to JSON.
///
/// This is used by the HTML report's JavaScript to render the pipeline graph.
#[derive(serde::Serialize)]
pub struct UnitData {
pub i: UnitIndex,
pub name: String,
pub version: String,
pub mode: String,
pub target: String,
pub features: Vec<String>,
pub start: f64,
pub duration: f64,
pub unblocked_units: Vec<UnitIndex>,
pub unblocked_rmeta_units: Vec<UnitIndex>,
pub sections: Option<Vec<(report::SectionName, report::SectionData)>>,
}
impl<'gctx> Timings<'gctx> {
pub fn new(bcx: &BuildContext<'_, 'gctx>) -> Timings<'gctx> {
let start = bcx.gctx.invocation_instant();
let enabled = bcx.logger.is_some();
if !enabled {
return Timings {
gctx: bcx.gctx,
enabled,
start,
unit_to_index: HashMap::default(),
active: HashMap::default(),
last_cpu_state: None,
last_cpu_recording: Instant::now(),
cpu_usage: Vec::new(),
};
}
let last_cpu_state = match State::current() {
Ok(state) => Some(state),
Err(e) => {
tracing::info!("failed to get CPU state, CPU tracking disabled: {:?}", e);
None
}
};
Timings {
gctx: bcx.gctx,
enabled,
start,
unit_to_index: bcx.unit_to_index.clone(),
active: HashMap::default(),
last_cpu_state,
last_cpu_recording: Instant::now(),
cpu_usage: Vec::new(),
}
}
/// Mark that a unit has started running.
pub fn unit_start(&mut self, build_runner: &BuildRunner<'_, '_>, id: JobId, unit: Unit) {
let Some(logger) = build_runner.bcx.logger else {
return;
};
let mut target = if unit.target.is_lib()
&& matches!(unit.mode, CompileMode::Build | CompileMode::Check { .. })
{
// Special case for brevity, since most dependencies hit this path.
"".to_string()
} else {
format!(" {}", unit.target.description_named())
};
match unit.mode {
CompileMode::Test => target.push_str(" (test)"),
CompileMode::Build => {}
CompileMode::Check { test: true } => target.push_str(" (check-test)"),
CompileMode::Check { test: false } => target.push_str(" (check)"),
CompileMode::Doc { .. } => target.push_str(" (doc)"),
CompileMode::Doctest => target.push_str(" (doc test)"),
CompileMode::Docscrape => target.push_str(" (doc scrape)"),
CompileMode::RunCustomBuild => target.push_str(" (run)"),
}
let start = self.start.elapsed().as_secs_f64();
logger.log(LogMessage::UnitStarted {
index: self.unit_to_index[&unit],
elapsed: start,
});
assert!(self.active.insert(id, unit).is_none());
}
/// Mark that the `.rmeta` file as generated.
pub fn unit_rmeta_finished(
&mut self,
build_runner: &BuildRunner<'_, '_>,
id: JobId,
unblocked: Vec<&Unit>,
) {
let Some(logger) = build_runner.bcx.logger else {
return;
};
// `id` may not always be active. "fresh" units unconditionally
// generate `Message::Finish`, but this active map only tracks dirty
// units.
let Some(unit) = self.active.get(&id) else {
return;
};
let elapsed = self.start.elapsed().as_secs_f64();
let unblocked = unblocked.iter().map(|u| self.unit_to_index[u]).collect();
logger.log(LogMessage::UnitRmetaFinished {
index: self.unit_to_index[unit],
elapsed,
unblocked,
});
}
/// Mark that a unit has finished running.
pub fn unit_finished(
&mut self,
build_runner: &BuildRunner<'_, '_>,
id: JobId,
unblocked: Vec<&Unit>,
) {
let Some(logger) = build_runner.bcx.logger else {
return;
};
// See note above in `unit_rmeta_finished`, this may not always be active.
let Some(unit) = self.active.remove(&id) else {
return;
};
let elapsed = self.start.elapsed().as_secs_f64();
let unblocked = unblocked.iter().map(|u| self.unit_to_index[u]).collect();
logger.log(LogMessage::UnitFinished {
index: self.unit_to_index[&unit],
elapsed,
unblocked,
});
}
/// Handle the start/end of a compilation section.
pub fn unit_section_timing(
&mut self,
build_runner: &BuildRunner<'_, '_>,
id: JobId,
section_timing: &SectionTiming,
) {
let Some(logger) = build_runner.bcx.logger else {
return;
};
let Some(unit) = self.active.get(&id) else {
return;
};
let elapsed = self.start.elapsed().as_secs_f64();
let index = self.unit_to_index[&unit];
let section = section_timing.name.clone();
logger.log(match section_timing.event {
SectionTimingEvent::Start => LogMessage::UnitSectionStarted {
index,
elapsed,
section,
},
SectionTimingEvent::End => LogMessage::UnitSectionFinished {
index,
elapsed,
section,
},
})
}
/// Take a sample of CPU usage
pub fn record_cpu(&mut self) {
if !self.enabled {
return;
}
let Some(prev) = &mut self.last_cpu_state else {
return;
};
// Don't take samples too frequently, even if requested.
let now = Instant::now();
if self.last_cpu_recording.elapsed() < Duration::from_millis(100) {
return;
}
let current = match State::current() {
Ok(s) => s,
Err(e) => {
tracing::info!("failed to get CPU state: {:?}", e);
return;
}
};
let pct_idle = current.idle_since(prev);
*prev = current;
self.last_cpu_recording = now;
let dur = now.duration_since(self.start).as_secs_f64();
self.cpu_usage.push((dur, 100.0 - pct_idle));
}
/// Call this when all units are finished.
pub fn finished(
&mut self,
build_runner: &BuildRunner<'_, '_>,
error: &Option<anyhow::Error>,
) -> CargoResult<()> {
if let Some(logger) = build_runner.bcx.logger
&& let Some(logs) = logger.get_logs()
{
let timings_path = build_runner
.files()
.timings_dir()
.expect("artifact-dir was not locked");
paths::create_dir_all(&timings_path)?;
let run_id = logger.run_id();
let filename = timings_path.join(format!("cargo-timing-{run_id}.html"));
let mut f = BufWriter::new(paths::create(&filename)?);
let mut ctx = prepare_context(logs.into_iter(), run_id, false)?;
ctx.error = error;
ctx.cpu_usage = &self.cpu_usage;
report::write_html(ctx, &mut f)?;
let unstamped_filename = timings_path.join("cargo-timing.html");
paths::link_or_copy(&filename, &unstamped_filename)?;
let mut shell = self.gctx.shell();
let timing_path = std::env::current_dir().unwrap_or_default().join(&filename);
let link = shell.err_file_hyperlink(&timing_path);
let msg = format!("report saved to {link}{}{link:#}", timing_path.display(),);
shell.status_with_color("Timing", msg, &style::NOTE)?;
}
Ok(())
}
}
/// Start or end of a section timing.
#[derive(serde::Deserialize, Debug)]
#[serde(rename_all = "kebab-case")]
pub enum SectionTimingEvent {
Start,
End,
}
/// Represents a certain section (phase) of rustc compilation.
/// It is emitted by rustc when the `--json=timings` flag is used.
#[derive(serde::Deserialize, Debug)]
pub struct SectionTiming {
pub name: String,
pub event: SectionTimingEvent,
}