Skip to main content

cargo/ops/cargo_report/
timings.rs

1//! The `cargo report timings` command.
2
3use std::collections::HashSet;
4use std::fs::File;
5use std::io::BufReader;
6use std::path::PathBuf;
7
8use annotate_snippets::Level;
9use anyhow::Context as _;
10use cargo_util::paths;
11use indexmap::IndexMap;
12use indexmap::map::Entry;
13use itertools::Itertools as _;
14use tempfile::TempDir;
15
16use crate::AlreadyPrintedError;
17use crate::CargoResult;
18use crate::GlobalContext;
19use crate::core::Workspace;
20use crate::core::compiler::UnitIndex;
21use crate::core::compiler::timings::CompilationSection;
22use crate::core::compiler::timings::UnitData;
23use crate::core::compiler::timings::report::RenderContext;
24use crate::core::compiler::timings::report::aggregate_sections;
25use crate::core::compiler::timings::report::compute_concurrency;
26use crate::core::compiler::timings::report::round_to_centisecond;
27use crate::core::compiler::timings::report::write_html;
28use crate::ops::cargo_report::util::find_log_file;
29use crate::ops::cargo_report::util::unit_target_description;
30use crate::util::log_message::FingerprintStatus;
31use crate::util::log_message::LogMessage;
32use crate::util::log_message::Target;
33use crate::util::logger::RunId;
34use crate::util::style;
35
36pub struct ReportTimingsOptions<'gctx> {
37    /// Whether to attempt to open the browser after the report is generated
38    pub open_result: bool,
39    pub gctx: &'gctx GlobalContext,
40    pub id: Option<RunId>,
41}
42
43/// Collects sections data for later post-processing through [`aggregate_sections`].
44struct UnitEntry {
45    target: Target,
46    data: UnitData,
47    sections: IndexMap<String, CompilationSection>,
48    rmeta_time: Option<f64>,
49}
50
51pub fn report_timings(
52    gctx: &GlobalContext,
53    ws: Option<&Workspace<'_>>,
54    opts: ReportTimingsOptions<'_>,
55) -> CargoResult<()> {
56    let Some((log, run_id)) = find_log_file(gctx, ws, opts.id.as_ref())? else {
57        let context = if let Some(ws) = ws {
58            format!(" for workspace at `{}`", ws.root().display())
59        } else {
60            String::new()
61        };
62        let (title, note) = if let Some(id) = &opts.id {
63            (
64                format!("session `{id}` not found{context}"),
65                "run `cargo report sessions` to list available sessions",
66            )
67        } else {
68            (
69                format!("no sessions found{context}"),
70                "run command with `-Z build-analysis` to generate log files",
71            )
72        };
73        let report = [Level::ERROR
74            .primary_title(title)
75            .element(Level::NOTE.message(note))];
76        gctx.shell().print_report(&report, false)?;
77        return Err(AlreadyPrintedError::new(anyhow::anyhow!("")).into());
78    };
79
80    let reader = BufReader::new(File::open(&log)?);
81    let iter = serde_json::Deserializer::from_reader(reader)
82        .into_iter::<LogMessage>()
83        .enumerate()
84        .filter_map(|(idx, msg)| match msg {
85            Ok(msg) => Some(msg),
86            Err(e) => {
87                tracing::warn!("failed to parse log message at index {idx}: {e}");
88                None
89            }
90        });
91    let ctx = prepare_context(iter, &run_id)
92        .with_context(|| format!("failed to analyze log at `{}`", log.display()))?;
93
94    // If we are in a workspace,
95    // put timing reports in <target-dir>/cargo-timings/` as usual for easy access.
96    // Otherwise in a temporary directory
97    let reports_dir = if let Some(ws) = ws {
98        let target_dir = ws.target_dir();
99        let target_dir = target_dir.as_path_unlocked();
100        paths::create_dir_all_excluded_from_backups_atomic(target_dir)?;
101        let timings_dir = target_dir.join("cargo-timings");
102        paths::create_dir_all(&timings_dir)?;
103        timings_dir
104    } else if let Ok(path) = gctx.get_env("__CARGO_TEST_REPORT_TIMINGS_TEMPDIR") {
105        PathBuf::from(path.to_owned())
106    } else {
107        TempDir::with_prefix("cargo-timings-")?.keep()
108    };
109
110    let timing_path = reports_dir.join(format!("cargo-timing-{run_id}.html"));
111
112    let mut out_file = std::fs::OpenOptions::new()
113        .write(true)
114        .create(true)
115        .truncate(true)
116        .open(&timing_path)
117        .with_context(|| format!("failed to open `{}`", timing_path.display()))?;
118
119    write_html(ctx, &mut out_file)?;
120
121    let link = gctx.shell().err_file_hyperlink(&timing_path);
122    let msg = format!("report saved to {link}{}{link:#}", timing_path.display());
123    gctx.shell()
124        .status_with_color("Timing", msg, &style::NOTE)?;
125
126    if opts.open_result {
127        crate::util::open::open(&timing_path, gctx)?;
128    }
129
130    Ok(())
131}
132
133pub(crate) fn prepare_context<I>(log: I, run_id: &RunId) -> CargoResult<RenderContext<'_>>
134where
135    I: Iterator<Item = LogMessage>,
136{
137    let mut ctx = RenderContext {
138        start_str: run_id.timestamp().to_string(),
139        root_units: Default::default(),
140        profile: Default::default(),
141        total_fresh: Default::default(),
142        total_dirty: Default::default(),
143        unit_data: Default::default(),
144        concurrency: Default::default(),
145        cpu_usage: Default::default(),
146        rustc_version: Default::default(),
147        host: Default::default(),
148        requested_targets: Default::default(),
149        jobs: 0,
150        num_cpus: None,
151        error: &None,
152    };
153    let mut units: IndexMap<_, UnitEntry> = IndexMap::new();
154
155    let mut platform_targets = HashSet::new();
156
157    let mut requested_units: HashSet<UnitIndex> = HashSet::new();
158
159    for msg in log {
160        match msg {
161            LogMessage::BuildStarted {
162                cwd: _,
163                host,
164                jobs,
165                num_cpus,
166                profile,
167                rustc_version,
168                rustc_version_verbose,
169                target_dir: _,
170                workspace_root: _,
171            } => {
172                let rustc_version = rustc_version_verbose
173                    .lines()
174                    .next()
175                    .map(ToOwned::to_owned)
176                    .unwrap_or(rustc_version);
177                ctx.host = host;
178                ctx.jobs = jobs;
179                ctx.num_cpus = num_cpus;
180                ctx.profile = profile;
181                ctx.rustc_version = rustc_version;
182            }
183            LogMessage::UnitRegistered {
184                package_id,
185                target,
186                mode,
187                platform,
188                index,
189                features,
190                requested,
191                dependencies: _,
192            } => {
193                if requested {
194                    requested_units.insert(index);
195                }
196                platform_targets.insert(platform);
197
198                let version = package_id
199                    .version()
200                    .map(|v| v.to_string())
201                    .unwrap_or_else(|| "N/A".into());
202
203                let target_str = unit_target_description(&target, mode);
204
205                let mode_str = if mode.is_run_custom_build() {
206                    "run-custom-build"
207                } else {
208                    "todo"
209                };
210
211                let data = UnitData {
212                    i: index,
213                    name: package_id.name().to_string(),
214                    version,
215                    mode: mode_str.to_owned(),
216                    target: target_str,
217                    features,
218                    start: 0.0,
219                    duration: 0.0,
220                    unblocked_units: Vec::new(),
221                    unblocked_rmeta_units: Vec::new(),
222                    sections: None,
223                };
224
225                units.insert(
226                    index,
227                    UnitEntry {
228                        target,
229                        data,
230                        sections: IndexMap::new(),
231                        rmeta_time: None,
232                    },
233                );
234            }
235            LogMessage::UnitFingerprint { status, .. } => match status {
236                FingerprintStatus::New => ctx.total_dirty += 1,
237                FingerprintStatus::Dirty => ctx.total_dirty += 1,
238                FingerprintStatus::Fresh => ctx.total_fresh += 1,
239            },
240            LogMessage::UnitStarted { index, elapsed } => {
241                units
242                    .entry(index)
243                    .and_modify(|unit| unit.data.start = elapsed)
244                    .or_insert_with(|| {
245                        unreachable!("unit {index} must have been registered first")
246                    });
247            }
248            LogMessage::UnitRmetaFinished {
249                index,
250                elapsed,
251                unblocked,
252            } => match units.entry(index) {
253                Entry::Occupied(mut e) => {
254                    let elapsed = f64::max(elapsed - e.get().data.start, 0.0);
255                    e.get_mut().data.unblocked_rmeta_units = unblocked;
256                    e.get_mut().data.duration = elapsed;
257                    e.get_mut().rmeta_time = Some(elapsed);
258                }
259                Entry::Vacant(_) => {
260                    tracing::warn!(
261                        "section `frontend` ended, but unit {index} has no start recorded"
262                    )
263                }
264            },
265            LogMessage::UnitSectionStarted {
266                index,
267                elapsed,
268                section,
269            } => match units.entry(index) {
270                Entry::Occupied(mut e) => {
271                    let elapsed = f64::max(elapsed - e.get().data.start, 0.0);
272                    if e.get_mut()
273                        .sections
274                        .insert(
275                            section.clone(),
276                            CompilationSection {
277                                start: elapsed,
278                                end: None,
279                            },
280                        )
281                        .is_some()
282                    {
283                        tracing::warn!(
284                            "section `{section}` for unit {index} started more than once",
285                        );
286                    }
287                }
288                Entry::Vacant(_) => {
289                    tracing::warn!(
290                        "section `{section}` started, but unit {index} has no start recorded"
291                    )
292                }
293            },
294            LogMessage::UnitSectionFinished {
295                index,
296                elapsed,
297                section,
298            } => match units.entry(index) {
299                Entry::Occupied(mut e) => {
300                    let elapsed = f64::max(elapsed - e.get().data.start, 0.0);
301                    if let Some(section) = e.get_mut().sections.get_mut(&section) {
302                        section.end = Some(elapsed);
303                    } else {
304                        tracing::warn!(
305                            "section `{section}` for unit {index} ended, but section `{section}` has no start recorded"
306                        );
307                    }
308                }
309                Entry::Vacant(_) => {
310                    tracing::warn!(
311                        "section `{section}` ended, but unit {index} has no start recorded"
312                    )
313                }
314            },
315            LogMessage::UnitFinished {
316                index,
317                elapsed,
318                unblocked,
319            } => match units.entry(index) {
320                Entry::Occupied(mut e) => {
321                    let elapsed = f64::max(elapsed - e.get().data.start, 0.0);
322                    e.get_mut().data.duration = elapsed;
323                    e.get_mut().data.unblocked_units = unblocked;
324                }
325                Entry::Vacant(_) => {
326                    tracing::warn!("unit {index} ended, but it has no start recorded");
327                }
328            },
329            _ => {} // skip non-timing logs
330        }
331    }
332
333    ctx.root_units = {
334        let mut root_map: IndexMap<_, Vec<_>> = IndexMap::new();
335        for index in requested_units {
336            let unit = &units[&index];
337            // Pretty much like `core::Target::description_named`
338            let target_desc = if unit.target.kind == "lib" {
339                "lib".to_owned()
340            } else if unit.target.kind == "build-script" {
341                "build script".to_owned()
342            } else {
343                format!(r#" {} "{}""#, unit.target.name, unit.target.kind)
344            };
345            root_map.entry(index).or_default().push(target_desc);
346        }
347        root_map
348            .into_iter()
349            .sorted_by_key(|(i, _)| *i)
350            .map(|(index, targets)| {
351                let unit = &units[&index];
352                let pkg_desc = format!("{} {}", unit.data.name, unit.data.version);
353                (pkg_desc, targets)
354            })
355            .collect()
356    };
357
358    let unit_data: Vec<_> = units
359        .into_values()
360        .map(
361            |UnitEntry {
362                 target: _,
363                 mut data,
364                 sections,
365                 rmeta_time,
366             }| {
367                // Post-processing for compilation sections we've collected so far.
368                data.sections = aggregate_sections(sections, data.duration, rmeta_time);
369                data.start = round_to_centisecond(data.start);
370                data.duration = round_to_centisecond(data.duration);
371                data
372            },
373        )
374        .sorted_unstable_by(|a, b| a.start.partial_cmp(&b.start).unwrap())
375        .collect();
376
377    if unit_data.is_empty() {
378        anyhow::bail!("no timing data found in log");
379    }
380
381    ctx.unit_data = unit_data;
382    ctx.concurrency = compute_concurrency(&ctx.unit_data);
383    ctx.requested_targets = platform_targets.into_iter().sorted_unstable().collect();
384
385    Ok(ctx)
386}