diff --git a/src/bin/cargo/commands/report.rs b/src/bin/cargo/commands/report.rs index 2f5717e21f7..e31edb19133 100644 --- a/src/bin/cargo/commands/report.rs +++ b/src/bin/cargo/commands/report.rs @@ -44,6 +44,11 @@ pub fn cli() -> Command { .default_value("10"), ), ) + .subcommand( + subcommand("rebuilds") + .about("Reports rebuild reasons from previous sessions (unstable)") + .arg_manifest_path(), + ) } pub fn exec(gctx: &mut GlobalContext, args: &ArgMatches) -> CliResult { @@ -75,6 +80,19 @@ pub fn exec(gctx: &mut GlobalContext, args: &ArgMatches) -> CliResult { ops::report_sessions(gctx, ws.as_ref(), opts)?; Ok(()) } + Some(("rebuilds", args)) => { + gctx.cli_unstable().fail_if_stable_command( + gctx, + "report rebuilds", + 15844, + "build-analysis", + gctx.cli_unstable().build_analysis, + )?; + let ws = args.workspace(gctx).ok(); + let opts = rebuilds_opts(args)?; + ops::report_rebuilds(gctx, ws.as_ref(), opts)?; + Ok(()) + } Some((cmd, _)) => { unreachable!("unexpected command {}", cmd) } @@ -112,3 +130,7 @@ fn sessions_opts(args: &ArgMatches) -> CargoResult { Ok(ops::ReportSessionsOptions { limit }) } + +fn rebuilds_opts(_args: &ArgMatches) -> CargoResult { + Ok(ops::ReportRebuildsOptions {}) +} diff --git a/src/cargo/core/compiler/fingerprint/dep_info.rs b/src/cargo/core/compiler/fingerprint/dep_info.rs index 3c3900edfa9..324f4b8c37d 100644 --- a/src/cargo/core/compiler/fingerprint/dep_info.rs +++ b/src/cargo/core/compiler/fingerprint/dep_info.rs @@ -678,6 +678,16 @@ impl Serialize for Checksum { } } +impl<'de> serde::Deserialize<'de> for Checksum { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + s.parse().map_err(serde::de::Error::custom) + } +} + #[derive(Debug, thiserror::Error)] pub enum InvalidChecksum { #[error("algorithm portion incorrect, expected `sha256`, or `blake3`")] diff --git a/src/cargo/core/compiler/fingerprint/dirty_reason.rs b/src/cargo/core/compiler/fingerprint/dirty_reason.rs index dddb850eddc..8e4e9a9364f 100644 --- a/src/cargo/core/compiler/fingerprint/dirty_reason.rs +++ b/src/cargo/core/compiler/fingerprint/dirty_reason.rs @@ -1,3 +1,4 @@ +use std::collections::HashMap; use std::fmt; use std::fmt::Debug; @@ -10,7 +11,7 @@ use crate::core::Shell; /// to a recompile. Usually constructed via [`Fingerprint::compare`]. /// /// [`Fingerprint::compare`]: super::Fingerprint::compare -#[derive(Clone, Debug, Serialize)] +#[derive(Clone, Debug, Serialize, Deserialize)] #[serde(tag = "dirty_reason", rename_all = "kebab-case")] pub enum DirtyReason { RustcChanged, @@ -61,8 +62,8 @@ pub enum DirtyReason { new_value: Option, }, LocalFingerprintTypeChanged { - old: &'static str, - new: &'static str, + old: String, + new: String, }, NumberOfDependenciesChanged { old: usize, @@ -73,11 +74,7 @@ pub enum DirtyReason { new: InternedString, }, UnitDependencyInfoChanged { - old_name: InternedString, - old_fingerprint: u64, - - new_name: InternedString, - new_fingerprint: u64, + unit: u64, }, FsStatusOutdated(FsStatus), NothingObvious, @@ -148,7 +145,13 @@ impl DirtyReason { } } - pub fn present_to(&self, s: &mut Shell, unit: &Unit, root: &Path) -> CargoResult<()> { + pub fn present_to( + &self, + s: &mut Shell, + unit: &Unit, + root: &Path, + index_to_unit: &HashMap, + ) -> CargoResult<()> { match self { DirtyReason::RustcChanged => s.dirty_because(unit, "the toolchain changed"), DirtyReason::FeaturesChanged { .. } => { @@ -220,8 +223,12 @@ impl DirtyReason { unit, format_args!("name of dependency changed ({old} => {new})"), ), - DirtyReason::UnitDependencyInfoChanged { .. } => { - s.dirty_because(unit, "dependency info changed") + DirtyReason::UnitDependencyInfoChanged { unit: dep_unit } => { + let dep_name = index_to_unit.get(dep_unit).map(|u| u.pkg.name()).unwrap(); + s.dirty_because( + unit, + format_args!("info of dependency `{dep_name}` changed"), + ) } DirtyReason::FsStatusOutdated(status) => match status { FsStatus::Stale => s.dirty_because(unit, "stale, unknown reason"), @@ -301,19 +308,23 @@ impl DirtyReason { ), }, FsStatus::StaleDependency { - name, + unit: dep_unit, dep_mtime, max_mtime, - .. } => { + let dep_name = index_to_unit.get(dep_unit).map(|u| u.pkg.name()).unwrap(); let after = Self::after(*max_mtime, *dep_mtime, "last build"); s.dirty_because( unit, - format_args!("the dependency {name} was rebuilt ({after})"), + format_args!("the dependency `{dep_name}` was rebuilt ({after})"), ) } - FsStatus::StaleDepFingerprint { name } => { - s.dirty_because(unit, format_args!("the dependency {name} was rebuilt")) + FsStatus::StaleDepFingerprint { unit: dep_unit } => { + let dep_name = index_to_unit.get(dep_unit).map(|u| u.pkg.name()).unwrap(); + s.dirty_because( + unit, + format_args!("the dependency `{dep_name}` was rebuilt"), + ) } FsStatus::UpToDate { .. } => { unreachable!() @@ -529,8 +540,8 @@ mod json_schema { str![[r#" { "dirty_reason": "unit-dependency-name-changed", - "old": "old_dep", - "new": "new_dep" + "new": "new_dep", + "old": "old_dep" } "#]] .is_json() @@ -539,21 +550,13 @@ mod json_schema { #[test] fn unit_dependency_info_changed() { - let reason = DirtyReason::UnitDependencyInfoChanged { - old_name: "serde".into(), - old_fingerprint: 0x1234567890abcdef, - new_name: "serde".into(), - new_fingerprint: 0xfedcba0987654321, - }; + let reason = DirtyReason::UnitDependencyInfoChanged { unit: 15 }; assert_data_eq!( to_json(&reason), str![[r#" { "dirty_reason": "unit-dependency-info-changed", - "new_fingerprint": 18364757930599072545, - "new_name": "serde", - "old_fingerprint": 1311768467294899695, - "old_name": "serde" + "unit": 15 } "#]] .is_json() @@ -647,7 +650,7 @@ mod json_schema { #[test] fn fs_status_stale_dependency() { let reason = DirtyReason::FsStatusOutdated(FsStatus::StaleDependency { - name: "serde".into(), + unit: 42, dep_mtime: FileTime::from_unix_time(1730567892, 789000000), max_mtime: FileTime::from_unix_time(1730567890, 123000000), }); @@ -659,7 +662,7 @@ mod json_schema { "dirty_reason": "fs-status-outdated", "fs_status": "stale-dependency", "max_mtime": 1730567890123.0, - "name": "serde" + "unit": 42 } "#]] .is_json() @@ -668,16 +671,14 @@ mod json_schema { #[test] fn fs_status_stale_dep_fingerprint() { - let reason = DirtyReason::FsStatusOutdated(FsStatus::StaleDepFingerprint { - name: "tokio".into(), - }); + let reason = DirtyReason::FsStatusOutdated(FsStatus::StaleDepFingerprint { unit: 42 }); assert_data_eq!( to_json(&reason), str![[r#" { "dirty_reason": "fs-status-outdated", "fs_status": "stale-dep-fingerprint", - "name": "tokio" + "unit": 42 } "#]] .is_json() @@ -830,8 +831,8 @@ mod json_schema { #[test] fn local_fingerprint_type_changed() { let reason = DirtyReason::LocalFingerprintTypeChanged { - old: "precalculated", - new: "rerun-if-changed", + old: "precalculated".to_owned(), + new: "rerun-if-changed".to_owned(), }; assert_data_eq!( to_json(&reason), diff --git a/src/cargo/core/compiler/fingerprint/mod.rs b/src/cargo/core/compiler/fingerprint/mod.rs index c06312de433..5197165d27c 100644 --- a/src/cargo/core/compiler/fingerprint/mod.rs +++ b/src/cargo/core/compiler/fingerprint/mod.rs @@ -657,6 +657,10 @@ pub struct Fingerprint { /// The rustc target. This is only relevant for `.json` files, otherwise /// the metadata hash segregates the units. compile_kind: u64, + /// Unit index for this fingerprint, used for tracing cascading rebuilds. + /// Not persisted to disk as indices can change between builds. + #[serde(skip)] + index: u64, /// Description of whether the filesystem status for this unit is up to date /// or should be considered stale. #[serde(skip)] @@ -670,7 +674,7 @@ pub struct Fingerprint { } /// Indication of the status on the filesystem for a particular unit. -#[derive(Clone, Default, Debug, Serialize)] +#[derive(Clone, Default, Debug, Serialize, Deserialize)] #[serde(tag = "fs_status", rename_all = "kebab-case")] pub enum FsStatus { /// This unit is to be considered stale, even if hash information all @@ -685,15 +689,15 @@ pub enum FsStatus { /// A dependency was stale. StaleDependency { - name: InternedString, - #[serde(serialize_with = "serialize_file_time")] + unit: u64, + #[serde(with = "serde_file_time")] dep_mtime: FileTime, - #[serde(serialize_with = "serialize_file_time")] + #[serde(with = "serde_file_time")] max_mtime: FileTime, }, - /// A dependency was stale. - StaleDepFingerprint { name: InternedString }, + /// A dependency's fingerprint was stale. + StaleDepFingerprint { unit: u64 }, /// This unit is up-to-date. All outputs and their corresponding mtime are /// listed in the payload here for other dependencies to compare against. @@ -713,14 +717,31 @@ impl FsStatus { } } -/// Serialize FileTime as milliseconds with nano. -fn serialize_file_time(ft: &FileTime, s: S) -> Result -where - S: serde::Serializer, -{ - let secs_as_millis = ft.unix_seconds() as f64 * 1000.0; - let nanos_as_millis = ft.nanoseconds() as f64 / 1_000_000.0; - (secs_as_millis + nanos_as_millis).serialize(s) +mod serde_file_time { + use filetime::FileTime; + use serde::Deserialize; + use serde::Serialize; + + /// Serialize FileTime as milliseconds with nano. + pub(super) fn serialize(ft: &FileTime, s: S) -> Result + where + S: serde::Serializer, + { + let secs_as_millis = ft.unix_seconds() as f64 * 1000.0; + let nanos_as_millis = ft.nanoseconds() as f64 / 1_000_000.0; + (secs_as_millis + nanos_as_millis).serialize(s) + } + + /// Deserialize FileTime from milliseconds with nano. + pub(super) fn deserialize<'de, D>(d: D) -> Result + where + D: serde::Deserializer<'de>, + { + let millis = f64::deserialize(d)?; + let secs = (millis / 1000.0) as i64; + let nanos = ((millis % 1000.0) * 1_000_000.0) as u32; + Ok(FileTime::from_unix_time(secs, nanos)) + } } impl Serialize for DepFingerprint { @@ -823,7 +844,7 @@ enum LocalFingerprint { } /// See [`FsStatus::StaleItem`]. -#[derive(Clone, Debug, Serialize)] +#[derive(Clone, Debug, Serialize, Deserialize)] #[serde(tag = "stale_item", rename_all = "kebab-case")] pub enum StaleItem { MissingFile { @@ -842,10 +863,10 @@ pub enum StaleItem { }, ChangedFile { reference: PathBuf, - #[serde(serialize_with = "serialize_file_time")] + #[serde(with = "serde_file_time")] reference_mtime: FileTime, stale: PathBuf, - #[serde(serialize_with = "serialize_file_time")] + #[serde(with = "serde_file_time")] stale_mtime: FileTime, }, ChangedChecksum { @@ -1011,6 +1032,7 @@ impl Fingerprint { rustflags: Vec::new(), config: 0, compile_kind: 0, + index: 0, fs_status: FsStatus::Stale, outputs: Vec::new(), } @@ -1161,8 +1183,8 @@ impl Fingerprint { } (a, b) => { return DirtyReason::LocalFingerprintTypeChanged { - old: b.kind(), - new: a.kind(), + old: b.kind().to_owned(), + new: a.kind().to_owned(), }; } } @@ -1184,10 +1206,7 @@ impl Fingerprint { if a.fingerprint.hash_u64() != b.fingerprint.hash_u64() { return DirtyReason::UnitDependencyInfoChanged { - new_name: a.name, - new_fingerprint: a.fingerprint.hash_u64(), - old_name: b.name, - old_fingerprint: b.fingerprint.hash_u64(), + unit: a.fingerprint.index, }; } } @@ -1263,7 +1282,9 @@ impl Fingerprint { | FsStatus::StaleItem(_) | FsStatus::StaleDependency { .. } | FsStatus::StaleDepFingerprint { .. } => { - self.fs_status = FsStatus::StaleDepFingerprint { name: dep.name }; + self.fs_status = FsStatus::StaleDepFingerprint { + unit: dep.fingerprint.index, + }; return Ok(()); } }; @@ -1305,7 +1326,7 @@ impl Fingerprint { ); self.fs_status = FsStatus::StaleDependency { - name: dep.name, + unit: dep.fingerprint.index, dep_mtime: *dep_mtime, max_mtime: *max_mtime, }; @@ -1637,6 +1658,7 @@ fn calculate_normal( memoized_hash: Mutex::new(None), config: Hasher::finish(&config), compile_kind, + index: build_runner.bcx.unit_to_index[unit], rustflags: extra_flags, fs_status: FsStatus::Stale, outputs, @@ -1703,6 +1725,7 @@ See https://doc.rust-lang.org/cargo/reference/build-scripts.html#rerun-if-change deps, outputs: if overridden { Vec::new() } else { vec![output] }, rustflags, + index: build_runner.bcx.unit_to_index[unit], // Most of the other info is blank here as we don't really include it // in the execution of the build script, but... this may be a latent diff --git a/src/cargo/core/compiler/job_queue/mod.rs b/src/cargo/core/compiler/job_queue/mod.rs index fdaceac904a..265646e07ba 100644 --- a/src/cargo/core/compiler/job_queue/mod.rs +++ b/src/cargo/core/compiler/job_queue/mod.rs @@ -182,6 +182,9 @@ struct DrainState<'gctx> { next_id: u32, timings: Timings<'gctx>, + /// Map from unit index to unit, for looking up dependency information. + index_to_unit: HashMap, + /// Tokens that are currently owned by this Cargo, and may be "associated" /// with a rustc process. They may also be unused, though if so will be /// dropped on the next loop iteration. @@ -495,6 +498,12 @@ impl<'gctx> JobQueue<'gctx> { progress, next_id: 0, timings: self.timings, + index_to_unit: build_runner + .bcx + .unit_to_index + .iter() + .map(|(unit, &index)| (index, unit.clone())) + .collect(), tokens: Vec::new(), pending_queue: Vec::new(), print: DiagnosticPrinter::new( @@ -1159,8 +1168,9 @@ impl<'gctx> DrainState<'gctx> { // being a compiled package. Dirty(dirty_reason) => { if !dirty_reason.is_fresh_build() { - gctx.shell() - .verbose(|shell| dirty_reason.present_to(shell, unit, ws_root))?; + gctx.shell().verbose(|shell| { + dirty_reason.present_to(shell, unit, ws_root, &self.index_to_unit) + })?; } if unit.mode.is_doc() { diff --git a/src/cargo/core/compiler/timings/mod.rs b/src/cargo/core/compiler/timings/mod.rs index 1d8331182b1..198f3350c03 100644 --- a/src/cargo/core/compiler/timings/mod.rs +++ b/src/cargo/core/compiler/timings/mod.rs @@ -196,9 +196,10 @@ impl<'gctx> Timings<'gctx> { if !self.enabled { return; } - let mut target = if unit.target.is_lib() && unit.mode == CompileMode::Build { - // Special case for brevity, since most dependencies hit - // this path. + 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()) diff --git a/src/cargo/ops/cargo_report/mod.rs b/src/cargo/ops/cargo_report/mod.rs index d265418f3c1..8d26055f9bb 100644 --- a/src/cargo/ops/cargo_report/mod.rs +++ b/src/cargo/ops/cargo_report/mod.rs @@ -1,3 +1,4 @@ +pub mod rebuilds; pub mod sessions; pub mod timings; pub mod util; diff --git a/src/cargo/ops/cargo_report/rebuilds.rs b/src/cargo/ops/cargo_report/rebuilds.rs new file mode 100644 index 00000000000..ae0c3a66e96 --- /dev/null +++ b/src/cargo/ops/cargo_report/rebuilds.rs @@ -0,0 +1,518 @@ +//! The `cargo report rebuilds` command. + +use std::collections::{HashMap, HashSet}; +use std::fs::File; +use std::io::BufReader; +use std::path::Path; + +use annotate_snippets::Group; +use annotate_snippets::Level; +use anyhow::Context as _; +use cargo_util_schemas::core::PackageIdSpec; +use itertools::Itertools as _; + +use crate::AlreadyPrintedError; +use crate::CargoResult; +use crate::GlobalContext; +use crate::core::Workspace; +use crate::core::compiler::CompileMode; +use crate::core::compiler::fingerprint::DirtyReason; +use crate::core::compiler::fingerprint::FsStatus; +use crate::core::compiler::fingerprint::StaleItem; +use crate::ops::cargo_report::util::list_log_files; +use crate::ops::cargo_report::util::unit_target_description; +use crate::util::log_message::FingerprintStatus; +use crate::util::log_message::LogMessage; +use crate::util::log_message::Target; +use crate::util::logger::RunId; +use crate::util::style; + +const DEFAULT_DISPLAY_LIMIT: usize = 5; + +pub struct ReportRebuildsOptions {} + +pub fn report_rebuilds( + gctx: &GlobalContext, + ws: Option<&Workspace<'_>>, + _opts: ReportRebuildsOptions, +) -> CargoResult<()> { + let Some((log, run_id)) = list_log_files(gctx, ws)?.next() else { + let context = if let Some(ws) = ws { + format!(" for workspace at `{}`", ws.root().display()) + } else { + String::new() + }; + let title = format!("no sessions found{context}"); + let note = "run command with `-Z build-analysis` to generate log files"; + let report = [Level::ERROR + .primary_title(title) + .element(Level::NOTE.message(note))]; + gctx.shell().print_report(&report, false)?; + return Err(AlreadyPrintedError::new(anyhow::anyhow!("")).into()); + }; + + let ctx = prepare_context(&log) + .with_context(|| format!("failed to analyze log at `{}`", log.display()))?; + let ws_root = ws.map(|ws| ws.root()).unwrap_or(gctx.cwd()); + + display_report(gctx, ctx, &run_id, ws_root)?; + + Ok(()) +} + +struct Context { + root_rebuilds: Vec, + units: HashMap, + total_cached: usize, + total_new: usize, + total_rebuilt: usize, +} + +struct UnitInfo { + package_id: PackageIdSpec, + target: Target, + mode: CompileMode, +} + +struct RootRebuild { + unit_index: u64, + reason: DirtyReason, + affected_units: Vec, +} + +fn prepare_context(log: &Path) -> CargoResult { + let reader = BufReader::new(File::open(log)?); + + let mut units: HashMap = HashMap::new(); + let mut dependencies: HashMap> = HashMap::new(); + let mut dirty_reasons: HashMap = HashMap::new(); + let mut total_cached = 0; + let mut total_new = 0; + let mut total_rebuilt = 0; + + for (log_index, result) in serde_json::Deserializer::from_reader(reader) + .into_iter::() + .enumerate() + { + let msg = match result { + Ok(msg) => msg, + Err(e) => { + tracing::warn!("failed to parse log message at index {log_index}: {e}"); + continue; + } + }; + + match msg { + LogMessage::UnitRegistered { + package_id, + target, + mode, + index, + dependencies: deps, + .. + } => { + units.insert( + index, + UnitInfo { + package_id, + target, + mode, + }, + ); + dependencies.insert(index, deps); + } + LogMessage::UnitFingerprint { + index, + status, + cause, + .. + } => { + if let Some(reason) = cause { + dirty_reasons.insert(index, reason); + } + match status { + FingerprintStatus::Fresh => { + total_cached += 1; + } + FingerprintStatus::Dirty => { + total_rebuilt += 1; + } + FingerprintStatus::New => { + total_new += 1; + dirty_reasons.insert(index, DirtyReason::FreshBuild); + } + } + } + _ => {} + } + } + + // reverse dependency graph (dependents of each unit) + let mut reverse_deps: HashMap> = HashMap::new(); + for (unit_id, deps) in &dependencies { + for dep_id in deps { + reverse_deps.entry(*dep_id).or_default().push(*unit_id); + } + } + + let rebuilt_units: HashSet = dirty_reasons.keys().copied().collect(); + + // Root rebuilds: units that rebuilt but none of their dependencies rebuilt + let root_rebuilds: Vec<_> = dirty_reasons + .iter() + .filter(|(unit_index, _)| { + let has_rebuilt_deps = dependencies + .get(unit_index) + .map(|deps| deps.iter().any(|dep| rebuilt_units.contains(dep))) + .unwrap_or_default(); + !has_rebuilt_deps + }) + .map(|(&unit_index, reason)| { + let affected_units = find_cascading_rebuilds(unit_index, &reverse_deps, &rebuilt_units); + RootRebuild { + unit_index, + reason: reason.clone(), + affected_units, + } + }) + .sorted_by(|a, b| { + b.affected_units + .len() + .cmp(&a.affected_units.len()) + .then_with(|| { + let a_name = units.get(&a.unit_index).map(|u| u.package_id.name()); + let b_name = units.get(&b.unit_index).map(|u| u.package_id.name()); + a_name.cmp(&b_name) + }) + }) + .collect(); + + Ok(Context { + root_rebuilds, + units, + total_cached, + total_new, + total_rebuilt, + }) +} + +/// Finds all units that were rebuilt as a cascading effect of the given root rebuild. +fn find_cascading_rebuilds( + root_rebuild: u64, + dependents: &HashMap>, + rebuilt_units: &HashSet, +) -> Vec { + let mut affected = Vec::new(); + let mut visited = HashSet::new(); + let mut queue = vec![root_rebuild]; + visited.insert(root_rebuild); + + while let Some(unit) = queue.pop() { + if let Some(deps) = dependents.get(&unit) { + for &dep in deps { + if !visited.contains(&dep) && rebuilt_units.contains(&dep) { + visited.insert(dep); + affected.push(dep); + queue.push(dep); + } + } + } + } + + affected.sort_unstable(); + affected +} + +fn display_report( + gctx: &GlobalContext, + ctx: Context, + run_id: &RunId, + ws_root: &Path, +) -> CargoResult<()> { + let verbose = gctx.shell().verbosity() == crate::core::shell::Verbosity::Verbose; + let extra_verbose = gctx.extra_verbose(); + + let Context { + root_rebuilds, + units, + total_cached, + total_new, + total_rebuilt, + } = ctx; + + let header = style::HEADER; + let subheader = style::LITERAL; + let mut shell = gctx.shell(); + let stderr = shell.err(); + + writeln!(stderr, "{header}Session:{header:#} {run_id}")?; + + // Render summary + let rebuilt_plural = plural(total_rebuilt); + + writeln!( + stderr, + "{header}Status:{header:#} {total_rebuilt} unit{rebuilt_plural} rebuilt, {total_cached} cached, {total_new} new" + )?; + writeln!(stderr)?; + + if total_rebuilt == 0 && total_new == 0 { + // Don't show detailed report if all units are cached. + return Ok(()); + } + + if total_rebuilt == 0 && total_cached == 0 { + // Don't show detailed report if all units are new build. + return Ok(()); + } + + // Render root rebuilds and cascading count + let root_rebuild_count = root_rebuilds.len(); + let cascading_count: usize = root_rebuilds.iter().map(|r| r.affected_units.len()).sum(); + + let root_plural = plural(root_rebuild_count); + let cascading_plural = plural(cascading_count); + + writeln!(stderr, "{header}Rebuild impact:{header:#}",)?; + writeln!( + stderr, + " root rebuilds: {root_rebuild_count} unit{root_plural}" + )?; + writeln!( + stderr, + " cascading: {cascading_count} unit{cascading_plural}" + )?; + writeln!(stderr)?; + + // Render each root rebuilds + let display_limit = if verbose { + root_rebuilds.len() + } else { + DEFAULT_DISPLAY_LIMIT.min(root_rebuilds.len()) + }; + let truncated_count = root_rebuilds.len().saturating_sub(display_limit); + + if truncated_count > 0 { + let count = root_rebuilds.len(); + writeln!( + stderr, + "{header}Root rebuilds:{header:#} (top {display_limit} of {count} by impact)", + )?; + } else { + writeln!(stderr, "{header}Root rebuilds:{header:#}",)?; + } + + for (idx, root_rebuild) in root_rebuilds.iter().take(display_limit).enumerate() { + let unit_desc = units + .get(&root_rebuild.unit_index) + .map(unit_description) + .expect("must have the unit"); + + let reason_str = format_dirty_reason(&root_rebuild.reason, &units, ws_root); + + writeln!( + stderr, + " {subheader}{idx}. {unit_desc}:{subheader:#} {reason_str}", + )?; + + if root_rebuild.affected_units.is_empty() { + writeln!(stderr, " impact: no cascading rebuilds")?; + } else { + let count = root_rebuild.affected_units.len(); + let plural = plural(count); + writeln!( + stderr, + " impact: {count} dependent unit{plural} rebuilt" + )?; + + if extra_verbose { + for affected in &root_rebuild.affected_units { + if let Some(affected) = units.get(affected) { + let desc = unit_description(affected); + writeln!(stderr, " - {desc}")?; + } + } + } + } + } + + // Render --verbose notes + drop(shell); + let has_cascading_rebuilds = root_rebuilds.iter().any(|rr| !rr.affected_units.is_empty()); + + if !verbose && truncated_count > 0 { + writeln!(gctx.shell().err())?; + let note = "pass `--verbose` to show all root rebuilds"; + gctx.shell().print_report( + &[Group::with_title(Level::NOTE.secondary_title(note))], + false, + )?; + } else if !extra_verbose && has_cascading_rebuilds { + writeln!(gctx.shell().err())?; + let note = "pass `-vv` to show all affected rebuilt unit lists"; + gctx.shell().print_report( + &[Group::with_title(Level::NOTE.secondary_title(note))], + false, + )?; + } + + Ok(()) +} + +fn unit_description(unit: &UnitInfo) -> String { + let name = unit.package_id.name(); + let version = unit + .package_id + .version() + .map(|v| v.to_string()) + .unwrap_or_else(|| "".into()); + let target = unit_target_description(&unit.target, unit.mode); + + let literal = style::LITERAL; + let nop = style::NOP; + + format!("{literal}{name}@{version}{literal:#}{nop}{target}{nop:#}") +} + +fn plural(len: usize) -> &'static str { + if len == 1 { "" } else { "s" } +} + +fn format_dirty_reason( + reason: &DirtyReason, + units: &HashMap, + ws_root: &Path, +) -> String { + match reason { + DirtyReason::RustcChanged => "toolchain changed".to_string(), + DirtyReason::FeaturesChanged { old, new } => { + format!("activated features changed: {old} -> {new}") + } + DirtyReason::DeclaredFeaturesChanged { old, new } => { + format!("declared features changed: {old} -> {new}") + } + DirtyReason::TargetConfigurationChanged => "target configuration changed".to_string(), + DirtyReason::PathToSourceChanged => "path to source changed".to_string(), + DirtyReason::ProfileConfigurationChanged => "profile configuration changed".to_string(), + DirtyReason::RustflagsChanged { old, new } => { + let old = old.join(", "); + let new = new.join(", "); + format!("rustflags changed: {old} -> {new}") + } + DirtyReason::ConfigSettingsChanged => "config settings changed".to_string(), + DirtyReason::CompileKindChanged => "compile target changed".to_string(), + DirtyReason::FsStatusOutdated(status) => match status { + FsStatus::Stale => "filesystem status stale".to_string(), + FsStatus::StaleItem(item) => match item { + StaleItem::MissingFile { path } => { + let path = path.strip_prefix(ws_root).unwrap_or(path).display(); + format!("file missing: {path}") + } + StaleItem::UnableToReadFile { path } => { + let path = path.strip_prefix(ws_root).unwrap_or(path).display(); + format!("unable to read file: {path}") + } + StaleItem::FailedToReadMetadata { path } => { + let path = path.strip_prefix(ws_root).unwrap_or(path).display(); + format!("failed to read file metadata: {path}") + } + StaleItem::FileSizeChanged { + path, + old_size: old, + new_size: new, + } => { + let path = path.strip_prefix(ws_root).unwrap_or(path).display(); + format!("file size changed: {path} ({old} -> {new} bytes)") + } + StaleItem::ChangedFile { stale, .. } => { + let path = stale.strip_prefix(ws_root).unwrap_or(stale).display(); + format!("file modified: {path}") + } + StaleItem::ChangedChecksum { + source, + stored_checksum: old, + new_checksum: new, + } => { + let path = source.strip_prefix(ws_root).unwrap_or(source).display(); + format!("file checksum changed: {path} ({old} -> {new})") + } + StaleItem::MissingChecksum { path } => { + let path = path.strip_prefix(ws_root).unwrap_or(path).display(); + format!("checksum missing: {path}") + } + StaleItem::ChangedEnv { + var, + previous, + current, + } => { + let old = previous.as_deref().unwrap_or(""); + let new = current.as_deref().unwrap_or(""); + format!("environment variable changed ({var}): {old} -> {new}") + } + }, + FsStatus::StaleDepFingerprint { unit } => units + .get(unit) + .map(|u| format!("dependency rebuilt: {}", unit_description(u))) + .unwrap_or_else(|| format!("dependency rebuilt: unit {unit}")), + FsStatus::StaleDependency { unit, .. } => units + .get(unit) + .map(|u| format!("dependency rebuilt: {}", unit_description(u))) + .unwrap_or_else(|| format!("dependency rebuilt: unit {unit}")), + FsStatus::UpToDate { .. } => "up to date".to_string(), + }, + DirtyReason::EnvVarChanged { + name, + old_value, + new_value, + } => { + let old = old_value.as_deref().unwrap_or(""); + let new = new_value.as_deref().unwrap_or(""); + format!("environment variable changed ({name}): {old} -> {new}") + } + DirtyReason::EnvVarsChanged { old, new } => { + format!("environment variables changed: {old} -> {new}") + } + DirtyReason::LocalFingerprintTypeChanged { old, new } => { + format!("local fingerprint type changed: {old} -> {new}") + } + DirtyReason::NumberOfDependenciesChanged { old, new } => { + format!("number of dependencies changed: {old} -> {new}") + } + DirtyReason::UnitDependencyNameChanged { old, new } => { + format!("dependency name changed: {old} -> {new}") + } + DirtyReason::UnitDependencyInfoChanged { unit } => units + .get(unit) + .map(|u| format!("dependency info changed: {}", unit_description(u))) + .unwrap_or_else(|| "dependency info changed".to_string()), + DirtyReason::LocalLengthsChanged => "local lengths changed".to_string(), + DirtyReason::PrecalculatedComponentsChanged { old, new } => { + format!("precalculated components changed: {old} -> {new}") + } + DirtyReason::ChecksumUseChanged { old } => { + if *old { + "checksum use changed: enabled -> disabled".to_string() + } else { + "checksum use changed: disabled -> enabled".to_string() + } + } + DirtyReason::DepInfoOutputChanged { old, new } => { + let old = old.strip_prefix(ws_root).unwrap_or(old).display(); + let new = new.strip_prefix(ws_root).unwrap_or(new).display(); + format!("dependency info output changed: {old} -> {new}") + } + DirtyReason::RerunIfChangedOutputFileChanged { old, new } => { + let old = old.strip_prefix(ws_root).unwrap_or(old).display(); + let new = new.strip_prefix(ws_root).unwrap_or(new).display(); + format!("rerun-if-changed output file changed: {old} -> {new}") + } + DirtyReason::RerunIfChangedOutputPathsChanged { old, new } => { + let old = old.len(); + let new = new.len(); + format!("rerun-if-changed paths changed: {old} path(s) -> {new} path(s)",) + } + DirtyReason::NothingObvious => "nothing obvious".to_string(), + DirtyReason::Forced => "forced rebuild".to_string(), + DirtyReason::FreshBuild => "fresh build".to_string(), + } +} diff --git a/src/cargo/ops/cargo_report/timings.rs b/src/cargo/ops/cargo_report/timings.rs index 5d649183813..b860683b8d1 100644 --- a/src/cargo/ops/cargo_report/timings.rs +++ b/src/cargo/ops/cargo_report/timings.rs @@ -18,7 +18,6 @@ use crate::AlreadyPrintedError; use crate::CargoResult; use crate::GlobalContext; use crate::core::Workspace; -use crate::core::compiler::CompileMode; use crate::core::compiler::timings::CompilationSection; use crate::core::compiler::timings::UnitData; use crate::core::compiler::timings::report::RenderContext; @@ -27,6 +26,7 @@ use crate::core::compiler::timings::report::compute_concurrency; use crate::core::compiler::timings::report::round_to_centisecond; use crate::core::compiler::timings::report::write_html; use crate::ops::cargo_report::util::list_log_files; +use crate::ops::cargo_report::util::unit_target_description; use crate::util::log_message::FingerprintStatus; use crate::util::log_message::LogMessage; use crate::util::log_message::Target; @@ -189,28 +189,7 @@ fn prepare_context(log: &Path, run_id: &RunId) -> CargoResult target_str.push_str(" (test)"), - CompileMode::Build => {} - CompileMode::Check { test: true } => target_str.push_str(" (check-test)"), - CompileMode::Check { test: false } => target_str.push_str(" (check)"), - CompileMode::Doc { .. } => target_str.push_str(" (doc)"), - CompileMode::Doctest => target_str.push_str(" (doc test)"), - CompileMode::Docscrape => target_str.push_str(" (doc scrape)"), - CompileMode::RunCustomBuild => target_str.push_str(" (run)"), - } + let target_str = unit_target_description(&target, mode); let mode_str = if mode.is_run_custom_build() { "run-custom-build" diff --git a/src/cargo/ops/cargo_report/util.rs b/src/cargo/ops/cargo_report/util.rs index 0ebfe716df0..682eac2aa30 100644 --- a/src/cargo/ops/cargo_report/util.rs +++ b/src/cargo/ops/cargo_report/util.rs @@ -6,7 +6,9 @@ use std::path::PathBuf; use crate::CargoResult; use crate::GlobalContext; use crate::core::Workspace; +use crate::core::compiler::CompileMode; use crate::util::BuildLogger; +use crate::util::log_message::Target; use crate::util::logger::RunId; /// Lists log files by calling a callback for each valid log file. @@ -57,3 +59,31 @@ pub fn list_log_files( Ok(Box::new(walk)) } + +pub fn unit_target_description(target: &Target, mode: CompileMode) -> String { + // This is pretty similar to how the current `core::compiler::timings` + // renders `core::manifest::Target`. However, our target is + // a simplified type so we cannot reuse the same logic here. + let mut target_str = + if target.kind == "lib" && matches!(mode, CompileMode::Build | CompileMode::Check { .. }) { + // Special case for brevity, since most dependencies hit this path. + "".to_string() + } else if target.kind == "build-script" { + " build-script".to_string() + } else { + format!(r#" {} "{}""#, target.name, target.kind) + }; + + match mode { + CompileMode::Test => target_str.push_str(" (test)"), + CompileMode::Build => {} + CompileMode::Check { test: true } => target_str.push_str(" (check-test)"), + CompileMode::Check { test: false } => target_str.push_str(" (check)"), + CompileMode::Doc { .. } => target_str.push_str(" (doc)"), + CompileMode::Doctest => target_str.push_str(" (doc test)"), + CompileMode::Docscrape => target_str.push_str(" (doc scrape)"), + CompileMode::RunCustomBuild => target_str.push_str(" (run)"), + } + + target_str +} diff --git a/src/cargo/ops/mod.rs b/src/cargo/ops/mod.rs index 84e557760c7..b56aff448ed 100644 --- a/src/cargo/ops/mod.rs +++ b/src/cargo/ops/mod.rs @@ -17,6 +17,8 @@ pub use self::cargo_package::check_yanked; pub use self::cargo_package::package; pub use self::cargo_pkgid::pkgid; pub use self::cargo_read_manifest::read_package; +pub use self::cargo_report::rebuilds::ReportRebuildsOptions; +pub use self::cargo_report::rebuilds::report_rebuilds; pub use self::cargo_report::sessions::ReportSessionsOptions; pub use self::cargo_report::sessions::report_sessions; pub use self::cargo_report::timings::ReportTimingsOptions; diff --git a/src/cargo/util/log_message.rs b/src/cargo/util/log_message.rs index d742de06026..c354bb740d7 100644 --- a/src/cargo/util/log_message.rs +++ b/src/cargo/util/log_message.rs @@ -146,7 +146,7 @@ pub enum LogMessage { /// Status of the rebuild detection fingerprint of this unit status: FingerprintStatus, /// Reason why the unit is dirty and needs rebuilding. - #[serde(default, skip_deserializing, skip_serializing_if = "Option::is_none")] + #[serde(default, skip_serializing_if = "Option::is_none")] cause: Option, }, } diff --git a/tests/testsuite/artifact_dep.rs b/tests/testsuite/artifact_dep.rs index 1d893b88e3b..1b35ea850fe 100644 --- a/tests/testsuite/artifact_dep.rs +++ b/tests/testsuite/artifact_dep.rs @@ -2708,7 +2708,7 @@ fn calc_bin_artifact_fingerprint() { [DIRTY] bar v0.5.0 ([ROOT]/foo/bar): the file `bar/src/main.rs` has changed ([..]) [COMPILING] bar v0.5.0 ([ROOT]/foo/bar) [RUNNING] `rustc --crate-name bar [..]` -[DIRTY] foo v0.1.0 ([ROOT]/foo): the dependency bar was rebuilt +[DIRTY] foo v0.1.0 ([ROOT]/foo): the dependency `bar` was rebuilt [CHECKING] foo v0.1.0 ([ROOT]/foo) [RUNNING] `rustc --crate-name foo [..]` [FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s diff --git a/tests/testsuite/cargo_report/help/stdout.term.svg b/tests/testsuite/cargo_report/help/stdout.term.svg index c611c9d62b3..b423e7a967b 100644 --- a/tests/testsuite/cargo_report/help/stdout.term.svg +++ b/tests/testsuite/cargo_report/help/stdout.term.svg @@ -1,4 +1,4 @@ - +