1use 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 pub open_result: bool,
39 pub gctx: &'gctx GlobalContext,
40 pub id: Option<RunId>,
41}
42
43struct 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 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 command: _,
163 cwd: _,
164 host,
165 jobs,
166 num_cpus,
167 profile,
168 rustc_version,
169 rustc_version_verbose,
170 target_dir: _,
171 workspace_root: _,
172 } => {
173 let rustc_version = rustc_version_verbose
174 .lines()
175 .next()
176 .map(ToOwned::to_owned)
177 .unwrap_or(rustc_version);
178 ctx.host = host;
179 ctx.jobs = jobs;
180 ctx.num_cpus = num_cpus;
181 ctx.profile = profile;
182 ctx.rustc_version = rustc_version;
183 }
184 LogMessage::UnitRegistered {
185 package_id,
186 target,
187 mode,
188 platform,
189 index,
190 features,
191 requested,
192 dependencies: _,
193 } => {
194 if requested {
195 requested_units.insert(index);
196 }
197 platform_targets.insert(platform);
198
199 let version = package_id
200 .version()
201 .map(|v| v.to_string())
202 .unwrap_or_else(|| "N/A".into());
203
204 let target_str = unit_target_description(&target, mode);
205
206 let mode_str = if mode.is_run_custom_build() {
207 "run-custom-build"
208 } else {
209 "todo"
210 };
211
212 let data = UnitData {
213 i: index,
214 name: package_id.name().to_string(),
215 version,
216 mode: mode_str.to_owned(),
217 target: target_str,
218 features,
219 start: 0.0,
220 duration: 0.0,
221 unblocked_units: Vec::new(),
222 unblocked_rmeta_units: Vec::new(),
223 sections: None,
224 };
225
226 units.insert(
227 index,
228 UnitEntry {
229 target,
230 data,
231 sections: IndexMap::new(),
232 rmeta_time: None,
233 },
234 );
235 }
236 LogMessage::UnitFingerprint { status, .. } => match status {
237 FingerprintStatus::New => ctx.total_dirty += 1,
238 FingerprintStatus::Dirty => ctx.total_dirty += 1,
239 FingerprintStatus::Fresh => ctx.total_fresh += 1,
240 },
241 LogMessage::UnitStarted { index, elapsed } => {
242 units
243 .entry(index)
244 .and_modify(|unit| unit.data.start = elapsed)
245 .or_insert_with(|| {
246 unreachable!("unit {index} must have been registered first")
247 });
248 }
249 LogMessage::UnitRmetaFinished {
250 index,
251 elapsed,
252 unblocked,
253 } => match units.entry(index) {
254 Entry::Occupied(mut e) => {
255 let elapsed = f64::max(elapsed - e.get().data.start, 0.0);
256 e.get_mut().data.unblocked_rmeta_units = unblocked;
257 e.get_mut().data.duration = elapsed;
258 e.get_mut().rmeta_time = Some(elapsed);
259 }
260 Entry::Vacant(_) => {
261 tracing::warn!(
262 "section `frontend` ended, but unit {index} has no start recorded"
263 )
264 }
265 },
266 LogMessage::UnitSectionStarted {
267 index,
268 elapsed,
269 section,
270 } => match units.entry(index) {
271 Entry::Occupied(mut e) => {
272 let elapsed = f64::max(elapsed - e.get().data.start, 0.0);
273 if e.get_mut()
274 .sections
275 .insert(
276 section.clone(),
277 CompilationSection {
278 start: elapsed,
279 end: None,
280 },
281 )
282 .is_some()
283 {
284 tracing::warn!(
285 "section `{section}` for unit {index} started more than once",
286 );
287 }
288 }
289 Entry::Vacant(_) => {
290 tracing::warn!(
291 "section `{section}` started, but unit {index} has no start recorded"
292 )
293 }
294 },
295 LogMessage::UnitSectionFinished {
296 index,
297 elapsed,
298 section,
299 } => match units.entry(index) {
300 Entry::Occupied(mut e) => {
301 let elapsed = f64::max(elapsed - e.get().data.start, 0.0);
302 if let Some(section) = e.get_mut().sections.get_mut(§ion) {
303 section.end = Some(elapsed);
304 } else {
305 tracing::warn!(
306 "section `{section}` for unit {index} ended, but section `{section}` has no start recorded"
307 );
308 }
309 }
310 Entry::Vacant(_) => {
311 tracing::warn!(
312 "section `{section}` ended, but unit {index} has no start recorded"
313 )
314 }
315 },
316 LogMessage::UnitFinished {
317 index,
318 elapsed,
319 unblocked,
320 } => match units.entry(index) {
321 Entry::Occupied(mut e) => {
322 let elapsed = f64::max(elapsed - e.get().data.start, 0.0);
323 e.get_mut().data.duration = elapsed;
324 e.get_mut().data.unblocked_units = unblocked;
325 }
326 Entry::Vacant(_) => {
327 tracing::warn!("unit {index} ended, but it has no start recorded");
328 }
329 },
330 _ => {} }
332 }
333
334 ctx.root_units = {
335 let mut root_map: IndexMap<_, Vec<_>> = IndexMap::new();
336 for index in requested_units {
337 let unit = &units[&index];
338 let target_desc = if unit.target.kind == "lib" {
340 "lib".to_owned()
341 } else if unit.target.kind == "build-script" {
342 "build script".to_owned()
343 } else {
344 format!(r#" {} "{}""#, unit.target.name, unit.target.kind)
345 };
346 root_map.entry(index).or_default().push(target_desc);
347 }
348 root_map
349 .into_iter()
350 .sorted_by_key(|(i, _)| *i)
351 .map(|(index, targets)| {
352 let unit = &units[&index];
353 let pkg_desc = format!("{} {}", unit.data.name, unit.data.version);
354 (pkg_desc, targets)
355 })
356 .collect()
357 };
358
359 let unit_data: Vec<_> = units
360 .into_values()
361 .map(
362 |UnitEntry {
363 target: _,
364 mut data,
365 sections,
366 rmeta_time,
367 }| {
368 data.sections = aggregate_sections(sections, data.duration, rmeta_time);
370 data.start = round_to_centisecond(data.start);
371 data.duration = round_to_centisecond(data.duration);
372 data
373 },
374 )
375 .sorted_unstable_by(|a, b| a.start.partial_cmp(&b.start).unwrap())
376 .collect();
377
378 if unit_data.is_empty() {
379 anyhow::bail!("no timing data found in log");
380 }
381
382 ctx.unit_data = unit_data;
383 ctx.concurrency = compute_concurrency(&ctx.unit_data);
384 ctx.requested_targets = platform_targets.into_iter().sorted_unstable().collect();
385
386 Ok(ctx)
387}