1use super::{CompileMode, Unit};
6use crate::core::PackageId;
7use crate::core::compiler::job_queue::JobId;
8use crate::core::compiler::{BuildContext, BuildRunner, TimingOutput};
9use crate::util::cpu::State;
10use crate::util::machine_message::{self, Message};
11use crate::util::style;
12use crate::util::{CargoResult, GlobalContext};
13use anyhow::Context as _;
14use cargo_util::paths;
15use std::collections::HashMap;
16use std::io::{BufWriter, Write};
17use std::thread::available_parallelism;
18use std::time::{Duration, Instant};
19
20pub struct Timings<'gctx> {
28 gctx: &'gctx GlobalContext,
29 enabled: bool,
31 report_html: bool,
33 report_json: bool,
35 start: Instant,
37 start_str: String,
39 root_targets: Vec<(String, Vec<String>)>,
43 profile: String,
45 total_fresh: u32,
47 total_dirty: u32,
49 unit_times: Vec<UnitTime>,
51 active: HashMap<JobId, UnitTime>,
54 concurrency: Vec<Concurrency>,
57 last_cpu_state: Option<State>,
59 last_cpu_recording: Instant,
60 cpu_usage: Vec<(f64, f64)>,
64}
65
66struct UnitTime {
68 unit: Unit,
69 target: String,
71 start: f64,
73 duration: f64,
75 rmeta_time: Option<f64>,
78 unlocked_units: Vec<Unit>,
80 unlocked_rmeta_units: Vec<Unit>,
82}
83
84#[derive(serde::Serialize)]
86struct Concurrency {
87 t: f64,
89 active: usize,
91 waiting: usize,
93 inactive: usize,
96}
97
98impl<'gctx> Timings<'gctx> {
99 pub fn new(bcx: &BuildContext<'_, 'gctx>, root_units: &[Unit]) -> Timings<'gctx> {
100 let has_report = |what| bcx.build_config.timing_outputs.contains(&what);
101 let report_html = has_report(TimingOutput::Html);
102 let report_json = has_report(TimingOutput::Json);
103 let enabled = report_html | report_json;
104
105 let mut root_map: HashMap<PackageId, Vec<String>> = HashMap::new();
106 for unit in root_units {
107 let target_desc = unit.target.description_named();
108 root_map
109 .entry(unit.pkg.package_id())
110 .or_default()
111 .push(target_desc);
112 }
113 let root_targets = root_map
114 .into_iter()
115 .map(|(pkg_id, targets)| {
116 let pkg_desc = format!("{} {}", pkg_id.name(), pkg_id.version());
117 (pkg_desc, targets)
118 })
119 .collect();
120 let start_str = jiff::Timestamp::now().to_string();
121 let profile = bcx.build_config.requested_profile.to_string();
122 let last_cpu_state = if enabled {
123 match State::current() {
124 Ok(state) => Some(state),
125 Err(e) => {
126 tracing::info!("failed to get CPU state, CPU tracking disabled: {:?}", e);
127 None
128 }
129 }
130 } else {
131 None
132 };
133
134 Timings {
135 gctx: bcx.gctx,
136 enabled,
137 report_html,
138 report_json,
139 start: bcx.gctx.creation_time(),
140 start_str,
141 root_targets,
142 profile,
143 total_fresh: 0,
144 total_dirty: 0,
145 unit_times: Vec::new(),
146 active: HashMap::new(),
147 concurrency: Vec::new(),
148 last_cpu_state,
149 last_cpu_recording: Instant::now(),
150 cpu_usage: Vec::new(),
151 }
152 }
153
154 pub fn unit_start(&mut self, id: JobId, unit: Unit) {
156 if !self.enabled {
157 return;
158 }
159 let mut target = if unit.target.is_lib() && unit.mode == CompileMode::Build {
160 "".to_string()
163 } else {
164 format!(" {}", unit.target.description_named())
165 };
166 match unit.mode {
167 CompileMode::Test => target.push_str(" (test)"),
168 CompileMode::Build => {}
169 CompileMode::Check { test: true } => target.push_str(" (check-test)"),
170 CompileMode::Check { test: false } => target.push_str(" (check)"),
171 CompileMode::Doc { .. } => target.push_str(" (doc)"),
172 CompileMode::Doctest => target.push_str(" (doc test)"),
173 CompileMode::Docscrape => target.push_str(" (doc scrape)"),
174 CompileMode::RunCustomBuild => target.push_str(" (run)"),
175 }
176 let unit_time = UnitTime {
177 unit,
178 target,
179 start: self.start.elapsed().as_secs_f64(),
180 duration: 0.0,
181 rmeta_time: None,
182 unlocked_units: Vec::new(),
183 unlocked_rmeta_units: Vec::new(),
184 };
185 assert!(self.active.insert(id, unit_time).is_none());
186 }
187
188 pub fn unit_rmeta_finished(&mut self, id: JobId, unlocked: Vec<&Unit>) {
190 if !self.enabled {
191 return;
192 }
193 let Some(unit_time) = self.active.get_mut(&id) else {
197 return;
198 };
199 let t = self.start.elapsed().as_secs_f64();
200 unit_time.rmeta_time = Some(t - unit_time.start);
201 assert!(unit_time.unlocked_rmeta_units.is_empty());
202 unit_time
203 .unlocked_rmeta_units
204 .extend(unlocked.iter().cloned().cloned());
205 }
206
207 pub fn unit_finished(&mut self, id: JobId, unlocked: Vec<&Unit>) {
209 if !self.enabled {
210 return;
211 }
212 let Some(mut unit_time) = self.active.remove(&id) else {
214 return;
215 };
216 let t = self.start.elapsed().as_secs_f64();
217 unit_time.duration = t - unit_time.start;
218 assert!(unit_time.unlocked_units.is_empty());
219 unit_time
220 .unlocked_units
221 .extend(unlocked.iter().cloned().cloned());
222 if self.report_json {
223 let msg = machine_message::TimingInfo {
224 package_id: unit_time.unit.pkg.package_id().to_spec(),
225 target: &unit_time.unit.target,
226 mode: unit_time.unit.mode,
227 duration: unit_time.duration,
228 rmeta_time: unit_time.rmeta_time,
229 }
230 .to_json_string();
231 crate::drop_println!(self.gctx, "{}", msg);
232 }
233 self.unit_times.push(unit_time);
234 }
235
236 pub fn mark_concurrency(&mut self, active: usize, waiting: usize, inactive: usize) {
238 if !self.enabled {
239 return;
240 }
241 let c = Concurrency {
242 t: self.start.elapsed().as_secs_f64(),
243 active,
244 waiting,
245 inactive,
246 };
247 self.concurrency.push(c);
248 }
249
250 pub fn add_fresh(&mut self) {
252 self.total_fresh += 1;
253 }
254
255 pub fn add_dirty(&mut self) {
257 self.total_dirty += 1;
258 }
259
260 pub fn record_cpu(&mut self) {
262 if !self.enabled {
263 return;
264 }
265 let Some(prev) = &mut self.last_cpu_state else {
266 return;
267 };
268 let now = Instant::now();
270 if self.last_cpu_recording.elapsed() < Duration::from_millis(100) {
271 return;
272 }
273 let current = match State::current() {
274 Ok(s) => s,
275 Err(e) => {
276 tracing::info!("failed to get CPU state: {:?}", e);
277 return;
278 }
279 };
280 let pct_idle = current.idle_since(prev);
281 *prev = current;
282 self.last_cpu_recording = now;
283 let dur = now.duration_since(self.start).as_secs_f64();
284 self.cpu_usage.push((dur, 100.0 - pct_idle));
285 }
286
287 pub fn finished(
289 &mut self,
290 build_runner: &BuildRunner<'_, '_>,
291 error: &Option<anyhow::Error>,
292 ) -> CargoResult<()> {
293 if !self.enabled {
294 return Ok(());
295 }
296 self.mark_concurrency(0, 0, 0);
297 self.unit_times
298 .sort_unstable_by(|a, b| a.start.partial_cmp(&b.start).unwrap());
299 if self.report_html {
300 self.report_html(build_runner, error)
301 .context("failed to save timing report")?;
302 }
303 Ok(())
304 }
305
306 fn report_html(
308 &self,
309 build_runner: &BuildRunner<'_, '_>,
310 error: &Option<anyhow::Error>,
311 ) -> CargoResult<()> {
312 let duration = self.start.elapsed().as_secs_f64();
313 let timestamp = self.start_str.replace(&['-', ':'][..], "");
314 let timings_path = build_runner.files().host_root().join("cargo-timings");
315 paths::create_dir_all(&timings_path)?;
316 let filename = timings_path.join(format!("cargo-timing-{}.html", timestamp));
317 let mut f = BufWriter::new(paths::create(&filename)?);
318 let roots: Vec<&str> = self
319 .root_targets
320 .iter()
321 .map(|(name, _targets)| name.as_str())
322 .collect();
323 f.write_all(HTML_TMPL.replace("{ROOTS}", &roots.join(", ")).as_bytes())?;
324 self.write_summary_table(&mut f, duration, build_runner.bcx, error)?;
325 f.write_all(HTML_CANVAS.as_bytes())?;
326 self.write_unit_table(&mut f)?;
327 writeln!(
329 f,
330 "<script>\n\
331 DURATION = {};",
332 f64::ceil(duration) as u32
333 )?;
334 self.write_js_data(&mut f)?;
335 write!(
336 f,
337 "{}\n\
338 </script>\n\
339 </body>\n\
340 </html>\n\
341 ",
342 include_str!("timings.js")
343 )?;
344 drop(f);
345
346 let unstamped_filename = timings_path.join("cargo-timing.html");
347 paths::link_or_copy(&filename, &unstamped_filename)?;
348
349 let mut shell = self.gctx.shell();
350 let timing_path = std::env::current_dir().unwrap_or_default().join(&filename);
351 let link = shell.err_file_hyperlink(&timing_path);
352 let msg = format!("report saved to {link}{}{link:#}", timing_path.display(),);
353 shell.status_with_color("Timing", msg, &style::NOTE)?;
354
355 Ok(())
356 }
357
358 fn write_summary_table(
360 &self,
361 f: &mut impl Write,
362 duration: f64,
363 bcx: &BuildContext<'_, '_>,
364 error: &Option<anyhow::Error>,
365 ) -> CargoResult<()> {
366 let targets: Vec<String> = self
367 .root_targets
368 .iter()
369 .map(|(name, targets)| format!("{} ({})", name, targets.join(", ")))
370 .collect();
371 let targets = targets.join("<br>");
372 let time_human = if duration > 60.0 {
373 format!(" ({}m {:.1}s)", duration as u32 / 60, duration % 60.0)
374 } else {
375 "".to_string()
376 };
377 let total_time = format!("{:.1}s{}", duration, time_human);
378 let max_concurrency = self.concurrency.iter().map(|c| c.active).max().unwrap();
379 let num_cpus = available_parallelism()
380 .map(|x| x.get().to_string())
381 .unwrap_or_else(|_| "n/a".into());
382 let rustc_info = render_rustc_info(bcx);
383 let error_msg = match error {
384 Some(e) => format!(r#"<tr><td class="error-text">Error:</td><td>{e}</td></tr>"#),
385 None => "".to_string(),
386 };
387 write!(
388 f,
389 r#"
390<table class="my-table summary-table">
391 <tr>
392 <td>Targets:</td><td>{}</td>
393 </tr>
394 <tr>
395 <td>Profile:</td><td>{}</td>
396 </tr>
397 <tr>
398 <td>Fresh units:</td><td>{}</td>
399 </tr>
400 <tr>
401 <td>Dirty units:</td><td>{}</td>
402 </tr>
403 <tr>
404 <td>Total units:</td><td>{}</td>
405 </tr>
406 <tr>
407 <td>Max concurrency:</td><td>{} (jobs={} ncpu={})</td>
408 </tr>
409 <tr>
410 <td>Build start:</td><td>{}</td>
411 </tr>
412 <tr>
413 <td>Total time:</td><td>{}</td>
414 </tr>
415 <tr>
416 <td>rustc:</td><td>{}</td>
417 </tr>
418{}
419</table>
420"#,
421 targets,
422 self.profile,
423 self.total_fresh,
424 self.total_dirty,
425 self.total_fresh + self.total_dirty,
426 max_concurrency,
427 bcx.jobs(),
428 num_cpus,
429 self.start_str,
430 total_time,
431 rustc_info,
432 error_msg,
433 )?;
434 Ok(())
435 }
436
437 fn write_js_data(&self, f: &mut impl Write) -> CargoResult<()> {
440 let unit_map: HashMap<Unit, usize> = self
442 .unit_times
443 .iter()
444 .enumerate()
445 .map(|(i, ut)| (ut.unit.clone(), i))
446 .collect();
447 #[derive(serde::Serialize)]
448 struct UnitData {
449 i: usize,
450 name: String,
451 version: String,
452 mode: String,
453 target: String,
454 start: f64,
455 duration: f64,
456 rmeta_time: Option<f64>,
457 unlocked_units: Vec<usize>,
458 unlocked_rmeta_units: Vec<usize>,
459 }
460 let round = |x: f64| (x * 100.0).round() / 100.0;
461 let unit_data: Vec<UnitData> = self
462 .unit_times
463 .iter()
464 .enumerate()
465 .map(|(i, ut)| {
466 let mode = if ut.unit.mode.is_run_custom_build() {
467 "run-custom-build"
468 } else {
469 "todo"
470 }
471 .to_string();
472
473 let unlocked_units: Vec<usize> = ut
477 .unlocked_units
478 .iter()
479 .filter_map(|unit| unit_map.get(unit).copied())
480 .collect();
481 let unlocked_rmeta_units: Vec<usize> = ut
482 .unlocked_rmeta_units
483 .iter()
484 .filter_map(|unit| unit_map.get(unit).copied())
485 .collect();
486 UnitData {
487 i,
488 name: ut.unit.pkg.name().to_string(),
489 version: ut.unit.pkg.version().to_string(),
490 mode,
491 target: ut.target.clone(),
492 start: round(ut.start),
493 duration: round(ut.duration),
494 rmeta_time: ut.rmeta_time.map(round),
495 unlocked_units,
496 unlocked_rmeta_units,
497 }
498 })
499 .collect();
500 writeln!(
501 f,
502 "const UNIT_DATA = {};",
503 serde_json::to_string_pretty(&unit_data)?
504 )?;
505 writeln!(
506 f,
507 "const CONCURRENCY_DATA = {};",
508 serde_json::to_string_pretty(&self.concurrency)?
509 )?;
510 writeln!(
511 f,
512 "const CPU_USAGE = {};",
513 serde_json::to_string_pretty(&self.cpu_usage)?
514 )?;
515 Ok(())
516 }
517
518 fn write_unit_table(&self, f: &mut impl Write) -> CargoResult<()> {
520 write!(
521 f,
522 r#"
523<table class="my-table">
524 <thead>
525 <tr>
526 <th></th>
527 <th>Unit</th>
528 <th>Total</th>
529 <th>Codegen</th>
530 <th>Features</th>
531 </tr>
532 </thead>
533 <tbody>
534"#
535 )?;
536 let mut units: Vec<&UnitTime> = self.unit_times.iter().collect();
537 units.sort_unstable_by(|a, b| b.duration.partial_cmp(&a.duration).unwrap());
538 for (i, unit) in units.iter().enumerate() {
539 let codegen = match unit.codegen_time() {
540 None => "".to_string(),
541 Some((_rt, ctime, cent)) => format!("{:.1}s ({:.0}%)", ctime, cent),
542 };
543 let features = unit.unit.features.join(", ");
544 write!(
545 f,
546 r#"
547<tr>
548 <td>{}.</td>
549 <td>{}{}</td>
550 <td>{:.1}s</td>
551 <td>{}</td>
552 <td>{}</td>
553</tr>
554"#,
555 i + 1,
556 unit.name_ver(),
557 unit.target,
558 unit.duration,
559 codegen,
560 features,
561 )?;
562 }
563 write!(f, "</tbody>\n</table>\n")?;
564 Ok(())
565 }
566}
567
568impl UnitTime {
569 fn codegen_time(&self) -> Option<(f64, f64, f64)> {
571 self.rmeta_time.map(|rmeta_time| {
572 let ctime = self.duration - rmeta_time;
573 let cent = (ctime / self.duration) * 100.0;
574 (rmeta_time, ctime, cent)
575 })
576 }
577
578 fn name_ver(&self) -> String {
579 format!("{} v{}", self.unit.pkg.name(), self.unit.pkg.version())
580 }
581}
582
583fn render_rustc_info(bcx: &BuildContext<'_, '_>) -> String {
584 let version = bcx
585 .rustc()
586 .verbose_version
587 .lines()
588 .next()
589 .expect("rustc version");
590 let requested_target = bcx
591 .build_config
592 .requested_kinds
593 .iter()
594 .map(|kind| bcx.target_data.short_name(kind))
595 .collect::<Vec<_>>()
596 .join(", ");
597 format!(
598 "{}<br>Host: {}<br>Target: {}",
599 version,
600 bcx.rustc().host,
601 requested_target
602 )
603}
604
605static HTML_TMPL: &str = r#"
606<html>
607<head>
608 <title>Cargo Build Timings — {ROOTS}</title>
609 <meta charset="utf-8">
610<style type="text/css">
611:root {
612 --error-text: #e80000;
613 --text: #000;
614 --background: #fff;
615 --h1-border-bottom: #c0c0c0;
616 --table-box-shadow: rgba(0, 0, 0, 0.1);
617 --table-th: #d5dde5;
618 --table-th-background: #1b1e24;
619 --table-th-border-bottom: #9ea7af;
620 --table-th-border-right: #343a45;
621 --table-tr-border-top: #c1c3d1;
622 --table-tr-border-bottom: #c1c3d1;
623 --table-tr-odd-background: #ebebeb;
624 --table-td-background: #ffffff;
625 --table-td-border-right: #C1C3D1;
626 --canvas-background: #f7f7f7;
627 --canvas-axes: #303030;
628 --canvas-grid: #e6e6e6;
629 --canvas-block: #aa95e8;
630 --canvas-custom-build: #f0b165;
631 --canvas-not-custom-build: #95cce8;
632 --canvas-dep-line: #ddd;
633 --canvas-dep-line-highlighted: #000;
634 --canvas-cpu: rgba(250, 119, 0, 0.2);
635}
636
637@media (prefers-color-scheme: dark) {
638 :root {
639 --error-text: #e80000;
640 --text: #fff;
641 --background: #121212;
642 --h1-border-bottom: #444;
643 --table-box-shadow: rgba(255, 255, 255, 0.1);
644 --table-th: #a0a0a0;
645 --table-th-background: #2c2c2c;
646 --table-th-border-bottom: #555;
647 --table-th-border-right: #444;
648 --table-tr-border-top: #333;
649 --table-tr-border-bottom: #333;
650 --table-tr-odd-background: #1e1e1e;
651 --table-td-background: #262626;
652 --table-td-border-right: #333;
653 --canvas-background: #1a1a1a;
654 --canvas-axes: #b0b0b0;
655 --canvas-grid: #333;
656 --canvas-block: #aa95e8;
657 --canvas-custom-build: #f0b165;
658 --canvas-not-custom-build: #95cce8;
659 --canvas-dep-line: #444;
660 --canvas-dep-line-highlighted: #fff;
661 --canvas-cpu: rgba(250, 119, 0, 0.2);
662 }
663}
664
665html {
666 font-family: sans-serif;
667 color: var(--text);
668 background: var(--background);
669}
670
671.canvas-container {
672 position: relative;
673 margin-top: 5px;
674 margin-bottom: 5px;
675}
676
677h1 {
678 border-bottom: 1px solid var(--h1-border-bottom);
679}
680
681.graph {
682 display: block;
683}
684
685.my-table {
686 margin-top: 20px;
687 margin-bottom: 20px;
688 border-collapse: collapse;
689 box-shadow: 0 5px 10px var(--table-box-shadow);
690}
691
692.my-table th {
693 color: var(--table-th);
694 background: var(--table-th-background);
695 border-bottom: 4px solid var(--table-th-border-bottom);
696 border-right: 1px solid var(--table-th-border-right);
697 font-size: 18px;
698 font-weight: 100;
699 padding: 12px;
700 text-align: left;
701 vertical-align: middle;
702}
703
704.my-table th:first-child {
705 border-top-left-radius: 3px;
706}
707
708.my-table th:last-child {
709 border-top-right-radius: 3px;
710 border-right:none;
711}
712
713.my-table tr {
714 border-top: 1px solid var(--table-tr-border-top);
715 border-bottom: 1px solid var(--table-tr-border-bottom);
716 font-size: 16px;
717 font-weight: normal;
718}
719
720.my-table tr:first-child {
721 border-top:none;
722}
723
724.my-table tr:last-child {
725 border-bottom:none;
726}
727
728.my-table tr:nth-child(odd) td {
729 background: var(--table-tr-odd-background);
730}
731
732.my-table tr:last-child td:first-child {
733 border-bottom-left-radius:3px;
734}
735
736.my-table tr:last-child td:last-child {
737 border-bottom-right-radius:3px;
738}
739
740.my-table td {
741 background: var(--table-td-background);
742 padding: 10px;
743 text-align: left;
744 vertical-align: middle;
745 font-weight: 300;
746 font-size: 14px;
747 border-right: 1px solid var(--table-td-border-right);
748}
749
750.my-table td:last-child {
751 border-right: 0px;
752}
753
754.summary-table td:first-child {
755 vertical-align: top;
756 text-align: right;
757}
758
759.input-table td {
760 text-align: center;
761}
762
763.error-text {
764 color: var(--error-text);
765}
766
767</style>
768</head>
769<body>
770
771<h1>Cargo Build Timings</h1>
772See <a href="https://doc.rust-lang.org/nightly/cargo/reference/timings.html">Documentation</a>
773"#;
774
775static HTML_CANVAS: &str = r#"
776<table class="input-table">
777 <tr>
778 <td><label for="min-unit-time">Min unit time:</label></td>
779 <td title="Scale corresponds to a number of pixels per second. It is automatically initialized based on your viewport width.">
780 <label for="scale">Scale:</label>
781 </td>
782 </tr>
783 <tr>
784 <td><input type="range" min="0" max="30" step="0.1" value="0" id="min-unit-time"></td>
785 <!--
786 The scale corresponds to some number of "pixels per second".
787 Its min, max, and initial values are automatically set by JavaScript on page load,
788 based on the client viewport.
789 -->
790 <td><input type="range" min="1" max="100" value="50" id="scale"></td>
791 </tr>
792 <tr>
793 <td><output for="min-unit-time" id="min-unit-time-output"></output></td>
794 <td><output for="scale" id="scale-output"></output></td>
795 </tr>
796</table>
797
798<div id="pipeline-container" class="canvas-container">
799 <canvas id="pipeline-graph" class="graph" style="position: absolute; left: 0; top: 0; z-index: 0;"></canvas>
800 <canvas id="pipeline-graph-lines" style="position: absolute; left: 0; top: 0; z-index: 1; pointer-events:none;"></canvas>
801</div>
802<div class="canvas-container">
803 <canvas id="timing-graph" class="graph"></canvas>
804</div>
805"#;