1use crate::util::data_structures::HashMap;
4use crate::util::data_structures::HashSet;
5use std::borrow::Cow;
6use std::io::Write;
7
8use crate::util::data_structures::IndexMap;
9use itertools::Itertools as _;
10
11use crate::CargoResult;
12use crate::compiler::UnitIndex;
13use crate::util::HumanBytes;
14
15use super::CompilationSection;
16use super::UnitData;
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 write_memory_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
355fn write_memory_table(ctx: &RenderContext<'_>, f: &mut impl Write) -> CargoResult<()> {
360 let mut units: Vec<_> = ctx
361 .unit_data
362 .iter()
363 .filter(|u| u.peak_memory.is_some())
364 .collect();
365 if units.is_empty() {
366 return Ok(());
367 }
368 units.sort_unstable_by(|a, b| b.peak_memory.cmp(&a.peak_memory));
369
370 write!(
371 f,
372 r#"
373<h2>Peak memory usage per unit</h2>
374<table class="my-table">
375<thead>
376<tr>
377 <th></th>
378 <th>Unit</th>
379 <th>Peak memory</th>
380</tr>
381</thead>
382<tbody>
383"#
384 )?;
385 for (i, unit) in units.iter().enumerate() {
386 write!(
387 f,
388 r#"
389<tr>
390<td>{}.</td>
391<td>{}{}</td>
392<td>{}</td>
393</tr>
394"#,
395 i + 1,
396 format_args!("{} v{}", unit.name, unit.version),
397 unit.target,
398 format_args!("{:.1}", HumanBytes(unit.peak_memory.unwrap())),
399 )?;
400 }
401 write!(f, "</tbody>\n</table>\n")?;
402 Ok(())
403}
404
405pub fn compute_concurrency(unit_data: &[UnitData]) -> Vec<Concurrency> {
407 if unit_data.is_empty() {
408 return Vec::new();
409 }
410
411 let unit_by_index: HashMap<_, _> = unit_data.iter().map(|u| (u.i, u)).collect();
412
413 enum UnblockedBy {
414 Rmeta(UnitIndex),
415 Full(UnitIndex),
416 }
417
418 let mut unblocked_by: HashMap<_, _> = HashMap::default();
420 for unit in unit_data {
421 for id in unit.unblocked_rmeta_units.iter() {
422 assert!(
423 unblocked_by
424 .insert(*id, UnblockedBy::Rmeta(unit.i))
425 .is_none()
426 );
427 }
428
429 for id in unit.unblocked_units.iter() {
430 assert!(
431 unblocked_by
432 .insert(*id, UnblockedBy::Full(unit.i))
433 .is_none()
434 );
435 }
436 }
437
438 let ready_time = |unit: &UnitData| -> Option<f64> {
439 let dep = unblocked_by.get(&unit.i)?;
440 match dep {
441 UnblockedBy::Rmeta(id) => {
442 let dep = unit_by_index.get(id)?;
443 let duration = dep.sections.iter().flatten().find_map(|(name, section)| {
444 matches!(name, SectionName::Frontend).then_some(section.end)
445 });
446
447 Some(dep.start + duration.unwrap_or(dep.duration))
448 }
449 UnblockedBy::Full(id) => {
450 let dep = unit_by_index.get(id)?;
451 Some(dep.start + dep.duration)
452 }
453 }
454 };
455
456 #[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd)]
457 enum State {
458 Ready,
459 Start,
460 End,
461 }
462
463 let mut events: Vec<_> = unit_data
464 .iter()
465 .flat_map(|unit| {
466 let ready = ready_time(unit).unwrap_or(unit.start).min(unit.start);
469
470 [
471 (ready, State::Ready, unit.i),
472 (unit.start, State::Start, unit.i),
473 (unit.start + unit.duration, State::End, unit.i),
474 ]
475 })
476 .collect();
477
478 events.sort_by(|a, b| {
479 a.0.partial_cmp(&b.0)
480 .unwrap()
481 .then_with(|| a.1.cmp(&b.1))
482 .then_with(|| a.2.cmp(&b.2))
483 });
484
485 let mut concurrency: Vec<Concurrency> = Vec::new();
486 let mut inactive: HashSet<UnitIndex> = unit_data.iter().map(|unit| unit.i).collect();
487 let mut waiting: HashSet<UnitIndex> = HashSet::default();
488 let mut active: HashSet<UnitIndex> = HashSet::default();
489
490 for (t, state, unit_id) in events {
491 match state {
492 State::Ready => {
493 inactive.remove(&unit_id);
494 waiting.insert(unit_id);
495 active.remove(&unit_id);
496 }
497 State::Start => {
498 inactive.remove(&unit_id);
499 waiting.remove(&unit_id);
500 active.insert(unit_id);
501 }
502 State::End => {
503 inactive.remove(&unit_id);
504 waiting.remove(&unit_id);
505 active.remove(&unit_id);
506 }
507 }
508
509 let record = Concurrency {
510 t,
511 active: active.len(),
512 waiting: waiting.len(),
513 inactive: inactive.len(),
514 };
515
516 if let Some(last) = concurrency.last_mut()
517 && last.t == t
518 {
519 *last = record;
522 } else {
523 concurrency.push(record);
524 }
525 }
526
527 concurrency
528}
529
530pub fn aggregate_sections(
539 sections: IndexMap<String, CompilationSection>,
540 end: f64,
541 rmeta_time: Option<f64>,
542) -> Option<Vec<(SectionName, SectionData)>> {
543 if !sections.is_empty() {
544 let mut sections = sections.into_iter().fold(
549 vec![(
553 SectionName::Frontend,
554 SectionData {
555 start: 0.0,
556 end: round_to_centisecond(end),
557 },
558 )],
559 |mut sections, (name, section)| {
560 let previous = sections.last_mut().unwrap();
561 previous.1.end = section.start;
563
564 sections.push((
565 SectionName::Named(name),
566 SectionData {
567 start: round_to_centisecond(section.start),
568 end: round_to_centisecond(section.end.unwrap_or(end)),
569 },
570 ));
571
572 sections
573 },
574 );
575
576 if let Some((_, section)) = sections.last()
583 && section.end < end
584 {
585 sections.push((
586 SectionName::Other,
587 SectionData {
588 start: round_to_centisecond(section.end),
589 end: round_to_centisecond(end),
590 },
591 ));
592 }
593 Some(sections)
594 } else if let Some(rmeta) = rmeta_time {
595 Some(vec![
597 (
598 SectionName::Frontend,
599 SectionData {
600 start: 0.0,
601 end: round_to_centisecond(rmeta),
602 },
603 ),
604 (
605 SectionName::Codegen,
606 SectionData {
607 start: round_to_centisecond(rmeta),
608 end: round_to_centisecond(end),
609 },
610 ),
611 ])
612 } else {
613 None
615 }
616}
617
618pub fn round_to_centisecond(x: f64) -> f64 {
620 (x * 100.0).round() / 100.0
621}
622
623static HTML_TMPL: &str = r#"
624<html>
625<head>
626 <title>Cargo Build Timings — {ROOTS}</title>
627 <meta charset="utf-8">
628<style type="text/css">
629:root {
630 --error-text: #e80000;
631 --text: #000;
632 --background: #fff;
633 --h1-border-bottom: #c0c0c0;
634 --table-box-shadow: rgba(0, 0, 0, 0.1);
635 --table-th: #d5dde5;
636 --table-th-background: #1b1e24;
637 --table-th-border-bottom: #9ea7af;
638 --table-th-border-right: #343a45;
639 --table-tr-border-top: #c1c3d1;
640 --table-tr-border-bottom: #c1c3d1;
641 --table-tr-odd-background: #ebebeb;
642 --table-td-background: #ffffff;
643 --table-td-border-right: #C1C3D1;
644 --canvas-background: #f7f7f7;
645 --canvas-axes: #303030;
646 --canvas-grid: #e6e6e6;
647 --canvas-codegen: #aa95e8;
648 --canvas-link: #95e8aa;
649 --canvas-other: #e895aa;
650 --canvas-custom-build: #f0b165;
651 --canvas-not-custom-build: #95cce8;
652 --canvas-dep-line: #ddd;
653 --canvas-dep-line-highlighted: #000;
654 --canvas-cpu: rgba(250, 119, 0, 0.2);
655}
656
657@media (prefers-color-scheme: dark) {
658 :root {
659 --error-text: #e80000;
660 --text: #fff;
661 --background: #121212;
662 --h1-border-bottom: #444;
663 --table-box-shadow: rgba(255, 255, 255, 0.1);
664 --table-th: #a0a0a0;
665 --table-th-background: #2c2c2c;
666 --table-th-border-bottom: #555;
667 --table-th-border-right: #444;
668 --table-tr-border-top: #333;
669 --table-tr-border-bottom: #333;
670 --table-tr-odd-background: #1e1e1e;
671 --table-td-background: #262626;
672 --table-td-border-right: #333;
673 --canvas-background: #1a1a1a;
674 --canvas-axes: #b0b0b0;
675 --canvas-grid: #333;
676 --canvas-block: #aa95e8;
677 --canvas-custom-build: #f0b165;
678 --canvas-not-custom-build: #95cce8;
679 --canvas-dep-line: #444;
680 --canvas-dep-line-highlighted: #fff;
681 --canvas-cpu: rgba(250, 119, 0, 0.2);
682 }
683}
684
685html {
686 font-family: sans-serif;
687 color: var(--text);
688 background: var(--background);
689}
690
691.canvas-container {
692 position: relative;
693 margin-top: 5px;
694 margin-bottom: 5px;
695}
696
697.canvas-container.hidden {
698 display: none;
699}
700
701h1 {
702 border-bottom: 1px solid var(--h1-border-bottom);
703}
704
705.graph {
706 display: block;
707}
708
709.my-table {
710 margin-top: 20px;
711 margin-bottom: 20px;
712 border-collapse: collapse;
713 box-shadow: 0 5px 10px var(--table-box-shadow);
714}
715
716.my-table th {
717 color: var(--table-th);
718 background: var(--table-th-background);
719 border-bottom: 4px solid var(--table-th-border-bottom);
720 border-right: 1px solid var(--table-th-border-right);
721 font-size: 18px;
722 font-weight: 100;
723 padding: 12px;
724 text-align: left;
725 vertical-align: middle;
726}
727
728.my-table th:first-child {
729 border-top-left-radius: 3px;
730}
731
732.my-table th:last-child {
733 border-top-right-radius: 3px;
734 border-right:none;
735}
736
737.my-table tr {
738 border-top: 1px solid var(--table-tr-border-top);
739 border-bottom: 1px solid var(--table-tr-border-bottom);
740 font-size: 16px;
741 font-weight: normal;
742}
743
744.my-table tr:first-child {
745 border-top:none;
746}
747
748.my-table tr:last-child {
749 border-bottom:none;
750}
751
752.my-table tr:nth-child(odd) td {
753 background: var(--table-tr-odd-background);
754}
755
756.my-table tr:last-child td:first-child {
757 border-bottom-left-radius:3px;
758}
759
760.my-table tr:last-child td:last-child {
761 border-bottom-right-radius:3px;
762}
763
764.my-table td {
765 background: var(--table-td-background);
766 padding: 10px;
767 text-align: left;
768 vertical-align: middle;
769 font-weight: 300;
770 font-size: 14px;
771 border-right: 1px solid var(--table-td-border-right);
772}
773
774.my-table td:last-child {
775 border-right: 0px;
776}
777
778.summary-table td:first-child {
779 vertical-align: top;
780 text-align: right;
781}
782
783.input-table td {
784 text-align: center;
785}
786
787.error-text {
788 color: var(--error-text);
789}
790
791</style>
792</head>
793<body>
794
795<h1>Cargo Build Timings</h1>
796See <a href="https://doc.rust-lang.org/nightly/cargo/reference/timings.html">Documentation</a>
797"#;
798
799static HTML_CANVAS: &str = r#"
800<table class="input-table">
801 <tr>
802 <td><label for="min-unit-time">Min unit time:</label></td>
803 <td title="Scale corresponds to a number of pixels per second. It is automatically initialized based on your viewport width.">
804 <label for="scale">Scale:</label>
805 </td>
806 </tr>
807 <tr>
808 <td><input type="range" min="0" max="30" step="0.1" value="0" id="min-unit-time"></td>
809 <!--
810 The scale corresponds to some number of "pixels per second".
811 Its min, max, and initial values are automatically set by JavaScript on page load,
812 based on the client viewport.
813 -->
814 <td><input type="range" min="1" max="100" value="50" id="scale"></td>
815 </tr>
816 <tr>
817 <td><output for="min-unit-time" id="min-unit-time-output"></output></td>
818 <td><output for="scale" id="scale-output"></output></td>
819 <td></td>
820 </tr>
821</table>
822
823<div id="pipeline-container" class="canvas-container"></div>
824<div id="timing-container" class="canvas-container"></div>
825"#;