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