1use cargo_util::collections::HashMap;
4use cargo_util::collections::HashSet;
5use std::borrow::Cow;
6use std::io::Write;
7
8use cargo_util::collections::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 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 let show_memory = units.iter().any(|u| u.peak_memory.is_some());
288
289 write!(
290 f,
291 r#"
292<table class="my-table" id="unit-table">
293<thead>
294<tr>
295 <th></th>
296 <th>Unit</th>
297 <th aria-sort="descending">Total</th>
298 {headers}
299 {memory_header}
300 <th>Features</th>
301</tr>
302</thead>
303<tbody>
304"#,
305 headers = headers
306 .iter()
307 .map(|h| format!("<th>{}</th>", h.capitalized_name()))
308 .join("\n"),
309 memory_header = if show_memory {
310 r#"<th title="Peak memory (RSS)">Memory</th>"#
311 } else {
312 ""
313 },
314 )?;
315
316 for (i, (unit, aggregated_sections)) in units.iter().zip(aggregated).enumerate() {
317 let UnitData {
318 name,
319 version,
320 target,
321 duration,
322 peak_memory,
323 features,
324 ..
325 } = unit;
326
327 let section_cell = |section: Option<&SectionData>| match section {
328 Some(section) => {
329 let section_duration = section.duration();
330 let pct = (section_duration / duration) * 100.0;
331 format!(
332 r#"<td data-sort="{section_duration}">{section_duration:.1}s ({pct:.0}%)</td>"#
333 )
334 }
335 None => "<td></td>".to_string(),
336 };
337
338 let mut cells: HashMap<_, _> = aggregated_sections
343 .iter()
344 .flat_map(|sections| sections.into_iter().map(|s| (&s.0, &s.1)))
345 .collect();
346
347 let cells = headers
348 .iter()
349 .map(|header| section_cell(cells.remove(header)))
350 .join("\n");
351
352 let memory = match (show_memory, *peak_memory) {
353 (false, _) => String::new(),
354 (true, Some(bytes)) => {
355 format!(r#"<td data-sort="{bytes}">{:.1}</td>"#, HumanBytes(bytes))
356 }
357 (true, None) => "<td></td>".to_string(),
358 };
359
360 let features = features.join(", ");
361 write!(
362 f,
363 r#"
364<tr>
365<td>{}.</td>
366<td>{name} v{version}{target}</td>
367<td data-sort="{duration}">{duration:.1}s</td>
368{cells}
369{memory}
370<td>{features}</td>
371</tr>
372"#,
373 i + 1,
374 )?;
375 }
376 write!(f, "</tbody>\n</table>\n")?;
377 Ok(())
378}
379
380pub fn compute_concurrency(unit_data: &[UnitData]) -> Vec<Concurrency> {
382 if unit_data.is_empty() {
383 return Vec::new();
384 }
385
386 let unit_by_index: HashMap<_, _> = unit_data.iter().map(|u| (u.i, u)).collect();
387
388 enum UnblockedBy {
389 Rmeta(UnitIndex),
390 Full(UnitIndex),
391 }
392
393 let mut unblocked_by: HashMap<_, _> = HashMap::default();
395 for unit in unit_data {
396 for id in unit.unblocked_rmeta_units.iter() {
397 assert!(
398 unblocked_by
399 .insert(*id, UnblockedBy::Rmeta(unit.i))
400 .is_none()
401 );
402 }
403
404 for id in unit.unblocked_units.iter() {
405 assert!(
406 unblocked_by
407 .insert(*id, UnblockedBy::Full(unit.i))
408 .is_none()
409 );
410 }
411 }
412
413 let ready_time = |unit: &UnitData| -> Option<f64> {
414 let dep = unblocked_by.get(&unit.i)?;
415 match dep {
416 UnblockedBy::Rmeta(id) => {
417 let dep = unit_by_index.get(id)?;
418 let duration = dep.sections.iter().flatten().find_map(|(name, section)| {
419 matches!(name, SectionName::Frontend).then_some(section.end)
420 });
421
422 Some(dep.start + duration.unwrap_or(dep.duration))
423 }
424 UnblockedBy::Full(id) => {
425 let dep = unit_by_index.get(id)?;
426 Some(dep.start + dep.duration)
427 }
428 }
429 };
430
431 #[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd)]
432 enum State {
433 Ready,
434 Start,
435 End,
436 }
437
438 let mut events: Vec<_> = unit_data
439 .iter()
440 .flat_map(|unit| {
441 let ready = ready_time(unit).unwrap_or(unit.start).min(unit.start);
444
445 [
446 (ready, State::Ready, unit.i),
447 (unit.start, State::Start, unit.i),
448 (unit.start + unit.duration, State::End, unit.i),
449 ]
450 })
451 .collect();
452
453 events.sort_by(|a, b| {
454 a.0.partial_cmp(&b.0)
455 .unwrap()
456 .then_with(|| a.1.cmp(&b.1))
457 .then_with(|| a.2.cmp(&b.2))
458 });
459
460 let mut concurrency: Vec<Concurrency> = Vec::new();
461 let mut inactive: HashSet<UnitIndex> = unit_data.iter().map(|unit| unit.i).collect();
462 let mut waiting: HashSet<UnitIndex> = HashSet::default();
463 let mut active: HashSet<UnitIndex> = HashSet::default();
464
465 for (t, state, unit_id) in events {
466 match state {
467 State::Ready => {
468 inactive.remove(&unit_id);
469 waiting.insert(unit_id);
470 active.remove(&unit_id);
471 }
472 State::Start => {
473 inactive.remove(&unit_id);
474 waiting.remove(&unit_id);
475 active.insert(unit_id);
476 }
477 State::End => {
478 inactive.remove(&unit_id);
479 waiting.remove(&unit_id);
480 active.remove(&unit_id);
481 }
482 }
483
484 let record = Concurrency {
485 t,
486 active: active.len(),
487 waiting: waiting.len(),
488 inactive: inactive.len(),
489 };
490
491 if let Some(last) = concurrency.last_mut()
492 && last.t == t
493 {
494 *last = record;
497 } else {
498 concurrency.push(record);
499 }
500 }
501
502 concurrency
503}
504
505pub fn aggregate_sections(
514 sections: IndexMap<String, CompilationSection>,
515 end: f64,
516 rmeta_time: Option<f64>,
517) -> Option<Vec<(SectionName, SectionData)>> {
518 if !sections.is_empty() {
519 let mut sections = sections.into_iter().fold(
524 vec![(
528 SectionName::Frontend,
529 SectionData {
530 start: 0.0,
531 end: round_to_centisecond(end),
532 },
533 )],
534 |mut sections, (name, section)| {
535 let previous = sections.last_mut().unwrap();
536 previous.1.end = section.start;
538
539 sections.push((
540 SectionName::Named(name),
541 SectionData {
542 start: round_to_centisecond(section.start),
543 end: round_to_centisecond(section.end.unwrap_or(end)),
544 },
545 ));
546
547 sections
548 },
549 );
550
551 if let Some((_, section)) = sections.last()
558 && section.end < end
559 {
560 sections.push((
561 SectionName::Other,
562 SectionData {
563 start: round_to_centisecond(section.end),
564 end: round_to_centisecond(end),
565 },
566 ));
567 }
568 Some(sections)
569 } else if let Some(rmeta) = rmeta_time {
570 Some(vec![
572 (
573 SectionName::Frontend,
574 SectionData {
575 start: 0.0,
576 end: round_to_centisecond(rmeta),
577 },
578 ),
579 (
580 SectionName::Codegen,
581 SectionData {
582 start: round_to_centisecond(rmeta),
583 end: round_to_centisecond(end),
584 },
585 ),
586 ])
587 } else {
588 None
590 }
591}
592
593pub fn round_to_centisecond(x: f64) -> f64 {
595 (x * 100.0).round() / 100.0
596}
597
598static HTML_TMPL: &str = r#"
599<html>
600<head>
601 <title>Cargo Build Timings — {ROOTS}</title>
602 <meta charset="utf-8">
603<style type="text/css">
604:root {
605 --error-text: #e80000;
606 --text: #000;
607 --background: #fff;
608 --h1-border-bottom: #c0c0c0;
609 --table-box-shadow: rgba(0, 0, 0, 0.1);
610 --table-th: #d5dde5;
611 --table-th-background: #1b1e24;
612 --table-th-border-bottom: #9ea7af;
613 --table-th-border-right: #343a45;
614 --table-tr-border-top: #c1c3d1;
615 --table-tr-border-bottom: #c1c3d1;
616 --table-tr-odd-background: #ebebeb;
617 --table-td-background: #ffffff;
618 --table-td-border-right: #C1C3D1;
619 --canvas-background: #f7f7f7;
620 --canvas-axes: #303030;
621 --canvas-grid: #e6e6e6;
622 --canvas-codegen: #aa95e8;
623 --canvas-link: #95e8aa;
624 --canvas-other: #e895aa;
625 --canvas-custom-build: #f0b165;
626 --canvas-not-custom-build: #95cce8;
627 --canvas-dep-line: #ddd;
628 --canvas-dep-line-highlighted: #000;
629 --canvas-cpu: rgba(250, 119, 0, 0.2);
630}
631
632@media (prefers-color-scheme: dark) {
633 :root {
634 --error-text: #e80000;
635 --text: #fff;
636 --background: #121212;
637 --h1-border-bottom: #444;
638 --table-box-shadow: rgba(255, 255, 255, 0.1);
639 --table-th: #a0a0a0;
640 --table-th-background: #2c2c2c;
641 --table-th-border-bottom: #555;
642 --table-th-border-right: #444;
643 --table-tr-border-top: #333;
644 --table-tr-border-bottom: #333;
645 --table-tr-odd-background: #1e1e1e;
646 --table-td-background: #262626;
647 --table-td-border-right: #333;
648 --canvas-background: #1a1a1a;
649 --canvas-axes: #b0b0b0;
650 --canvas-grid: #333;
651 --canvas-block: #aa95e8;
652 --canvas-custom-build: #f0b165;
653 --canvas-not-custom-build: #95cce8;
654 --canvas-dep-line: #444;
655 --canvas-dep-line-highlighted: #fff;
656 --canvas-cpu: rgba(250, 119, 0, 0.2);
657 }
658}
659
660html {
661 font-family: sans-serif;
662 color: var(--text);
663 background: var(--background);
664}
665
666.canvas-container {
667 position: relative;
668 margin-top: 5px;
669 margin-bottom: 5px;
670}
671
672.canvas-container.hidden {
673 display: none;
674}
675
676h1 {
677 border-bottom: 1px solid var(--h1-border-bottom);
678}
679
680.graph {
681 display: block;
682}
683
684.my-table {
685 margin-top: 20px;
686 margin-bottom: 20px;
687 border-collapse: collapse;
688 box-shadow: 0 5px 10px var(--table-box-shadow);
689}
690
691.my-table th {
692 color: var(--table-th);
693 background: var(--table-th-background);
694 border-bottom: 4px solid var(--table-th-border-bottom);
695 border-right: 1px solid var(--table-th-border-right);
696 font-size: 18px;
697 font-weight: 100;
698 padding: 12px;
699 text-align: left;
700 vertical-align: middle;
701}
702
703.my-table th:first-child {
704 border-top-left-radius: 3px;
705}
706
707.my-table th:last-child {
708 border-top-right-radius: 3px;
709 border-right:none;
710}
711
712.my-table tr {
713 border-top: 1px solid var(--table-tr-border-top);
714 border-bottom: 1px solid var(--table-tr-border-bottom);
715 font-size: 16px;
716 font-weight: normal;
717}
718
719.my-table tr:first-child {
720 border-top:none;
721}
722
723.my-table tr:last-child {
724 border-bottom:none;
725}
726
727.my-table tr:nth-child(odd) td {
728 background: var(--table-tr-odd-background);
729}
730
731.my-table tr:last-child td:first-child {
732 border-bottom-left-radius:3px;
733}
734
735.my-table tr:last-child td:last-child {
736 border-bottom-right-radius:3px;
737}
738
739.my-table td {
740 background: var(--table-td-background);
741 padding: 10px;
742 text-align: left;
743 vertical-align: middle;
744 font-weight: 300;
745 font-size: 14px;
746 border-right: 1px solid var(--table-td-border-right);
747}
748
749.my-table td:last-child {
750 border-right: 0px;
751}
752
753.summary-table td:first-child {
754 vertical-align: top;
755 text-align: right;
756}
757
758#unit-table th:not(:first-child) {
759 cursor: pointer;
760 position: relative;
761 /* Room for the sort arrows. */
762 padding-right: 28px;
763}
764
765/*
766 * Sort arrows:
767 * an up and a down triangle drawn with borders in the text color.
768 */
769#unit-table th:not(:first-child)::before,
770#unit-table th:not(:first-child)::after {
771 content: "";
772 position: absolute;
773 right: 10px;
774 border-left: 4px solid transparent;
775 border-right: 4px solid transparent;
776 opacity: 0.3;
777}
778
779#unit-table th:not(:first-child)::before {
780 bottom: 50%;
781 margin-bottom: 1px;
782 border-bottom: 5px solid currentColor;
783}
784
785#unit-table th:not(:first-child)::after {
786 top: 50%;
787 margin-top: 1px;
788 border-top: 5px solid currentColor;
789}
790
791#unit-table th[aria-sort="ascending"]::before,
792#unit-table th[aria-sort="descending"]::after {
793 opacity: 1;
794}
795
796.input-table td {
797 text-align: center;
798}
799
800.error-text {
801 color: var(--error-text);
802}
803
804</style>
805</head>
806<body>
807
808<h1>Cargo Build Timings</h1>
809See <a href="https://doc.rust-lang.org/nightly/cargo/reference/timings.html">Documentation</a>
810"#;
811
812static HTML_CANVAS: &str = r#"
813<table class="input-table">
814 <tr>
815 <td><label for="min-unit-time">Min unit time:</label></td>
816 <td title="Scale corresponds to a number of pixels per second. It is automatically initialized based on your viewport width.">
817 <label for="scale">Scale:</label>
818 </td>
819 </tr>
820 <tr>
821 <td><input type="range" min="0" max="30" step="0.1" value="0" id="min-unit-time"></td>
822 <!--
823 The scale corresponds to some number of "pixels per second".
824 Its min, max, and initial values are automatically set by JavaScript on page load,
825 based on the client viewport.
826 -->
827 <td><input type="range" min="1" max="100" value="50" id="scale"></td>
828 </tr>
829 <tr>
830 <td><output for="min-unit-time" id="min-unit-time-output"></output></td>
831 <td><output for="scale" id="scale-output"></output></td>
832 <td></td>
833 </tr>
834</table>
835
836<div id="pipeline-container" class="canvas-container"></div>
837<div id="timing-container" class="canvas-container"></div>
838"#;