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 std::collections::HashMap;
21use std::io::BufWriter;
22use std::time::{Duration, Instant};
23use tracing::warn;
24
25pub struct Timings<'gctx> {
33 gctx: &'gctx GlobalContext,
34 enabled: bool,
36 report_html: bool,
38 report_json: 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_times: Vec<UnitTime>,
56 active: HashMap<JobId, UnitTime>,
59 concurrency: Vec<Concurrency>,
62 last_cpu_state: Option<State>,
64 last_cpu_recording: Instant,
65 cpu_usage: Vec<(f64, f64)>,
69}
70
71#[derive(Copy, Clone, serde::Serialize)]
73pub struct CompilationSection {
74 start: f64,
76 end: Option<f64>,
78}
79
80struct UnitTime {
82 unit: Unit,
83 target: String,
85 start: f64,
87 duration: f64,
89 rmeta_time: Option<f64>,
92 unlocked_units: Vec<Unit>,
94 unlocked_rmeta_units: Vec<Unit>,
96 sections: IndexMap<String, CompilationSection>,
101}
102
103#[derive(serde::Serialize)]
105struct Concurrency {
106 t: f64,
108 active: usize,
110 waiting: usize,
112 inactive: usize,
115}
116
117#[derive(serde::Serialize)]
121struct UnitData {
122 i: usize,
123 name: String,
124 version: String,
125 mode: String,
126 target: String,
127 start: f64,
128 duration: f64,
129 rmeta_time: Option<f64>,
130 unlocked_units: Vec<usize>,
131 unlocked_rmeta_units: Vec<usize>,
132 sections: Option<Vec<(String, report::SectionData)>>,
133}
134
135impl<'gctx> Timings<'gctx> {
136 pub fn new(bcx: &BuildContext<'_, 'gctx>, root_units: &[Unit]) -> Timings<'gctx> {
137 let has_report = |what| bcx.build_config.timing_outputs.contains(&what);
138 let report_html = has_report(TimingOutput::Html);
139 let report_json = has_report(TimingOutput::Json);
140 let enabled = report_html | report_json | bcx.logger.is_some();
141
142 let mut root_map: HashMap<PackageId, Vec<String>> = HashMap::new();
143 for unit in root_units {
144 let target_desc = unit.target.description_named();
145 root_map
146 .entry(unit.pkg.package_id())
147 .or_default()
148 .push(target_desc);
149 }
150 let root_targets = root_map
151 .into_iter()
152 .map(|(pkg_id, targets)| {
153 let pkg_desc = format!("{} {}", pkg_id.name(), pkg_id.version());
154 (pkg_desc, targets)
155 })
156 .collect();
157 let start_str = jiff::Timestamp::now().to_string();
158 let profile = bcx.build_config.requested_profile.to_string();
159 let last_cpu_state = if enabled {
160 match State::current() {
161 Ok(state) => Some(state),
162 Err(e) => {
163 tracing::info!("failed to get CPU state, CPU tracking disabled: {:?}", e);
164 None
165 }
166 }
167 } else {
168 None
169 };
170
171 Timings {
172 gctx: bcx.gctx,
173 enabled,
174 report_html,
175 report_json,
176 start: bcx.gctx.creation_time(),
177 start_str,
178 root_targets,
179 profile,
180 total_fresh: 0,
181 total_dirty: 0,
182 unit_times: Vec::new(),
183 active: HashMap::new(),
184 concurrency: Vec::new(),
185 last_cpu_state,
186 last_cpu_recording: Instant::now(),
187 cpu_usage: Vec::new(),
188 }
189 }
190
191 pub fn unit_start(&mut self, id: JobId, unit: Unit) {
193 if !self.enabled {
194 return;
195 }
196 let mut target = if unit.target.is_lib() && unit.mode == CompileMode::Build {
197 "".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 unit_time = UnitTime {
214 unit,
215 target,
216 start: self.start.elapsed().as_secs_f64(),
217 duration: 0.0,
218 rmeta_time: None,
219 unlocked_units: Vec::new(),
220 unlocked_rmeta_units: Vec::new(),
221 sections: Default::default(),
222 };
223 assert!(self.active.insert(id, unit_time).is_none());
224 }
225
226 pub fn unit_rmeta_finished(&mut self, id: JobId, unlocked: Vec<&Unit>) {
228 if !self.enabled {
229 return;
230 }
231 let Some(unit_time) = self.active.get_mut(&id) else {
235 return;
236 };
237 let t = self.start.elapsed().as_secs_f64();
238 unit_time.rmeta_time = Some(t - unit_time.start);
239 assert!(unit_time.unlocked_rmeta_units.is_empty());
240 unit_time
241 .unlocked_rmeta_units
242 .extend(unlocked.iter().cloned().cloned());
243 }
244
245 pub fn unit_finished(
247 &mut self,
248 build_runner: &BuildRunner<'_, '_>,
249 id: JobId,
250 unlocked: Vec<&Unit>,
251 ) {
252 if !self.enabled {
253 return;
254 }
255 let Some(mut unit_time) = self.active.remove(&id) else {
257 return;
258 };
259 let t = self.start.elapsed().as_secs_f64();
260 unit_time.duration = t - unit_time.start;
261 assert!(unit_time.unlocked_units.is_empty());
262 unit_time
263 .unlocked_units
264 .extend(unlocked.iter().cloned().cloned());
265 if self.report_json {
266 let msg = machine_message::TimingInfo {
267 package_id: unit_time.unit.pkg.package_id().to_spec(),
268 target: &unit_time.unit.target,
269 mode: unit_time.unit.mode,
270 duration: unit_time.duration,
271 rmeta_time: unit_time.rmeta_time,
272 sections: unit_time.sections.clone().into_iter().collect(),
273 }
274 .to_json_string();
275 crate::drop_println!(self.gctx, "{}", msg);
276 }
277 if let Some(logger) = build_runner.bcx.logger {
278 logger.log(LogMessage::TimingInfo {
279 package_id: unit_time.unit.pkg.package_id().to_spec(),
280 target: unit_time.unit.target.clone(),
281 mode: unit_time.unit.mode,
282 duration: unit_time.duration,
283 rmeta_time: unit_time.rmeta_time,
284 sections: unit_time.sections.clone().into_iter().collect(),
285 });
286 }
287 self.unit_times.push(unit_time);
288 }
289
290 pub fn unit_section_timing(&mut self, id: JobId, section_timing: &SectionTiming) {
292 if !self.enabled {
293 return;
294 }
295 let Some(unit_time) = self.active.get_mut(&id) else {
296 return;
297 };
298 let now = self.start.elapsed().as_secs_f64();
299
300 match section_timing.event {
301 SectionTimingEvent::Start => {
302 unit_time.start_section(§ion_timing.name, now);
303 }
304 SectionTimingEvent::End => {
305 unit_time.end_section(§ion_timing.name, now);
306 }
307 }
308 }
309
310 pub fn mark_concurrency(&mut self, active: usize, waiting: usize, inactive: usize) {
312 if !self.enabled {
313 return;
314 }
315 let c = Concurrency {
316 t: self.start.elapsed().as_secs_f64(),
317 active,
318 waiting,
319 inactive,
320 };
321 self.concurrency.push(c);
322 }
323
324 pub fn add_fresh(&mut self) {
326 self.total_fresh += 1;
327 }
328
329 pub fn add_dirty(&mut self) {
331 self.total_dirty += 1;
332 }
333
334 pub fn record_cpu(&mut self) {
336 if !self.enabled {
337 return;
338 }
339 let Some(prev) = &mut self.last_cpu_state else {
340 return;
341 };
342 let now = Instant::now();
344 if self.last_cpu_recording.elapsed() < Duration::from_millis(100) {
345 return;
346 }
347 let current = match State::current() {
348 Ok(s) => s,
349 Err(e) => {
350 tracing::info!("failed to get CPU state: {:?}", e);
351 return;
352 }
353 };
354 let pct_idle = current.idle_since(prev);
355 *prev = current;
356 self.last_cpu_recording = now;
357 let dur = now.duration_since(self.start).as_secs_f64();
358 self.cpu_usage.push((dur, 100.0 - pct_idle));
359 }
360
361 pub fn finished(
363 &mut self,
364 build_runner: &BuildRunner<'_, '_>,
365 error: &Option<anyhow::Error>,
366 ) -> CargoResult<()> {
367 if !self.enabled {
368 return Ok(());
369 }
370 self.mark_concurrency(0, 0, 0);
371 self.unit_times
372 .sort_unstable_by(|a, b| a.start.partial_cmp(&b.start).unwrap());
373 if self.report_html {
374 let timestamp = self.start_str.replace(&['-', ':'][..], "");
375 let timings_path = build_runner
376 .files()
377 .timings_dir()
378 .expect("artifact-dir was not locked");
379 paths::create_dir_all(&timings_path)?;
380 let filename = timings_path.join(format!("cargo-timing-{}.html", timestamp));
381 let mut f = BufWriter::new(paths::create(&filename)?);
382
383 let rustc_version = build_runner
384 .bcx
385 .rustc()
386 .verbose_version
387 .lines()
388 .next()
389 .expect("rustc version");
390 let requested_targets = &build_runner
391 .bcx
392 .build_config
393 .requested_kinds
394 .iter()
395 .map(|kind| build_runner.bcx.target_data.short_name(kind))
396 .collect::<Vec<_>>();
397
398 let ctx = report::RenderContext {
399 start: self.start,
400 start_str: &self.start_str,
401 root_units: &self.root_targets,
402 profile: &self.profile,
403 total_fresh: self.total_fresh,
404 total_dirty: self.total_dirty,
405 unit_times: &self.unit_times,
406 concurrency: &self.concurrency,
407 cpu_usage: &self.cpu_usage,
408 rustc_version,
409 host: &build_runner.bcx.rustc().host,
410 requested_targets,
411 jobs: build_runner.bcx.jobs(),
412 error,
413 };
414 report::write_html(ctx, &mut f)?;
415
416 let unstamped_filename = timings_path.join("cargo-timing.html");
417 paths::link_or_copy(&filename, &unstamped_filename)?;
418
419 let mut shell = self.gctx.shell();
420 let timing_path = std::env::current_dir().unwrap_or_default().join(&filename);
421 let link = shell.err_file_hyperlink(&timing_path);
422 let msg = format!("report saved to {link}{}{link:#}", timing_path.display(),);
423 shell.status_with_color("Timing", msg, &style::NOTE)?;
424 }
425 Ok(())
426 }
427}
428
429impl UnitTime {
430 fn name_ver(&self) -> String {
431 format!("{} v{}", self.unit.pkg.name(), self.unit.pkg.version())
432 }
433
434 fn start_section(&mut self, name: &str, now: f64) {
435 if self
436 .sections
437 .insert(
438 name.to_string(),
439 CompilationSection {
440 start: now - self.start,
441 end: None,
442 },
443 )
444 .is_some()
445 {
446 warn!("compilation section {name} started more than once");
447 }
448 }
449
450 fn end_section(&mut self, name: &str, now: f64) {
451 let Some(section) = self.sections.get_mut(name) else {
452 warn!("compilation section {name} ended, but it has no start recorded");
453 return;
454 };
455 section.end = Some(now - self.start);
456 }
457}
458
459#[derive(serde::Deserialize, Debug)]
461#[serde(rename_all = "kebab-case")]
462pub enum SectionTimingEvent {
463 Start,
464 End,
465}
466
467#[derive(serde::Deserialize, Debug)]
470pub struct SectionTiming {
471 pub name: String,
472 pub event: SectionTimingEvent,
473}