cargo/core/compiler/timings/
mod.rs1mod report;
7
8use super::{CompileMode, Unit};
9use crate::core::PackageId;
10use crate::core::compiler::job_queue::JobId;
11use crate::core::compiler::{BuildContext, BuildRunner, TimingOutput};
12use crate::util::cpu::State;
13use crate::util::log_message::LogMessage;
14use crate::util::machine_message::{self, Message};
15use crate::util::style;
16use crate::util::{CargoResult, GlobalContext};
17
18use cargo_util::paths;
19use indexmap::IndexMap;
20use itertools::Itertools as _;
21use std::collections::HashMap;
22use std::io::BufWriter;
23use std::time::{Duration, Instant};
24use tracing::warn;
25
26pub struct Timings<'gctx> {
34 gctx: &'gctx GlobalContext,
35 enabled: bool,
37 report_html: bool,
39 report_json: bool,
41 start: Instant,
43 start_str: String,
45 root_targets: Vec<(String, Vec<String>)>,
49 profile: String,
51 total_fresh: u32,
53 total_dirty: u32,
55 unit_to_index: HashMap<Unit, u64>,
60 unit_times: Vec<UnitTime>,
62 active: HashMap<JobId, UnitTime>,
65 concurrency: Vec<Concurrency>,
68 last_cpu_state: Option<State>,
70 last_cpu_recording: Instant,
71 cpu_usage: Vec<(f64, f64)>,
75}
76
77#[derive(Copy, Clone, serde::Serialize)]
79pub struct CompilationSection {
80 start: f64,
82 end: Option<f64>,
84}
85
86struct UnitTime {
88 unit: Unit,
89 target: String,
91 start: f64,
93 duration: f64,
95 rmeta_time: Option<f64>,
98 unblocked_units: Vec<Unit>,
100 unblocked_rmeta_units: Vec<Unit>,
102 sections: IndexMap<String, CompilationSection>,
107}
108
109#[derive(serde::Serialize)]
111struct Concurrency {
112 t: f64,
114 active: usize,
116 waiting: usize,
118 inactive: usize,
121}
122
123#[derive(serde::Serialize)]
127struct UnitData {
128 i: usize,
129 name: String,
130 version: String,
131 mode: String,
132 target: String,
133 start: f64,
134 duration: f64,
135 rmeta_time: Option<f64>,
136 unblocked_units: Vec<usize>,
137 unblocked_rmeta_units: Vec<usize>,
138 sections: Option<Vec<(String, report::SectionData)>>,
139}
140
141impl<'gctx> Timings<'gctx> {
142 pub fn new(bcx: &BuildContext<'_, 'gctx>, root_units: &[Unit]) -> Timings<'gctx> {
143 let start = bcx.gctx.creation_time();
144 let has_report = |what| bcx.build_config.timing_outputs.contains(&what);
145 let report_html = has_report(TimingOutput::Html);
146 let report_json = has_report(TimingOutput::Json);
147 let enabled = report_html | report_json | bcx.logger.is_some();
148
149 if !enabled {
150 return Timings {
151 gctx: bcx.gctx,
152 enabled,
153 report_html,
154 report_json,
155 start,
156 start_str: String::new(),
157 root_targets: Vec::new(),
158 profile: String::new(),
159 total_fresh: 0,
160 total_dirty: 0,
161 unit_to_index: HashMap::new(),
162 unit_times: Vec::new(),
163 active: HashMap::new(),
164 concurrency: Vec::new(),
165 last_cpu_state: None,
166 last_cpu_recording: Instant::now(),
167 cpu_usage: Vec::new(),
168 };
169 }
170
171 let mut root_map: HashMap<PackageId, Vec<String>> = HashMap::new();
172 for unit in root_units {
173 let target_desc = unit.target.description_named();
174 root_map
175 .entry(unit.pkg.package_id())
176 .or_default()
177 .push(target_desc);
178 }
179 let root_targets = root_map
180 .into_iter()
181 .map(|(pkg_id, targets)| {
182 let pkg_desc = format!("{} {}", pkg_id.name(), pkg_id.version());
183 (pkg_desc, targets)
184 })
185 .collect();
186 let start_str = jiff::Timestamp::now().to_string();
187 let profile = bcx.build_config.requested_profile.to_string();
188 let last_cpu_state = match State::current() {
189 Ok(state) => Some(state),
190 Err(e) => {
191 tracing::info!("failed to get CPU state, CPU tracking disabled: {:?}", e);
192 None
193 }
194 };
195 let unit_to_index = bcx
196 .unit_graph
197 .keys()
198 .sorted()
199 .enumerate()
200 .map(|(i, unit)| (unit.clone(), i as u64))
201 .collect();
202
203 Timings {
204 gctx: bcx.gctx,
205 enabled,
206 report_html,
207 report_json,
208 start,
209 start_str,
210 root_targets,
211 profile,
212 total_fresh: 0,
213 total_dirty: 0,
214 unit_to_index,
215 unit_times: Vec::new(),
216 active: HashMap::new(),
217 concurrency: Vec::new(),
218 last_cpu_state,
219 last_cpu_recording: Instant::now(),
220 cpu_usage: Vec::new(),
221 }
222 }
223
224 pub fn unit_start(&mut self, build_runner: &BuildRunner<'_, '_>, id: JobId, unit: Unit) {
226 if !self.enabled {
227 return;
228 }
229 let mut target = if unit.target.is_lib() && unit.mode == CompileMode::Build {
230 "".to_string()
233 } else {
234 format!(" {}", unit.target.description_named())
235 };
236 match unit.mode {
237 CompileMode::Test => target.push_str(" (test)"),
238 CompileMode::Build => {}
239 CompileMode::Check { test: true } => target.push_str(" (check-test)"),
240 CompileMode::Check { test: false } => target.push_str(" (check)"),
241 CompileMode::Doc { .. } => target.push_str(" (doc)"),
242 CompileMode::Doctest => target.push_str(" (doc test)"),
243 CompileMode::Docscrape => target.push_str(" (doc scrape)"),
244 CompileMode::RunCustomBuild => target.push_str(" (run)"),
245 }
246 let start = self.start.elapsed().as_secs_f64();
247 let unit_time = UnitTime {
248 unit,
249 target,
250 start,
251 duration: 0.0,
252 rmeta_time: None,
253 unblocked_units: Vec::new(),
254 unblocked_rmeta_units: Vec::new(),
255 sections: Default::default(),
256 };
257 if let Some(logger) = build_runner.bcx.logger {
258 logger.log(LogMessage::UnitStarted {
259 package_id: unit_time.unit.pkg.package_id().to_spec(),
260 target: (&unit_time.unit.target).into(),
261 mode: unit_time.unit.mode,
262 index: self.unit_to_index[&unit_time.unit],
263 elapsed: start,
264 });
265 }
266 assert!(self.active.insert(id, unit_time).is_none());
267 }
268
269 pub fn unit_rmeta_finished(
271 &mut self,
272 build_runner: &BuildRunner<'_, '_>,
273 id: JobId,
274 unblocked: Vec<&Unit>,
275 ) {
276 if !self.enabled {
277 return;
278 }
279 let Some(unit_time) = self.active.get_mut(&id) else {
283 return;
284 };
285 let elapsed = self.start.elapsed().as_secs_f64();
286 unit_time.rmeta_time = Some(elapsed - unit_time.start);
287 assert!(unit_time.unblocked_rmeta_units.is_empty());
288 unit_time
289 .unblocked_rmeta_units
290 .extend(unblocked.iter().cloned().cloned());
291
292 if let Some(logger) = build_runner.bcx.logger {
293 let unblocked = unblocked.iter().map(|u| self.unit_to_index[u]).collect();
294 logger.log(LogMessage::UnitRmetaFinished {
295 index: self.unit_to_index[&unit_time.unit],
296 elapsed,
297 unblocked,
298 });
299 }
300 }
301
302 pub fn unit_finished(
304 &mut self,
305 build_runner: &BuildRunner<'_, '_>,
306 id: JobId,
307 unblocked: Vec<&Unit>,
308 ) {
309 if !self.enabled {
310 return;
311 }
312 let Some(mut unit_time) = self.active.remove(&id) else {
314 return;
315 };
316 let elapsed = self.start.elapsed().as_secs_f64();
317 unit_time.duration = elapsed - unit_time.start;
318 assert!(unit_time.unblocked_units.is_empty());
319 unit_time
320 .unblocked_units
321 .extend(unblocked.iter().cloned().cloned());
322 if self.report_json {
323 let msg = machine_message::TimingInfo {
324 package_id: unit_time.unit.pkg.package_id().to_spec(),
325 target: &unit_time.unit.target,
326 mode: unit_time.unit.mode,
327 duration: unit_time.duration,
328 rmeta_time: unit_time.rmeta_time,
329 sections: unit_time.sections.clone().into_iter().collect(),
330 }
331 .to_json_string();
332 crate::drop_println!(self.gctx, "{}", msg);
333 }
334 if let Some(logger) = build_runner.bcx.logger {
335 let unblocked = unblocked.iter().map(|u| self.unit_to_index[u]).collect();
336 logger.log(LogMessage::UnitFinished {
337 index: self.unit_to_index[&unit_time.unit],
338 elapsed,
339 unblocked,
340 });
341 }
342 self.unit_times.push(unit_time);
343 }
344
345 pub fn unit_section_timing(
347 &mut self,
348 build_runner: &BuildRunner<'_, '_>,
349 id: JobId,
350 section_timing: &SectionTiming,
351 ) {
352 if !self.enabled {
353 return;
354 }
355 let Some(unit_time) = self.active.get_mut(&id) else {
356 return;
357 };
358 let elapsed = self.start.elapsed().as_secs_f64();
359
360 match section_timing.event {
361 SectionTimingEvent::Start => {
362 unit_time.start_section(§ion_timing.name, elapsed);
363 }
364 SectionTimingEvent::End => {
365 unit_time.end_section(§ion_timing.name, elapsed);
366 }
367 }
368
369 if let Some(logger) = build_runner.bcx.logger {
370 let index = self.unit_to_index[&unit_time.unit];
371 let section = section_timing.name.clone();
372 logger.log(match section_timing.event {
373 SectionTimingEvent::Start => LogMessage::UnitSectionStarted {
374 index,
375 elapsed,
376 section,
377 },
378 SectionTimingEvent::End => LogMessage::UnitSectionFinished {
379 index,
380 elapsed,
381 section,
382 },
383 })
384 }
385 }
386
387 pub fn mark_concurrency(&mut self, active: usize, waiting: usize, inactive: usize) {
389 if !self.enabled {
390 return;
391 }
392 let c = Concurrency {
393 t: self.start.elapsed().as_secs_f64(),
394 active,
395 waiting,
396 inactive,
397 };
398 self.concurrency.push(c);
399 }
400
401 pub fn add_fresh(&mut self) {
403 self.total_fresh += 1;
404 }
405
406 pub fn add_dirty(&mut self) {
408 self.total_dirty += 1;
409 }
410
411 pub fn record_cpu(&mut self) {
413 if !self.enabled {
414 return;
415 }
416 let Some(prev) = &mut self.last_cpu_state else {
417 return;
418 };
419 let now = Instant::now();
421 if self.last_cpu_recording.elapsed() < Duration::from_millis(100) {
422 return;
423 }
424 let current = match State::current() {
425 Ok(s) => s,
426 Err(e) => {
427 tracing::info!("failed to get CPU state: {:?}", e);
428 return;
429 }
430 };
431 let pct_idle = current.idle_since(prev);
432 *prev = current;
433 self.last_cpu_recording = now;
434 let dur = now.duration_since(self.start).as_secs_f64();
435 self.cpu_usage.push((dur, 100.0 - pct_idle));
436 }
437
438 pub fn finished(
440 &mut self,
441 build_runner: &BuildRunner<'_, '_>,
442 error: &Option<anyhow::Error>,
443 ) -> CargoResult<()> {
444 if !self.enabled {
445 return Ok(());
446 }
447 self.mark_concurrency(0, 0, 0);
448 self.unit_times
449 .sort_unstable_by(|a, b| a.start.partial_cmp(&b.start).unwrap());
450 if self.report_html {
451 let timestamp = self.start_str.replace(&['-', ':'][..], "");
452 let timings_path = build_runner
453 .files()
454 .timings_dir()
455 .expect("artifact-dir was not locked");
456 paths::create_dir_all(&timings_path)?;
457 let filename = timings_path.join(format!("cargo-timing-{}.html", timestamp));
458 let mut f = BufWriter::new(paths::create(&filename)?);
459
460 let rustc_version = build_runner
461 .bcx
462 .rustc()
463 .verbose_version
464 .lines()
465 .next()
466 .expect("rustc version");
467 let requested_targets = &build_runner
468 .bcx
469 .build_config
470 .requested_kinds
471 .iter()
472 .map(|kind| build_runner.bcx.target_data.short_name(kind))
473 .collect::<Vec<_>>();
474
475 let ctx = report::RenderContext {
476 start: self.start,
477 start_str: &self.start_str,
478 root_units: &self.root_targets,
479 profile: &self.profile,
480 total_fresh: self.total_fresh,
481 total_dirty: self.total_dirty,
482 unit_times: &self.unit_times,
483 concurrency: &self.concurrency,
484 cpu_usage: &self.cpu_usage,
485 rustc_version,
486 host: &build_runner.bcx.rustc().host,
487 requested_targets,
488 jobs: build_runner.bcx.jobs(),
489 error,
490 };
491 report::write_html(ctx, &mut f)?;
492
493 let unstamped_filename = timings_path.join("cargo-timing.html");
494 paths::link_or_copy(&filename, &unstamped_filename)?;
495
496 let mut shell = self.gctx.shell();
497 let timing_path = std::env::current_dir().unwrap_or_default().join(&filename);
498 let link = shell.err_file_hyperlink(&timing_path);
499 let msg = format!("report saved to {link}{}{link:#}", timing_path.display(),);
500 shell.status_with_color("Timing", msg, &style::NOTE)?;
501 }
502 Ok(())
503 }
504}
505
506impl UnitTime {
507 fn name_ver(&self) -> String {
508 format!("{} v{}", self.unit.pkg.name(), self.unit.pkg.version())
509 }
510
511 fn start_section(&mut self, name: &str, now: f64) {
512 if self
513 .sections
514 .insert(
515 name.to_string(),
516 CompilationSection {
517 start: now - self.start,
518 end: None,
519 },
520 )
521 .is_some()
522 {
523 warn!("compilation section {name} started more than once");
524 }
525 }
526
527 fn end_section(&mut self, name: &str, now: f64) {
528 let Some(section) = self.sections.get_mut(name) else {
529 warn!("compilation section {name} ended, but it has no start recorded");
530 return;
531 };
532 section.end = Some(now - self.start);
533 }
534}
535
536#[derive(serde::Deserialize, Debug)]
538#[serde(rename_all = "kebab-case")]
539pub enum SectionTimingEvent {
540 Start,
541 End,
542}
543
544#[derive(serde::Deserialize, Debug)]
547pub struct SectionTiming {
548 pub name: String,
549 pub event: SectionTimingEvent,
550}