Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 20 additions & 5 deletions src/bin/cargo/commands/report.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ pub fn cli() -> Command {
subcommand("timings")
.about("Reports the build timings of previous sessions (unstable)")
.arg_manifest_path()
.arg(flag("open", "Opens the timing report in a browser")),
.arg(flag("open", "Opens the timing report in a browser"))
.arg(opt("id", "Session ID to report on").value_name("ID")),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should also go first

)
.subcommand(
subcommand("sessions")
Expand All @@ -47,7 +48,8 @@ pub fn cli() -> Command {
.subcommand(
subcommand("rebuilds")
.about("Reports rebuild reasons from previous sessions (unstable)")
.arg_manifest_path(),
.arg_manifest_path()
.arg(opt("id", "Session ID to report on").value_name("ID")),
)
}

Expand Down Expand Up @@ -120,8 +122,16 @@ fn timings_opts<'a>(
args: &ArgMatches,
) -> CargoResult<ops::ReportTimingsOptions<'a>> {
let open_result = args.get_flag("open");
let id = args
.get_one::<String>("id")
.map(|s| s.parse())
.transpose()?;

Ok(ops::ReportTimingsOptions { open_result, gctx })
Ok(ops::ReportTimingsOptions {
open_result,
gctx,
id,
})
}

fn sessions_opts(args: &ArgMatches) -> CargoResult<ops::ReportSessionsOptions> {
Expand All @@ -131,6 +141,11 @@ fn sessions_opts(args: &ArgMatches) -> CargoResult<ops::ReportSessionsOptions> {
Ok(ops::ReportSessionsOptions { limit })
}

fn rebuilds_opts(_args: &ArgMatches) -> CargoResult<ops::ReportRebuildsOptions> {
Ok(ops::ReportRebuildsOptions {})
fn rebuilds_opts(args: &ArgMatches) -> CargoResult<ops::ReportRebuildsOptions> {
let id = args
.get_one::<String>("id")
.map(|s| s.parse())
.transpose()?;

Ok(ops::ReportRebuildsOptions { id })
}
23 changes: 17 additions & 6 deletions src/cargo/ops/cargo_report/rebuilds.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ use crate::core::compiler::UnitIndex;
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::find_log_file;
use crate::ops::cargo_report::util::unit_target_description;
use crate::util::log_message::FingerprintStatus;
use crate::util::log_message::LogMessage;
Expand All @@ -30,21 +30,32 @@ use crate::util::style;

const DEFAULT_DISPLAY_LIMIT: usize = 5;

pub struct ReportRebuildsOptions {}
pub struct ReportRebuildsOptions {
pub id: Option<RunId>,
}

pub fn report_rebuilds(
gctx: &GlobalContext,
ws: Option<&Workspace<'_>>,
_opts: ReportRebuildsOptions,
opts: ReportRebuildsOptions,
) -> CargoResult<()> {
let Some((log, run_id)) = list_log_files(gctx, ws)?.next() else {
let Some((log, run_id)) = find_log_file(gctx, ws, opts.id.as_ref())? 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 (title, note) = if let Some(id) = &opts.id {
(
format!("session `{id}` not found{context}"),
"run `cargo report sessions` to list available sessions",
)
} else {
(
format!("no sessions found{context}"),
"run command with `-Z build-analysis` to generate log files",
)
};
let report = [Level::ERROR
.primary_title(title)
.element(Level::NOTE.message(note))];
Expand Down
18 changes: 14 additions & 4 deletions src/cargo/ops/cargo_report/timings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ use crate::core::compiler::timings::report::aggregate_sections;
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::find_log_file;
use crate::ops::cargo_report::util::unit_target_description;
use crate::util::log_message::FingerprintStatus;
use crate::util::log_message::LogMessage;
Expand All @@ -38,6 +38,7 @@ pub struct ReportTimingsOptions<'gctx> {
/// Whether to attempt to open the browser after the report is generated
pub open_result: bool,
pub gctx: &'gctx GlobalContext,
pub id: Option<RunId>,
}

/// Collects sections data for later post-processing through [`aggregate_sections`].
Expand All @@ -53,14 +54,23 @@ pub fn report_timings(
ws: Option<&Workspace<'_>>,
opts: ReportTimingsOptions<'_>,
) -> CargoResult<()> {
let Some((log, run_id)) = list_log_files(gctx, ws)?.next() else {
let Some((log, run_id)) = find_log_file(gctx, ws, opts.id.as_ref())? 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 (title, note) = if let Some(id) = &opts.id {
(
format!("session `{id}` not found{context}"),
"run `cargo report sessions` to list available sessions",
)
} else {
(
format!("no sessions found{context}"),
"run command with `-Z build-analysis` to generate log files",
)
};
let report = [Level::ERROR
.primary_title(title)
.element(Level::NOTE.message(note))];
Expand Down
18 changes: 18 additions & 0 deletions src/cargo/ops/cargo_report/util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,24 @@ pub fn list_log_files(
Ok(Box::new(walk))
}

pub fn find_log_file(
gctx: &GlobalContext,
ws: Option<&Workspace<'_>>,
id: Option<&RunId>,
) -> CargoResult<Option<(std::path::PathBuf, RunId)>> {
match id {
Some(requested_id) => {
for (path, run_id) in list_log_files(gctx, ws)? {
if run_id.to_string() == requested_id.to_string() {
return Ok(Some((path, run_id)));
}
}
Ok(None)
}
None => Ok(list_log_files(gctx, ws)?.next()),
}
}

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
Expand Down
81 changes: 81 additions & 0 deletions tests/testsuite/cargo_report_rebuilds/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -563,3 +563,84 @@ Root rebuilds:
"#]])
.run();
}

#[cargo_test]
fn with_session_id() {
let p = project()
.file("Cargo.toml", &basic_manifest("foo", "0.0.0"))
.file("src/lib.rs", "")
.build();

// First session: fresh build (1 new unit)
p.cargo("check -Zbuild-analysis")
.env("CARGO_BUILD_ANALYSIS_ENABLED", "true")
.masquerade_as_nightly_cargo(&["build-analysis"])
.run();

let first_log = paths::log_file(0);
let first_session_id = first_log.file_stem().unwrap().to_str().unwrap();

p.change_file("src/lib.rs", "// touched");

// Second session: rebuild (1 unit rebuilt)
p.cargo("check -Zbuild-analysis")
.env("CARGO_BUILD_ANALYSIS_ENABLED", "true")
.masquerade_as_nightly_cargo(&["build-analysis"])
.run();

let _ = paths::log_file(1);

// With --id, should use the first session (not the most recent second)
p.cargo(&format!(
"report rebuilds --id {first_session_id} -Zbuild-analysis"
))
.masquerade_as_nightly_cargo(&["build-analysis"])
.with_stderr_data(str![[r#"
Session: [..]
Status: 0 units rebuilt, 0 cached, 1 new


"#]])
.run();
}

#[cargo_test]
fn session_id_not_found() {
let p = project()
.file("Cargo.toml", &basic_manifest("foo", "0.0.0"))
.file("src/lib.rs", "")
.build();

p.cargo("check -Zbuild-analysis")
.env("CARGO_BUILD_ANALYSIS_ENABLED", "true")
.masquerade_as_nightly_cargo(&["build-analysis"])
.run();

p.cargo("report rebuilds --id 20260101T000000000Z-0000000000000000 -Zbuild-analysis")
.masquerade_as_nightly_cargo(&["build-analysis"])
.with_status(101)
.with_stderr_data(str![[r#"
[ERROR] session `20260101T000000000Z-0000000000000000` not found for workspace at `[ROOT]/foo`
|
= [NOTE] run `cargo report sessions` to list available sessions

"#]])
.run();
}

#[cargo_test]
fn invalid_session_id_format() {
let p = project()
.file("Cargo.toml", &basic_manifest("foo", "0.0.0"))
.file("src/lib.rs", "")
.build();

p.cargo("report rebuilds --id invalid-session-id -Zbuild-analysis")
.masquerade_as_nightly_cargo(&["build-analysis"])
.with_status(101)
.with_stderr_data(str![[r#"
[ERROR] expect run ID in format `20060724T012128000Z-<16-char-hex>`, got `invalid-session-id`

"#]])
.run();
}
32 changes: 17 additions & 15 deletions tests/testsuite/cargo_report_timings/help/stdout.term.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading