cargo/core/compiler/timings/
mod.rs1pub mod report;
7
8use super::CompileMode;
9use super::Unit;
10use super::UnitIndex;
11use crate::core::PackageId;
12use crate::core::compiler::BuildContext;
13use crate::core::compiler::BuildRunner;
14use crate::core::compiler::job_queue::JobId;
15use crate::util::cpu::State;
16use crate::util::log_message::LogMessage;
17use crate::util::style;
18use crate::util::{CargoResult, GlobalContext};
19
20use cargo_util::paths;
21use indexmap::IndexMap;
22use std::collections::HashMap;
23use std::io::BufWriter;
24use std::time::{Duration, Instant};
25use tracing::warn;
26
27pub struct Timings<'gctx> {
35 gctx: &'gctx GlobalContext,
36 enabled: bool,
38 report_html: bool,
40 start: Instant,
42 start_str: String,
44 root_targets: Vec<(String, Vec<String>)>,
48 profile: String,
50 total_fresh: u32,
52 total_dirty: u32,
54 unit_to_index: HashMap<Unit, UnitIndex>,
56 unit_times: Vec<UnitTime>,
58 active: HashMap<JobId, UnitTime>,
61 last_cpu_state: Option<State>,
63 last_cpu_recording: Instant,
64 cpu_usage: Vec<(f64, f64)>,
68}
69
70#[derive(Copy, Clone, serde::Serialize)]
72pub struct CompilationSection {
73 pub start: f64,
75 pub end: Option<f64>,
77}
78
79struct UnitTime {
81 unit: Unit,
82 target: String,
84 start: f64,
86 duration: f64,
88 rmeta_time: Option<f64>,
91 unblocked_units: Vec<Unit>,
93 unblocked_rmeta_units: Vec<Unit>,
95 sections: IndexMap<String, CompilationSection>,
100}
101
102#[derive(serde::Serialize)]
106pub struct UnitData {
107 pub i: UnitIndex,
108 pub name: String,
109 pub version: String,
110 pub mode: String,
111 pub target: String,
112 pub features: Vec<String>,
113 pub start: f64,
114 pub duration: f64,
115 pub unblocked_units: Vec<UnitIndex>,
116 pub unblocked_rmeta_units: Vec<UnitIndex>,
117 pub sections: Option<Vec<(report::SectionName, report::SectionData)>>,
118}
119
120impl<'gctx> Timings<'gctx> {
121 pub fn new(bcx: &BuildContext<'_, 'gctx>, root_units: &[Unit]) -> Timings<'gctx> {
122 let start = bcx.gctx.creation_time();
123 let report_html = bcx.build_config.timing_report;
124 let enabled = report_html | bcx.logger.is_some();
125
126 if !enabled {
127 return Timings {
128 gctx: bcx.gctx,
129 enabled,
130 report_html,
131 start,
132 start_str: String::new(),
133 root_targets: Vec::new(),
134 profile: String::new(),
135 total_fresh: 0,
136 total_dirty: 0,
137 unit_to_index: HashMap::new(),
138 unit_times: Vec::new(),
139 active: HashMap::new(),
140 last_cpu_state: None,
141 last_cpu_recording: Instant::now(),
142 cpu_usage: Vec::new(),
143 };
144 }
145
146 let mut root_map: HashMap<PackageId, Vec<String>> = HashMap::new();
147 for unit in root_units {
148 let target_desc = unit.target.description_named();
149 root_map
150 .entry(unit.pkg.package_id())
151 .or_default()
152 .push(target_desc);
153 }
154 let root_targets = root_map
155 .into_iter()
156 .map(|(pkg_id, targets)| {
157 let pkg_desc = format!("{} {}", pkg_id.name(), pkg_id.version());
158 (pkg_desc, targets)
159 })
160 .collect();
161 let start_str = jiff::Timestamp::now().to_string();
162 let profile = bcx.build_config.requested_profile.to_string();
163 let last_cpu_state = match State::current() {
164 Ok(state) => Some(state),
165 Err(e) => {
166 tracing::info!("failed to get CPU state, CPU tracking disabled: {:?}", e);
167 None
168 }
169 };
170
171 Timings {
172 gctx: bcx.gctx,
173 enabled,
174 report_html,
175 start,
176 start_str,
177 root_targets,
178 profile,
179 total_fresh: 0,
180 total_dirty: 0,
181 unit_to_index: bcx.unit_to_index.clone(),
182 unit_times: Vec::new(),
183 active: HashMap::new(),
184 last_cpu_state,
185 last_cpu_recording: Instant::now(),
186 cpu_usage: Vec::new(),
187 }
188 }
189
190 pub fn unit_start(&mut self, build_runner: &BuildRunner<'_, '_>, id: JobId, unit: Unit) {
192 if !self.enabled {
193 return;
194 }
195 let mut target = if unit.target.is_lib()
196 && matches!(unit.mode, CompileMode::Build | CompileMode::Check { .. })
197 {
198 "".to_string()
200 } else {
201 format!(" {}", unit.target.description_named())
202 };
203 match unit.mode {
204 CompileMode::Test => target.push_str(" (test)"),
205 CompileMode::Build => {}
206 CompileMode::Check { test: true } => target.push_str(" (check-test)"),
207 CompileMode::Check { test: false } => target.push_str(" (check)"),
208 CompileMode::Doc { .. } => target.push_str(" (doc)"),
209 CompileMode::Doctest => target.push_str(" (doc test)"),
210 CompileMode::Docscrape => target.push_str(" (doc scrape)"),
211 CompileMode::RunCustomBuild => target.push_str(" (run)"),
212 }
213 let start = self.start.elapsed().as_secs_f64();
214 let unit_time = UnitTime {
215 unit,
216 target,
217 start,
218 duration: 0.0,
219 rmeta_time: None,
220 unblocked_units: Vec::new(),
221 unblocked_rmeta_units: Vec::new(),
222 sections: Default::default(),
223 };
224 if let Some(logger) = build_runner.bcx.logger {
225 logger.log(LogMessage::UnitStarted {
226 index: self.unit_to_index[&unit_time.unit],
227 elapsed: start,
228 });
229 }
230 assert!(self.active.insert(id, unit_time).is_none());
231 }
232
233 pub fn unit_rmeta_finished(
235 &mut self,
236 build_runner: &BuildRunner<'_, '_>,
237 id: JobId,
238 unblocked: Vec<&Unit>,
239 ) {
240 if !self.enabled {
241 return;
242 }
243 let Some(unit_time) = self.active.get_mut(&id) else {
247 return;
248 };
249 let elapsed = self.start.elapsed().as_secs_f64();
250 unit_time.rmeta_time = Some(elapsed - unit_time.start);
251 assert!(unit_time.unblocked_rmeta_units.is_empty());
252 unit_time
253 .unblocked_rmeta_units
254 .extend(unblocked.iter().cloned().cloned());
255
256 if let Some(logger) = build_runner.bcx.logger {
257 let unblocked = unblocked.iter().map(|u| self.unit_to_index[u]).collect();
258 logger.log(LogMessage::UnitRmetaFinished {
259 index: self.unit_to_index[&unit_time.unit],
260 elapsed,
261 unblocked,
262 });
263 }
264 }
265
266 pub fn unit_finished(
268 &mut self,
269 build_runner: &BuildRunner<'_, '_>,
270 id: JobId,
271 unblocked: Vec<&Unit>,
272 ) {
273 if !self.enabled {
274 return;
275 }
276 let Some(mut unit_time) = self.active.remove(&id) else {
278 return;
279 };
280 let elapsed = self.start.elapsed().as_secs_f64();
281 unit_time.duration = elapsed - unit_time.start;
282 assert!(unit_time.unblocked_units.is_empty());
283 unit_time
284 .unblocked_units
285 .extend(unblocked.iter().cloned().cloned());
286
287 if let Some(logger) = build_runner.bcx.logger {
288 let unblocked = unblocked.iter().map(|u| self.unit_to_index[u]).collect();
289 logger.log(LogMessage::UnitFinished {
290 index: self.unit_to_index[&unit_time.unit],
291 elapsed,
292 unblocked,
293 });
294 }
295 self.unit_times.push(unit_time);
296 }
297
298 pub fn unit_section_timing(
300 &mut self,
301 build_runner: &BuildRunner<'_, '_>,
302 id: JobId,
303 section_timing: &SectionTiming,
304 ) {
305 if !self.enabled {
306 return;
307 }
308 let Some(unit_time) = self.active.get_mut(&id) else {
309 return;
310 };
311 let elapsed = self.start.elapsed().as_secs_f64();
312
313 match section_timing.event {
314 SectionTimingEvent::Start => {
315 unit_time.start_section(§ion_timing.name, elapsed);
316 }
317 SectionTimingEvent::End => {
318 unit_time.end_section(§ion_timing.name, elapsed);
319 }
320 }
321
322 if let Some(logger) = build_runner.bcx.logger {
323 let index = self.unit_to_index[&unit_time.unit];
324 let section = section_timing.name.clone();
325 logger.log(match section_timing.event {
326 SectionTimingEvent::Start => LogMessage::UnitSectionStarted {
327 index,
328 elapsed,
329 section,
330 },
331 SectionTimingEvent::End => LogMessage::UnitSectionFinished {
332 index,
333 elapsed,
334 section,
335 },
336 })
337 }
338 }
339
340 pub fn add_fresh(&mut self) {
342 self.total_fresh += 1;
343 }
344
345 pub fn add_dirty(&mut self) {
347 self.total_dirty += 1;
348 }
349
350 pub fn record_cpu(&mut self) {
352 if !self.enabled {
353 return;
354 }
355 let Some(prev) = &mut self.last_cpu_state else {
356 return;
357 };
358 let now = Instant::now();
360 if self.last_cpu_recording.elapsed() < Duration::from_millis(100) {
361 return;
362 }
363 let current = match State::current() {
364 Ok(s) => s,
365 Err(e) => {
366 tracing::info!("failed to get CPU state: {:?}", e);
367 return;
368 }
369 };
370 let pct_idle = current.idle_since(prev);
371 *prev = current;
372 self.last_cpu_recording = now;
373 let dur = now.duration_since(self.start).as_secs_f64();
374 self.cpu_usage.push((dur, 100.0 - pct_idle));
375 }
376
377 pub fn finished(
379 &mut self,
380 build_runner: &BuildRunner<'_, '_>,
381 error: &Option<anyhow::Error>,
382 ) -> CargoResult<()> {
383 if !self.enabled {
384 return Ok(());
385 }
386 self.unit_times
387 .sort_unstable_by(|a, b| a.start.partial_cmp(&b.start).unwrap());
388 if self.report_html {
389 let timestamp = self.start_str.replace(&['-', ':'][..], "");
390 let timings_path = build_runner
391 .files()
392 .timings_dir()
393 .expect("artifact-dir was not locked");
394 paths::create_dir_all(&timings_path)?;
395 let filename = timings_path.join(format!("cargo-timing-{}.html", timestamp));
396 let mut f = BufWriter::new(paths::create(&filename)?);
397
398 let rustc_version = build_runner
399 .bcx
400 .rustc()
401 .verbose_version
402 .lines()
403 .next()
404 .expect("rustc version");
405 let requested_targets = build_runner
406 .bcx
407 .build_config
408 .requested_kinds
409 .iter()
410 .map(|kind| build_runner.bcx.target_data.short_name(kind).to_owned())
411 .collect::<Vec<_>>();
412 let num_cpus = std::thread::available_parallelism()
413 .ok()
414 .map(|x| x.get() as u64);
415
416 let unit_data = report::to_unit_data(&self.unit_times, &self.unit_to_index);
417 let concurrency = report::compute_concurrency(&unit_data);
418
419 let ctx = report::RenderContext {
420 start_str: self.start_str.clone(),
421 root_units: self.root_targets.clone(),
422 profile: self.profile.clone(),
423 total_fresh: self.total_fresh,
424 total_dirty: self.total_dirty,
425 unit_data,
426 concurrency,
427 cpu_usage: &self.cpu_usage,
428 rustc_version: rustc_version.into(),
429 host: build_runner.bcx.rustc().host.to_string(),
430 requested_targets,
431 jobs: build_runner.bcx.jobs(),
432 num_cpus,
433 error,
434 };
435 report::write_html(ctx, &mut f)?;
436
437 let unstamped_filename = timings_path.join("cargo-timing.html");
438 paths::link_or_copy(&filename, &unstamped_filename)?;
439
440 let mut shell = self.gctx.shell();
441 let timing_path = std::env::current_dir().unwrap_or_default().join(&filename);
442 let link = shell.err_file_hyperlink(&timing_path);
443 let msg = format!("report saved to {link}{}{link:#}", timing_path.display(),);
444 shell.status_with_color("Timing", msg, &style::NOTE)?;
445 }
446 Ok(())
447 }
448}
449
450impl UnitTime {
451 fn start_section(&mut self, name: &str, now: f64) {
452 if self
453 .sections
454 .insert(
455 name.to_string(),
456 CompilationSection {
457 start: now - self.start,
458 end: None,
459 },
460 )
461 .is_some()
462 {
463 warn!("compilation section {name} started more than once");
464 }
465 }
466
467 fn end_section(&mut self, name: &str, now: f64) {
468 let Some(section) = self.sections.get_mut(name) else {
469 warn!("compilation section {name} ended, but it has no start recorded");
470 return;
471 };
472 section.end = Some(now - self.start);
473 }
474}
475
476#[derive(serde::Deserialize, Debug)]
478#[serde(rename_all = "kebab-case")]
479pub enum SectionTimingEvent {
480 Start,
481 End,
482}
483
484#[derive(serde::Deserialize, Debug)]
487pub struct SectionTiming {
488 pub name: String,
489 pub event: SectionTimingEvent,
490}