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