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};
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><label for="scale">Scale:</label></td>
780 </tr>
781 <tr>
782 <td><input type="range" min="0" max="30" step="0.1" value="0" id="min-unit-time"></td>
783 <td><input type="range" min="1" max="50" value="20" id="scale"></td>
784 </tr>
785 <tr>
786 <td><output for="min-unit-time" id="min-unit-time-output"></output></td>
787 <td><output for="scale" id="scale-output"></output></td>
788 </tr>
789</table>
790
791<div id="pipeline-container" class="canvas-container">
792 <canvas id="pipeline-graph" class="graph" style="position: absolute; left: 0; top: 0; z-index: 0;"></canvas>
793 <canvas id="pipeline-graph-lines" style="position: absolute; left: 0; top: 0; z-index: 1; pointer-events:none;"></canvas>
794</div>
795<div class="canvas-container">
796 <canvas id="timing-graph" class="graph"></canvas>
797</div>
798"#;