cargo/core/compiler/timings/
report.rs1use std::borrow::Cow;
4use std::collections::HashMap;
5use std::collections::HashSet;
6use std::io::Write;
7
8use indexmap::IndexMap;
9use itertools::Itertools as _;
10
11use crate::CargoResult;
12use crate::core::compiler::Unit;
13
14use super::CompilationSection;
15use super::UnitData;
16use super::UnitTime;
17
18#[derive(Clone, Hash, Eq, PartialEq)]
20pub enum SectionName {
21 Frontend,
22 Codegen,
23 Named(String),
24 Other,
25}
26
27impl SectionName {
28 fn name(&self) -> Cow<'static, str> {
30 match self {
31 SectionName::Frontend => "frontend".into(),
32 SectionName::Codegen => "codegen".into(),
33 SectionName::Named(n) => n.to_lowercase().into(),
34 SectionName::Other => "other".into(),
35 }
36 }
37
38 fn capitalized_name(&self) -> String {
39 fn capitalize(s: &str) -> String {
42 let first_char = s
43 .chars()
44 .next()
45 .map(|c| c.to_uppercase().to_string())
46 .unwrap_or_default();
47 format!("{first_char}{}", s.chars().skip(1).collect::<String>())
48 }
49 capitalize(&self.name())
50 }
51}
52
53impl serde::ser::Serialize for SectionName {
54 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
55 where
56 S: serde::Serializer,
57 {
58 self.name().serialize(serializer)
59 }
60}
61
62#[derive(Copy, Clone, serde::Serialize)]
64pub struct SectionData {
65 pub start: f64,
67 pub end: f64,
69}
70
71impl SectionData {
72 fn duration(&self) -> f64 {
73 (self.end - self.start).max(0.0)
74 }
75}
76
77#[derive(serde::Serialize)]
79pub struct Concurrency {
80 t: f64,
82 active: usize,
84 waiting: usize,
86 inactive: usize,
89}
90
91pub struct RenderContext<'a> {
92 pub start_str: String,
94 pub root_units: Vec<(String, Vec<String>)>,
98 pub profile: String,
100 pub total_fresh: u32,
102 pub total_dirty: u32,
104 pub unit_data: Vec<UnitData>,
106 pub concurrency: Vec<Concurrency>,
109 pub cpu_usage: &'a [(f64, f64)],
113 pub rustc_version: String,
115 pub host: String,
117 pub requested_targets: Vec<String>,
119 pub jobs: u32,
121 pub num_cpus: Option<u64>,
123 pub error: &'a Option<anyhow::Error>,
125}
126
127pub fn write_html(ctx: RenderContext<'_>, f: &mut impl Write) -> CargoResult<()> {
129 let duration = ctx.concurrency.last().map(|c| c.t).unwrap_or(0.0);
131 let roots: Vec<&str> = ctx
132 .root_units
133 .iter()
134 .map(|(name, _targets)| name.as_str())
135 .collect();
136 f.write_all(HTML_TMPL.replace("{ROOTS}", &roots.join(", ")).as_bytes())?;
137 write_summary_table(&ctx, f, duration)?;
138 f.write_all(HTML_CANVAS.as_bytes())?;
139 write_unit_table(&ctx, f)?;
140 writeln!(
142 f,
143 "<script>\n\
144 DURATION = {};",
145 f64::ceil(duration) as u32
146 )?;
147 write_js_data(&ctx, f)?;
148 write!(
149 f,
150 "{}\n\
151 </script>\n\
152 </body>\n\
153 </html>\n\
154 ",
155 include_str!("timings.js")
156 )?;
157
158 Ok(())
159}
160
161fn write_summary_table(
163 ctx: &RenderContext<'_>,
164 f: &mut impl Write,
165 duration: f64,
166) -> CargoResult<()> {
167 let targets = ctx
168 .root_units
169 .iter()
170 .map(|(name, targets)| format!("{} ({})", name, targets.join(", ")))
171 .collect::<Vec<_>>()
172 .join("<br>");
173
174 let total_units = ctx.total_fresh + ctx.total_dirty;
175
176 let time_human = if duration > 60.0 {
177 format!(" ({}m {:.1}s)", duration as u32 / 60, duration % 60.0)
178 } else {
179 "".to_string()
180 };
181 let total_time = format!("{:.1}s{}", duration, time_human);
182
183 let max_concurrency = ctx.concurrency.iter().map(|c| c.active).max().unwrap_or(0);
184 let num_cpus = ctx
185 .num_cpus
186 .map(|x| x.to_string())
187 .unwrap_or_else(|| "n/a".into());
188
189 let requested_targets = ctx.requested_targets.join(", ");
190
191 let error_msg = match ctx.error {
192 Some(e) => format!(r#"<tr><td class="error-text">Error:</td><td>{e}</td></tr>"#),
193 None => "".to_string(),
194 };
195
196 let RenderContext {
197 start_str,
198 profile,
199 total_fresh,
200 total_dirty,
201 rustc_version,
202 host,
203 jobs,
204 ..
205 } = &ctx;
206
207 write!(
208 f,
209 r#"
210<table class="my-table summary-table">
211<tr>
212<td>Targets:</td><td>{targets}</td>
213</tr>
214<tr>
215<td>Profile:</td><td>{profile}</td>
216</tr>
217<tr>
218<td>Fresh units:</td><td>{total_fresh}</td>
219</tr>
220<tr>
221<td>Dirty units:</td><td>{total_dirty}</td>
222</tr>
223<tr>
224<td>Total units:</td><td>{total_units}</td>
225</tr>
226<tr>
227<td>Max concurrency:</td><td>{max_concurrency} (jobs={jobs} ncpu={num_cpus})</td>
228</tr>
229<tr>
230<td>Build start:</td><td>{start_str}</td>
231</tr>
232<tr>
233<td>Total time:</td><td>{total_time}</td>
234</tr>
235<tr>
236<td>rustc:</td><td>{rustc_version}<br>Host: {host}<br>Target: {requested_targets}</td>
237</tr>
238{error_msg}
239</table>
240"#,
241 )?;
242 Ok(())
243}
244
245fn write_js_data(ctx: &RenderContext<'_>, f: &mut impl Write) -> CargoResult<()> {
248 writeln!(
249 f,
250 "const UNIT_DATA = {};",
251 serde_json::to_string_pretty(&ctx.unit_data)?
252 )?;
253 writeln!(
254 f,
255 "const CONCURRENCY_DATA = {};",
256 serde_json::to_string_pretty(&ctx.concurrency)?
257 )?;
258 writeln!(
259 f,
260 "const CPU_USAGE = {};",
261 serde_json::to_string_pretty(&ctx.cpu_usage)?
262 )?;
263 Ok(())
264}
265
266fn write_unit_table(ctx: &RenderContext<'_>, f: &mut impl Write) -> CargoResult<()> {
268 let mut units: Vec<_> = ctx.unit_data.iter().collect();
269 units.sort_unstable_by(|a, b| b.duration.partial_cmp(&a.duration).unwrap());
270
271 let aggregated: Vec<Option<_>> = units.iter().map(|u| u.sections.as_ref()).collect();
272
273 let headers: Vec<_> = aggregated
274 .iter()
275 .find_map(|s| s.as_ref())
276 .map(|sections| {
277 sections
278 .iter()
279 .filter(|(name, _)| !matches!(name, SectionName::Other))
282 .map(|s| s.0.clone())
283 .collect()
284 })
285 .unwrap_or_default();
286
287 write!(
288 f,
289 r#"
290<table class="my-table">
291<thead>
292<tr>
293 <th></th>
294 <th>Unit</th>
295 <th>Total</th>
296 {headers}
297 <th>Features</th>
298</tr>
299</thead>
300<tbody>
301"#,
302 headers = headers
303 .iter()
304 .map(|h| format!("<th>{}</th>", h.capitalized_name()))
305 .join("\n")
306 )?;
307
308 for (i, (unit, aggregated_sections)) in units.iter().zip(aggregated).enumerate() {
309 let format_duration = |section: Option<&SectionData>| match section {
310 Some(section) => {
311 let duration = section.duration();
312 let pct = (duration / unit.duration) * 100.0;
313 format!("{duration:.1}s ({:.0}%)", pct)
314 }
315 None => "".to_string(),
316 };
317
318 let mut cells: HashMap<_, _> = aggregated_sections
323 .iter()
324 .flat_map(|sections| sections.into_iter().map(|s| (&s.0, &s.1)))
325 .collect();
326
327 let cells = headers
328 .iter()
329 .map(|header| format!("<td>{}</td>", format_duration(cells.remove(header))))
330 .join("\n");
331
332 let features = unit.features.join(", ");
333 write!(
334 f,
335 r#"
336<tr>
337<td>{}.</td>
338<td>{}{}</td>
339<td>{:.1}s</td>
340{cells}
341<td>{features}</td>
342</tr>
343"#,
344 i + 1,
345 format_args!("{} v{}", unit.name, unit.version),
346 unit.target,
347 unit.duration,
348 )?;
349 }
350 write!(f, "</tbody>\n</table>\n")?;
351 Ok(())
352}
353
354pub(super) fn to_unit_data(
355 unit_times: &[UnitTime],
356 unit_map: &HashMap<Unit, u64>,
357) -> Vec<UnitData> {
358 unit_times
359 .iter()
360 .map(|ut| (unit_map[&ut.unit], ut))
361 .map(|(i, ut)| {
362 let mode = if ut.unit.mode.is_run_custom_build() {
363 "run-custom-build"
364 } else {
365 "todo"
366 }
367 .to_string();
368 let unblocked_units = ut
372 .unblocked_units
373 .iter()
374 .filter_map(|unit| unit_map.get(unit).copied())
375 .collect();
376 let unblocked_rmeta_units = ut
377 .unblocked_rmeta_units
378 .iter()
379 .filter_map(|unit| unit_map.get(unit).copied())
380 .collect();
381 let sections = aggregate_sections(ut.sections.clone(), ut.duration, ut.rmeta_time);
382
383 UnitData {
384 i,
385 name: ut.unit.pkg.name().to_string(),
386 version: ut.unit.pkg.version().to_string(),
387 mode,
388 target: ut.target.clone(),
389 features: ut.unit.features.iter().map(|f| f.to_string()).collect(),
390 start: round_to_centisecond(ut.start),
391 duration: round_to_centisecond(ut.duration),
392 unblocked_units,
393 unblocked_rmeta_units,
394 sections,
395 }
396 })
397 .collect()
398}
399
400pub fn compute_concurrency(unit_data: &[UnitData]) -> Vec<Concurrency> {
402 if unit_data.is_empty() {
403 return Vec::new();
404 }
405
406 let unit_by_index: HashMap<_, _> = unit_data.iter().map(|u| (u.i, u)).collect();
407
408 enum UnblockedBy {
409 Rmeta(u64),
410 Full(u64),
411 }
412
413 let mut unblocked_by: HashMap<_, _> = HashMap::new();
415 for unit in unit_data {
416 for id in unit.unblocked_rmeta_units.iter() {
417 assert!(
418 unblocked_by
419 .insert(*id, UnblockedBy::Rmeta(unit.i))
420 .is_none()
421 );
422 }
423
424 for id in unit.unblocked_units.iter() {
425 assert!(
426 unblocked_by
427 .insert(*id, UnblockedBy::Full(unit.i))
428 .is_none()
429 );
430 }
431 }
432
433 let ready_time = |unit: &UnitData| -> Option<f64> {
434 let dep = unblocked_by.get(&unit.i)?;
435 match dep {
436 UnblockedBy::Rmeta(id) => {
437 let dep = unit_by_index.get(id)?;
438 let duration = dep.sections.iter().flatten().find_map(|(name, section)| {
439 matches!(name, SectionName::Frontend).then_some(section.end)
440 });
441
442 Some(dep.start + duration.unwrap_or(dep.duration))
443 }
444 UnblockedBy::Full(id) => {
445 let dep = unit_by_index.get(id)?;
446 Some(dep.start + dep.duration)
447 }
448 }
449 };
450
451 #[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd)]
452 enum State {
453 Ready,
454 Start,
455 End,
456 }
457
458 let mut events: Vec<_> = unit_data
459 .iter()
460 .flat_map(|unit| {
461 let ready = ready_time(unit).unwrap_or(unit.start).min(unit.start);
464
465 [
466 (ready, State::Ready, unit.i),
467 (unit.start, State::Start, unit.i),
468 (unit.start + unit.duration, State::End, unit.i),
469 ]
470 })
471 .collect();
472
473 events.sort_by(|a, b| {
474 a.0.partial_cmp(&b.0)
475 .unwrap()
476 .then_with(|| a.1.cmp(&b.1))
477 .then_with(|| a.2.cmp(&b.2))
478 });
479
480 let mut concurrency: Vec<Concurrency> = Vec::new();
481 let mut inactive: HashSet<u64> = unit_data.iter().map(|unit| unit.i).collect();
482 let mut waiting: HashSet<u64> = HashSet::new();
483 let mut active: HashSet<u64> = HashSet::new();
484
485 for (t, state, unit_id) in events {
486 match state {
487 State::Ready => {
488 inactive.remove(&unit_id);
489 waiting.insert(unit_id);
490 active.remove(&unit_id);
491 }
492 State::Start => {
493 inactive.remove(&unit_id);
494 waiting.remove(&unit_id);
495 active.insert(unit_id);
496 }
497 State::End => {
498 inactive.remove(&unit_id);
499 waiting.remove(&unit_id);
500 active.remove(&unit_id);
501 }
502 }
503
504 let record = Concurrency {
505 t,
506 active: active.len(),
507 waiting: waiting.len(),
508 inactive: inactive.len(),
509 };
510
511 if let Some(last) = concurrency.last_mut()
512 && last.t == t
513 {
514 *last = record;
517 } else {
518 concurrency.push(record);
519 }
520 }
521
522 concurrency
523}
524
525pub fn aggregate_sections(
534 sections: IndexMap<String, CompilationSection>,
535 end: f64,
536 rmeta_time: Option<f64>,
537) -> Option<Vec<(SectionName, SectionData)>> {
538 if !sections.is_empty() {
539 let mut sections = sections.into_iter().fold(
544 vec![(
548 SectionName::Frontend,
549 SectionData {
550 start: 0.0,
551 end: round_to_centisecond(end),
552 },
553 )],
554 |mut sections, (name, section)| {
555 let previous = sections.last_mut().unwrap();
556 previous.1.end = section.start;
558
559 sections.push((
560 SectionName::Named(name),
561 SectionData {
562 start: round_to_centisecond(section.start),
563 end: round_to_centisecond(section.end.unwrap_or(end)),
564 },
565 ));
566
567 sections
568 },
569 );
570
571 if let Some((_, section)) = sections.last()
578 && section.end < end
579 {
580 sections.push((
581 SectionName::Other,
582 SectionData {
583 start: round_to_centisecond(section.end),
584 end: round_to_centisecond(end),
585 },
586 ));
587 }
588 Some(sections)
589 } else if let Some(rmeta) = rmeta_time {
590 Some(vec![
592 (
593 SectionName::Frontend,
594 SectionData {
595 start: 0.0,
596 end: round_to_centisecond(rmeta),
597 },
598 ),
599 (
600 SectionName::Codegen,
601 SectionData {
602 start: round_to_centisecond(rmeta),
603 end: round_to_centisecond(end),
604 },
605 ),
606 ])
607 } else {
608 None
610 }
611}
612
613pub fn round_to_centisecond(x: f64) -> f64 {
615 (x * 100.0).round() / 100.0
616}
617
618static HTML_TMPL: &str = r#"
619<html>
620<head>
621 <title>Cargo Build Timings — {ROOTS}</title>
622 <meta charset="utf-8">
623<style type="text/css">
624:root {
625 --error-text: #e80000;
626 --text: #000;
627 --background: #fff;
628 --h1-border-bottom: #c0c0c0;
629 --table-box-shadow: rgba(0, 0, 0, 0.1);
630 --table-th: #d5dde5;
631 --table-th-background: #1b1e24;
632 --table-th-border-bottom: #9ea7af;
633 --table-th-border-right: #343a45;
634 --table-tr-border-top: #c1c3d1;
635 --table-tr-border-bottom: #c1c3d1;
636 --table-tr-odd-background: #ebebeb;
637 --table-td-background: #ffffff;
638 --table-td-border-right: #C1C3D1;
639 --canvas-background: #f7f7f7;
640 --canvas-axes: #303030;
641 --canvas-grid: #e6e6e6;
642 --canvas-codegen: #aa95e8;
643 --canvas-link: #95e8aa;
644 --canvas-other: #e895aa;
645 --canvas-custom-build: #f0b165;
646 --canvas-not-custom-build: #95cce8;
647 --canvas-dep-line: #ddd;
648 --canvas-dep-line-highlighted: #000;
649 --canvas-cpu: rgba(250, 119, 0, 0.2);
650}
651
652@media (prefers-color-scheme: dark) {
653 :root {
654 --error-text: #e80000;
655 --text: #fff;
656 --background: #121212;
657 --h1-border-bottom: #444;
658 --table-box-shadow: rgba(255, 255, 255, 0.1);
659 --table-th: #a0a0a0;
660 --table-th-background: #2c2c2c;
661 --table-th-border-bottom: #555;
662 --table-th-border-right: #444;
663 --table-tr-border-top: #333;
664 --table-tr-border-bottom: #333;
665 --table-tr-odd-background: #1e1e1e;
666 --table-td-background: #262626;
667 --table-td-border-right: #333;
668 --canvas-background: #1a1a1a;
669 --canvas-axes: #b0b0b0;
670 --canvas-grid: #333;
671 --canvas-block: #aa95e8;
672 --canvas-custom-build: #f0b165;
673 --canvas-not-custom-build: #95cce8;
674 --canvas-dep-line: #444;
675 --canvas-dep-line-highlighted: #fff;
676 --canvas-cpu: rgba(250, 119, 0, 0.2);
677 }
678}
679
680html {
681 font-family: sans-serif;
682 color: var(--text);
683 background: var(--background);
684}
685
686.canvas-container {
687 position: relative;
688 margin-top: 5px;
689 margin-bottom: 5px;
690}
691
692h1 {
693 border-bottom: 1px solid var(--h1-border-bottom);
694}
695
696.graph {
697 display: block;
698}
699
700.my-table {
701 margin-top: 20px;
702 margin-bottom: 20px;
703 border-collapse: collapse;
704 box-shadow: 0 5px 10px var(--table-box-shadow);
705}
706
707.my-table th {
708 color: var(--table-th);
709 background: var(--table-th-background);
710 border-bottom: 4px solid var(--table-th-border-bottom);
711 border-right: 1px solid var(--table-th-border-right);
712 font-size: 18px;
713 font-weight: 100;
714 padding: 12px;
715 text-align: left;
716 vertical-align: middle;
717}
718
719.my-table th:first-child {
720 border-top-left-radius: 3px;
721}
722
723.my-table th:last-child {
724 border-top-right-radius: 3px;
725 border-right:none;
726}
727
728.my-table tr {
729 border-top: 1px solid var(--table-tr-border-top);
730 border-bottom: 1px solid var(--table-tr-border-bottom);
731 font-size: 16px;
732 font-weight: normal;
733}
734
735.my-table tr:first-child {
736 border-top:none;
737}
738
739.my-table tr:last-child {
740 border-bottom:none;
741}
742
743.my-table tr:nth-child(odd) td {
744 background: var(--table-tr-odd-background);
745}
746
747.my-table tr:last-child td:first-child {
748 border-bottom-left-radius:3px;
749}
750
751.my-table tr:last-child td:last-child {
752 border-bottom-right-radius:3px;
753}
754
755.my-table td {
756 background: var(--table-td-background);
757 padding: 10px;
758 text-align: left;
759 vertical-align: middle;
760 font-weight: 300;
761 font-size: 14px;
762 border-right: 1px solid var(--table-td-border-right);
763}
764
765.my-table td:last-child {
766 border-right: 0px;
767}
768
769.summary-table td:first-child {
770 vertical-align: top;
771 text-align: right;
772}
773
774.input-table td {
775 text-align: center;
776}
777
778.error-text {
779 color: var(--error-text);
780}
781
782</style>
783</head>
784<body>
785
786<h1>Cargo Build Timings</h1>
787See <a href="https://doc.rust-lang.org/nightly/cargo/reference/timings.html">Documentation</a>
788"#;
789
790static HTML_CANVAS: &str = r#"
791<table class="input-table">
792 <tr>
793 <td><label for="min-unit-time">Min unit time:</label></td>
794 <td title="Scale corresponds to a number of pixels per second. It is automatically initialized based on your viewport width.">
795 <label for="scale">Scale:</label>
796 </td>
797 </tr>
798 <tr>
799 <td><input type="range" min="0" max="30" step="0.1" value="0" id="min-unit-time"></td>
800 <!--
801 The scale corresponds to some number of "pixels per second".
802 Its min, max, and initial values are automatically set by JavaScript on page load,
803 based on the client viewport.
804 -->
805 <td><input type="range" min="1" max="100" value="50" id="scale"></td>
806 </tr>
807 <tr>
808 <td><output for="min-unit-time" id="min-unit-time-output"></output></td>
809 <td><output for="scale" id="scale-output"></output></td>
810 </tr>
811</table>
812
813<div id="pipeline-container" class="canvas-container">
814 <canvas id="pipeline-graph" class="graph" style="position: absolute; left: 0; top: 0; z-index: 0;"></canvas>
815 <canvas id="pipeline-graph-lines" style="position: absolute; left: 0; top: 0; z-index: 1; pointer-events:none;"></canvas>
816</div>
817<div class="canvas-container">
818 <canvas id="timing-graph" class="graph"></canvas>
819</div>
820"#;