1use std::collections::HashSet;
4use std::fs::File;
5use std::io::BufReader;
6use std::path::Path;
7use std::path::PathBuf;
8
9use annotate_snippets::Level;
10use anyhow::Context as _;
11use cargo_util::paths;
12use indexmap::IndexMap;
13use indexmap::map::Entry;
14use itertools::Itertools as _;
15use tempfile::TempDir;
16
17use crate::AlreadyPrintedError;
18use crate::CargoResult;
19use crate::GlobalContext;
20use crate::core::Workspace;
21use crate::core::compiler::UnitIndex;
22use crate::core::compiler::timings::CompilationSection;
23use crate::core::compiler::timings::UnitData;
24use crate::core::compiler::timings::report::RenderContext;
25use crate::core::compiler::timings::report::aggregate_sections;
26use crate::core::compiler::timings::report::compute_concurrency;
27use crate::core::compiler::timings::report::round_to_centisecond;
28use crate::core::compiler::timings::report::write_html;
29use crate::ops::cargo_report::util::find_log_file;
30use crate::ops::cargo_report::util::unit_target_description;
31use crate::util::log_message::FingerprintStatus;
32use crate::util::log_message::LogMessage;
33use crate::util::log_message::Target;
34use crate::util::logger::RunId;
35use crate::util::style;
36
37pub struct ReportTimingsOptions<'gctx> {
38 pub open_result: bool,
40 pub gctx: &'gctx GlobalContext,
41 pub id: Option<RunId>,
42}
43
44struct UnitEntry {
46 target: Target,
47 data: UnitData,
48 sections: IndexMap<String, CompilationSection>,
49 rmeta_time: Option<f64>,
50}
51
52pub fn report_timings(
53 gctx: &GlobalContext,
54 ws: Option<&Workspace<'_>>,
55 opts: ReportTimingsOptions<'_>,
56) -> CargoResult<()> {
57 let Some((log, run_id)) = find_log_file(gctx, ws, opts.id.as_ref())? else {
58 let context = if let Some(ws) = ws {
59 format!(" for workspace at `{}`", ws.root().display())
60 } else {
61 String::new()
62 };
63 let (title, note) = if let Some(id) = &opts.id {
64 (
65 format!("session `{id}` not found{context}"),
66 "run `cargo report sessions` to list available sessions",
67 )
68 } else {
69 (
70 format!("no sessions found{context}"),
71 "run command with `-Z build-analysis` to generate log files",
72 )
73 };
74 let report = [Level::ERROR
75 .primary_title(title)
76 .element(Level::NOTE.message(note))];
77 gctx.shell().print_report(&report, false)?;
78 return Err(AlreadyPrintedError::new(anyhow::anyhow!("")).into());
79 };
80
81 let ctx = prepare_context(&log, &run_id)
82 .with_context(|| format!("failed to analyze log at `{}`", log.display()))?;
83
84 let reports_dir = if let Some(ws) = ws {
88 let target_dir = ws.target_dir();
89 let target_dir = target_dir.as_path_unlocked();
90 paths::create_dir_all_excluded_from_backups_atomic(target_dir)?;
91 let timings_dir = target_dir.join("cargo-timings");
92 paths::create_dir_all(&timings_dir)?;
93 timings_dir
94 } else if let Ok(path) = gctx.get_env("__CARGO_TEST_REPORT_TIMINGS_TEMPDIR") {
95 PathBuf::from(path.to_owned())
96 } else {
97 TempDir::with_prefix("cargo-timings-")?.keep()
98 };
99
100 let timing_path = reports_dir.join(format!("cargo-timing-{run_id}.html"));
101
102 let mut out_file = std::fs::OpenOptions::new()
103 .write(true)
104 .create(true)
105 .truncate(true)
106 .open(&timing_path)
107 .with_context(|| format!("failed to open `{}`", timing_path.display()))?;
108
109 write_html(ctx, &mut out_file)?;
110
111 let link = gctx.shell().err_file_hyperlink(&timing_path);
112 let msg = format!("report saved to {link}{}{link:#}", timing_path.display());
113 gctx.shell()
114 .status_with_color("Timing", msg, &style::NOTE)?;
115
116 if opts.open_result {
117 crate::util::open::open(&timing_path, gctx)?;
118 }
119
120 Ok(())
121}
122
123fn prepare_context(log: &Path, run_id: &RunId) -> CargoResult<RenderContext<'static>> {
124 let reader = BufReader::new(File::open(&log)?);
125
126 let mut ctx = RenderContext {
127 start_str: run_id.timestamp().to_string(),
128 root_units: Default::default(),
129 profile: Default::default(),
130 total_fresh: Default::default(),
131 total_dirty: Default::default(),
132 unit_data: Default::default(),
133 concurrency: Default::default(),
134 cpu_usage: Default::default(),
135 rustc_version: Default::default(),
136 host: Default::default(),
137 requested_targets: Default::default(),
138 jobs: 0,
139 num_cpus: None,
140 error: &None,
141 };
142 let mut units: IndexMap<_, UnitEntry> = IndexMap::new();
143
144 let mut platform_targets = HashSet::new();
145
146 let mut requested_units: HashSet<UnitIndex> = HashSet::new();
147
148 for (log_index, result) in serde_json::Deserializer::from_reader(reader)
149 .into_iter::<LogMessage>()
150 .enumerate()
151 {
152 let msg = match result {
153 Ok(msg) => msg,
154 Err(e) => {
155 tracing::warn!("failed to parse log message at index {log_index}: {e}");
156 continue;
157 }
158 };
159
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(§ion) {
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 _ => {} }
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 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 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}