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 peak_memory: None,
228 unblocked_units: Vec::new(),
229 unblocked_rmeta_units: Vec::new(),
230 sections: None,
231 };
232
233 units.insert(
234 index,
235 UnitEntry {
236 target,
237 data,
238 sections: IndexMap::default(),
239 rmeta_time: None,
240 started: false,
241 },
242 );
243 }
244 LogMessage::UnitFingerprint { status, .. } => match status {
245 FingerprintStatus::New => ctx.total_dirty += 1,
246 FingerprintStatus::Dirty => ctx.total_dirty += 1,
247 FingerprintStatus::Fresh => ctx.total_fresh += 1,
248 },
249 LogMessage::UnitStarted { index, elapsed } => {
250 units
251 .entry(index)
252 .and_modify(|unit| {
253 unit.data.start = elapsed;
254 unit.started = true;
255 })
256 .or_insert_with(|| {
257 unreachable!("unit {index} must have been registered first")
258 });
259 }
260 LogMessage::UnitRmetaFinished {
261 index,
262 elapsed,
263 unblocked,
264 } => match units.entry(index) {
265 Entry::Occupied(mut e) => {
266 let elapsed = f64::max(elapsed - e.get().data.start, 0.0);
267 e.get_mut().data.unblocked_rmeta_units = unblocked;
268 e.get_mut().data.duration = elapsed;
269 e.get_mut().rmeta_time = Some(elapsed);
270 }
271 Entry::Vacant(_) => {
272 tracing::warn!(
273 "section `frontend` ended, but unit {index} has no start recorded"
274 )
275 }
276 },
277 LogMessage::UnitSectionStarted {
278 index,
279 elapsed,
280 section,
281 } => match units.entry(index) {
282 Entry::Occupied(mut e) => {
283 let elapsed = f64::max(elapsed - e.get().data.start, 0.0);
284 if e.get_mut()
285 .sections
286 .insert(
287 section.clone(),
288 CompilationSection {
289 start: elapsed,
290 end: None,
291 },
292 )
293 .is_some()
294 {
295 tracing::warn!(
296 "section `{section}` for unit {index} started more than once",
297 );
298 }
299 }
300 Entry::Vacant(_) => {
301 tracing::warn!(
302 "section `{section}` started, but unit {index} has no start recorded"
303 )
304 }
305 },
306 LogMessage::UnitSectionFinished {
307 index,
308 elapsed,
309 section,
310 } => match units.entry(index) {
311 Entry::Occupied(mut e) => {
312 let elapsed = f64::max(elapsed - e.get().data.start, 0.0);
313 if let Some(section) = e.get_mut().sections.get_mut(§ion) {
314 section.end = Some(elapsed);
315 } else {
316 tracing::warn!(
317 "section `{section}` for unit {index} ended, but section `{section}` has no start recorded"
318 );
319 }
320 }
321 Entry::Vacant(_) => {
322 tracing::warn!(
323 "section `{section}` ended, but unit {index} has no start recorded"
324 )
325 }
326 },
327 LogMessage::UnitPeakMemory { index, bytes } => match units.entry(index) {
328 Entry::Occupied(mut e) => {
329 e.get_mut().data.peak_memory = Some(bytes);
330 }
331 Entry::Vacant(_) => {
332 tracing::warn!("peak memory reported, but unit {index} has no start recorded")
333 }
334 },
335 LogMessage::UnitFinished {
336 index,
337 elapsed,
338 unblocked,
339 } => match units.entry(index) {
340 Entry::Occupied(mut e) => {
341 let elapsed = f64::max(elapsed - e.get().data.start, 0.0);
342 e.get_mut().data.duration = elapsed;
343 e.get_mut().data.unblocked_units = unblocked;
344 }
345 Entry::Vacant(_) => {
346 tracing::warn!("unit {index} ended, but it has no start recorded");
347 }
348 },
349 _ => {} }
351 }
352
353 if error_if_no_units && units.is_empty() {
360 anyhow::bail!("no timing data found in log");
361 }
362
363 ctx.root_units = {
364 let mut root_map: IndexMap<_, Vec<_>> = IndexMap::default();
365 for index in requested_units {
366 let unit = &units[&index];
367 let target_desc = if unit.target.kind == "lib" {
369 "lib".to_owned()
370 } else if unit.target.kind == "build-script" {
371 "build script".to_owned()
372 } else {
373 format!(r#" {} "{}""#, unit.target.name, unit.target.kind)
374 };
375 root_map.entry(index).or_default().push(target_desc);
376 }
377 root_map
378 .into_iter()
379 .sorted_by_key(|(i, _)| *i)
380 .map(|(index, targets)| {
381 let unit = &units[&index];
382 let pkg_desc = format!("{} {}", unit.data.name, unit.data.version);
383 (pkg_desc, targets)
384 })
385 .collect()
386 };
387
388 let started: HashSet<UnitIndex> = units
389 .iter()
390 .filter(|(_, entry)| entry.started)
391 .map(|(index, _)| *index)
392 .collect();
393
394 let unit_data: Vec<_> = units
399 .into_values()
400 .filter(|entry| entry.started)
401 .map(
402 |UnitEntry {
403 target: _,
404 mut data,
405 sections,
406 rmeta_time,
407 started: _,
408 }| {
409 data.unblocked_units.retain(|index| started.contains(index));
413 data.unblocked_rmeta_units
414 .retain(|index| started.contains(index));
415
416 data.sections = aggregate_sections(sections, data.duration, rmeta_time);
418 data.start = round_to_centisecond(data.start);
419 data.duration = round_to_centisecond(data.duration);
420 data
421 },
422 )
423 .sorted_unstable_by(|a, b| a.start.partial_cmp(&b.start).unwrap())
424 .collect();
425
426 ctx.unit_data = unit_data;
427 ctx.concurrency = compute_concurrency(&ctx.unit_data);
428 ctx.requested_targets = platform_targets.into_iter().sorted_unstable().collect();
429
430 Ok(ctx)
431}