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