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