cargo/core/compiler/timings/
report.rs1use std::collections::HashMap;
4use std::io::Write;
5use std::time::Instant;
6
7use itertools::Itertools as _;
8
9use crate::CargoResult;
10use crate::core::compiler::CompilationSection;
11use crate::core::compiler::Unit;
12
13use super::Concurrency;
14use super::UnitData;
15use super::UnitTime;
16
17const FRONTEND_SECTION_NAME: &str = "Frontend";
18const CODEGEN_SECTION_NAME: &str = "Codegen";
19
20enum AggregatedSections {
22 Sections(Vec<(String, SectionData)>),
24 OnlyMetadataTime {
26 frontend: SectionData,
27 codegen: SectionData,
28 },
29 OnlyTotalDuration,
31}
32
33#[derive(Copy, Clone, serde::Serialize)]
35pub(super) struct SectionData {
36 start: f64,
38 end: f64,
40}
41
42impl SectionData {
43 fn duration(&self) -> f64 {
44 (self.end - self.start).max(0.0)
45 }
46}
47
48pub struct RenderContext<'a> {
49 pub start: Instant,
51 pub start_str: &'a str,
53 pub root_units: &'a [(String, Vec<String>)],
57 pub profile: &'a str,
59 pub total_fresh: u32,
61 pub total_dirty: u32,
63 pub unit_times: &'a [UnitTime],
65 pub concurrency: &'a [Concurrency],
68 pub cpu_usage: &'a [(f64, f64)],
72 pub rustc_version: &'a str,
74 pub host: &'a str,
76 pub requested_targets: &'a [&'a str],
78 pub jobs: u32,
80 pub error: &'a Option<anyhow::Error>,
82}
83
84pub(super) fn write_html(ctx: RenderContext<'_>, f: &mut impl Write) -> CargoResult<()> {
86 let duration = ctx.start.elapsed().as_secs_f64();
87 let roots: Vec<&str> = ctx
88 .root_units
89 .iter()
90 .map(|(name, _targets)| name.as_str())
91 .collect();
92 f.write_all(HTML_TMPL.replace("{ROOTS}", &roots.join(", ")).as_bytes())?;
93 write_summary_table(&ctx, f, duration)?;
94 f.write_all(HTML_CANVAS.as_bytes())?;
95 write_unit_table(&ctx, f)?;
96 writeln!(
98 f,
99 "<script>\n\
100 DURATION = {};",
101 f64::ceil(duration) as u32
102 )?;
103 write_js_data(&ctx, f)?;
104 write!(
105 f,
106 "{}\n\
107 </script>\n\
108 </body>\n\
109 </html>\n\
110 ",
111 include_str!("timings.js")
112 )?;
113
114 Ok(())
115}
116
117fn write_summary_table(
119 ctx: &RenderContext<'_>,
120 f: &mut impl Write,
121 duration: f64,
122) -> CargoResult<()> {
123 let targets = ctx
124 .root_units
125 .iter()
126 .map(|(name, targets)| format!("{} ({})", name, targets.join(", ")))
127 .collect::<Vec<_>>()
128 .join("<br>");
129
130 let total_units = ctx.total_fresh + ctx.total_dirty;
131
132 let time_human = if duration > 60.0 {
133 format!(" ({}m {:.1}s)", duration as u32 / 60, duration % 60.0)
134 } else {
135 "".to_string()
136 };
137 let total_time = format!("{:.1}s{}", duration, time_human);
138
139 let max_concurrency = ctx.concurrency.iter().map(|c| c.active).max().unwrap();
140 let num_cpus = std::thread::available_parallelism()
141 .map(|x| x.get().to_string())
142 .unwrap_or_else(|_| "n/a".into());
143
144 let requested_targets = ctx.requested_targets.join(", ");
145
146 let error_msg = match ctx.error {
147 Some(e) => format!(r#"<tr><td class="error-text">Error:</td><td>{e}</td></tr>"#),
148 None => "".to_string(),
149 };
150
151 let RenderContext {
152 start_str,
153 profile,
154 total_fresh,
155 total_dirty,
156 rustc_version,
157 host,
158 jobs,
159 ..
160 } = &ctx;
161
162 write!(
163 f,
164 r#"
165<table class="my-table summary-table">
166<tr>
167<td>Targets:</td><td>{targets}</td>
168</tr>
169<tr>
170<td>Profile:</td><td>{profile}</td>
171</tr>
172<tr>
173<td>Fresh units:</td><td>{total_fresh}</td>
174</tr>
175<tr>
176<td>Dirty units:</td><td>{total_dirty}</td>
177</tr>
178<tr>
179<td>Total units:</td><td>{total_units}</td>
180</tr>
181<tr>
182<td>Max concurrency:</td><td>{max_concurrency} (jobs={jobs} ncpu={num_cpus})</td>
183</tr>
184<tr>
185<td>Build start:</td><td>{start_str}</td>
186</tr>
187<tr>
188<td>Total time:</td><td>{total_time}</td>
189</tr>
190<tr>
191<td>rustc:</td><td>{rustc_version}<br>Host: {host}<br>Target: {requested_targets}</td>
192</tr>
193{error_msg}
194</table>
195"#,
196 )?;
197 Ok(())
198}
199
200fn write_js_data(ctx: &RenderContext<'_>, f: &mut impl Write) -> CargoResult<()> {
203 let unit_data = to_unit_data(&ctx.unit_times);
204
205 writeln!(
206 f,
207 "const UNIT_DATA = {};",
208 serde_json::to_string_pretty(&unit_data)?
209 )?;
210 writeln!(
211 f,
212 "const CONCURRENCY_DATA = {};",
213 serde_json::to_string_pretty(&ctx.concurrency)?
214 )?;
215 writeln!(
216 f,
217 "const CPU_USAGE = {};",
218 serde_json::to_string_pretty(&ctx.cpu_usage)?
219 )?;
220 Ok(())
221}
222
223fn write_unit_table(ctx: &RenderContext<'_>, f: &mut impl Write) -> CargoResult<()> {
225 let mut units: Vec<&UnitTime> = ctx.unit_times.iter().collect();
226 units.sort_unstable_by(|a, b| b.duration.partial_cmp(&a.duration).unwrap());
227
228 fn capitalize(s: &str) -> String {
231 let first_char = s
232 .chars()
233 .next()
234 .map(|c| c.to_uppercase().to_string())
235 .unwrap_or_default();
236 format!("{first_char}{}", s.chars().skip(1).collect::<String>())
237 }
238
239 let aggregated: Vec<AggregatedSections> = units
245 .iter()
246 .map(|u|
247 match aggregate_sections(u) {
251 AggregatedSections::Sections(sections) => AggregatedSections::Sections(
252 sections.into_iter()
253 .map(|(name, data)| (capitalize(&name), data))
254 .collect()
255 ),
256 s => s
257 })
258 .collect();
259
260 let headers: Vec<String> = if let Some(sections) = aggregated.iter().find_map(|s| match s {
261 AggregatedSections::Sections(sections) => Some(sections),
262 _ => None,
263 }) {
264 sections.into_iter().map(|s| s.0.clone()).collect()
265 } else if aggregated
266 .iter()
267 .any(|s| matches!(s, AggregatedSections::OnlyMetadataTime { .. }))
268 {
269 vec![
270 FRONTEND_SECTION_NAME.to_string(),
271 CODEGEN_SECTION_NAME.to_string(),
272 ]
273 } else {
274 vec![]
275 };
276
277 write!(
278 f,
279 r#"
280<table class="my-table">
281<thead>
282<tr>
283 <th></th>
284 <th>Unit</th>
285 <th>Total</th>
286 {headers}
287 <th>Features</th>
288</tr>
289</thead>
290<tbody>
291"#,
292 headers = headers.iter().map(|h| format!("<th>{h}</th>")).join("\n")
293 )?;
294
295 for (i, (unit, aggregated_sections)) in units.iter().zip(aggregated).enumerate() {
296 let format_duration = |section: Option<SectionData>| match section {
297 Some(section) => {
298 let duration = section.duration();
299 let pct = (duration / unit.duration) * 100.0;
300 format!("{duration:.1}s ({:.0}%)", pct)
301 }
302 None => "".to_string(),
303 };
304
305 let mut cells: HashMap<&str, SectionData> = Default::default();
310
311 match &aggregated_sections {
312 AggregatedSections::Sections(sections) => {
313 for (name, data) in sections {
314 cells.insert(&name, *data);
315 }
316 }
317 AggregatedSections::OnlyMetadataTime { frontend, codegen } => {
318 cells.insert(FRONTEND_SECTION_NAME, *frontend);
319 cells.insert(CODEGEN_SECTION_NAME, *codegen);
320 }
321 AggregatedSections::OnlyTotalDuration => {}
322 };
323 let cells = headers
324 .iter()
325 .map(|header| {
326 format!(
327 "<td>{}</td>",
328 format_duration(cells.remove(header.as_str()))
329 )
330 })
331 .join("\n");
332
333 let features = unit.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 unit.name_ver(),
347 unit.target,
348 unit.duration,
349 )?;
350 }
351 write!(f, "</tbody>\n</table>\n")?;
352 Ok(())
353}
354
355fn to_unit_data(unit_times: &[UnitTime]) -> Vec<UnitData> {
356 let unit_map: HashMap<Unit, usize> = unit_times
358 .iter()
359 .enumerate()
360 .map(|(i, ut)| (ut.unit.clone(), i))
361 .collect();
362 let round = |x: f64| (x * 100.0).round() / 100.0;
363 unit_times
364 .iter()
365 .enumerate()
366 .map(|(i, ut)| {
367 let mode = if ut.unit.mode.is_run_custom_build() {
368 "run-custom-build"
369 } else {
370 "todo"
371 }
372 .to_string();
373 let unlocked_units: Vec<usize> = ut
377 .unlocked_units
378 .iter()
379 .filter_map(|unit| unit_map.get(unit).copied())
380 .collect();
381 let unlocked_rmeta_units: Vec<usize> = ut
382 .unlocked_rmeta_units
383 .iter()
384 .filter_map(|unit| unit_map.get(unit).copied())
385 .collect();
386 let aggregated = aggregate_sections(ut);
387 let sections = match aggregated {
388 AggregatedSections::Sections(mut sections) => {
389 if let Some((_, section)) = sections.last()
396 && section.end < ut.duration
397 {
398 sections.push((
399 "other".to_string(),
400 SectionData {
401 start: section.end,
402 end: ut.duration,
403 },
404 ));
405 }
406
407 Some(sections)
408 }
409 AggregatedSections::OnlyMetadataTime { .. }
410 | AggregatedSections::OnlyTotalDuration => None,
411 };
412
413 UnitData {
414 i,
415 name: ut.unit.pkg.name().to_string(),
416 version: ut.unit.pkg.version().to_string(),
417 mode,
418 target: ut.target.clone(),
419 start: round(ut.start),
420 duration: round(ut.duration),
421 rmeta_time: ut.rmeta_time.map(round),
422 unlocked_units,
423 unlocked_rmeta_units,
424 sections,
425 }
426 })
427 .collect()
428}
429
430fn aggregate_sections(unit_time: &UnitTime) -> AggregatedSections {
432 let end = unit_time.duration;
433
434 if !unit_time.sections.is_empty() {
435 let mut sections = vec![];
441
442 let mut previous_section = (
445 FRONTEND_SECTION_NAME.to_string(),
446 CompilationSection {
447 start: 0.0,
448 end: None,
449 },
450 );
451 for (name, section) in unit_time.sections.clone() {
452 sections.push((
455 previous_section.0.clone(),
456 SectionData {
457 start: previous_section.1.start,
458 end: previous_section.1.end.unwrap_or(section.start),
459 },
460 ));
461 previous_section = (name, section);
462 }
463 sections.push((
466 previous_section.0.clone(),
467 SectionData {
468 start: previous_section.1.start,
469 end: previous_section.1.end.unwrap_or(end),
470 },
471 ));
472
473 AggregatedSections::Sections(sections)
474 } else if let Some(rmeta) = unit_time.rmeta_time {
475 AggregatedSections::OnlyMetadataTime {
477 frontend: SectionData {
478 start: 0.0,
479 end: rmeta,
480 },
481 codegen: SectionData { start: rmeta, end },
482 }
483 } else {
484 AggregatedSections::OnlyTotalDuration
486 }
487}
488
489static HTML_TMPL: &str = r#"
490<html>
491<head>
492 <title>Cargo Build Timings — {ROOTS}</title>
493 <meta charset="utf-8">
494<style type="text/css">
495:root {
496 --error-text: #e80000;
497 --text: #000;
498 --background: #fff;
499 --h1-border-bottom: #c0c0c0;
500 --table-box-shadow: rgba(0, 0, 0, 0.1);
501 --table-th: #d5dde5;
502 --table-th-background: #1b1e24;
503 --table-th-border-bottom: #9ea7af;
504 --table-th-border-right: #343a45;
505 --table-tr-border-top: #c1c3d1;
506 --table-tr-border-bottom: #c1c3d1;
507 --table-tr-odd-background: #ebebeb;
508 --table-td-background: #ffffff;
509 --table-td-border-right: #C1C3D1;
510 --canvas-background: #f7f7f7;
511 --canvas-axes: #303030;
512 --canvas-grid: #e6e6e6;
513 --canvas-codegen: #aa95e8;
514 --canvas-link: #95e8aa;
515 --canvas-other: #e895aa;
516 --canvas-custom-build: #f0b165;
517 --canvas-not-custom-build: #95cce8;
518 --canvas-dep-line: #ddd;
519 --canvas-dep-line-highlighted: #000;
520 --canvas-cpu: rgba(250, 119, 0, 0.2);
521}
522
523@media (prefers-color-scheme: dark) {
524 :root {
525 --error-text: #e80000;
526 --text: #fff;
527 --background: #121212;
528 --h1-border-bottom: #444;
529 --table-box-shadow: rgba(255, 255, 255, 0.1);
530 --table-th: #a0a0a0;
531 --table-th-background: #2c2c2c;
532 --table-th-border-bottom: #555;
533 --table-th-border-right: #444;
534 --table-tr-border-top: #333;
535 --table-tr-border-bottom: #333;
536 --table-tr-odd-background: #1e1e1e;
537 --table-td-background: #262626;
538 --table-td-border-right: #333;
539 --canvas-background: #1a1a1a;
540 --canvas-axes: #b0b0b0;
541 --canvas-grid: #333;
542 --canvas-block: #aa95e8;
543 --canvas-custom-build: #f0b165;
544 --canvas-not-custom-build: #95cce8;
545 --canvas-dep-line: #444;
546 --canvas-dep-line-highlighted: #fff;
547 --canvas-cpu: rgba(250, 119, 0, 0.2);
548 }
549}
550
551html {
552 font-family: sans-serif;
553 color: var(--text);
554 background: var(--background);
555}
556
557.canvas-container {
558 position: relative;
559 margin-top: 5px;
560 margin-bottom: 5px;
561}
562
563h1 {
564 border-bottom: 1px solid var(--h1-border-bottom);
565}
566
567.graph {
568 display: block;
569}
570
571.my-table {
572 margin-top: 20px;
573 margin-bottom: 20px;
574 border-collapse: collapse;
575 box-shadow: 0 5px 10px var(--table-box-shadow);
576}
577
578.my-table th {
579 color: var(--table-th);
580 background: var(--table-th-background);
581 border-bottom: 4px solid var(--table-th-border-bottom);
582 border-right: 1px solid var(--table-th-border-right);
583 font-size: 18px;
584 font-weight: 100;
585 padding: 12px;
586 text-align: left;
587 vertical-align: middle;
588}
589
590.my-table th:first-child {
591 border-top-left-radius: 3px;
592}
593
594.my-table th:last-child {
595 border-top-right-radius: 3px;
596 border-right:none;
597}
598
599.my-table tr {
600 border-top: 1px solid var(--table-tr-border-top);
601 border-bottom: 1px solid var(--table-tr-border-bottom);
602 font-size: 16px;
603 font-weight: normal;
604}
605
606.my-table tr:first-child {
607 border-top:none;
608}
609
610.my-table tr:last-child {
611 border-bottom:none;
612}
613
614.my-table tr:nth-child(odd) td {
615 background: var(--table-tr-odd-background);
616}
617
618.my-table tr:last-child td:first-child {
619 border-bottom-left-radius:3px;
620}
621
622.my-table tr:last-child td:last-child {
623 border-bottom-right-radius:3px;
624}
625
626.my-table td {
627 background: var(--table-td-background);
628 padding: 10px;
629 text-align: left;
630 vertical-align: middle;
631 font-weight: 300;
632 font-size: 14px;
633 border-right: 1px solid var(--table-td-border-right);
634}
635
636.my-table td:last-child {
637 border-right: 0px;
638}
639
640.summary-table td:first-child {
641 vertical-align: top;
642 text-align: right;
643}
644
645.input-table td {
646 text-align: center;
647}
648
649.error-text {
650 color: var(--error-text);
651}
652
653</style>
654</head>
655<body>
656
657<h1>Cargo Build Timings</h1>
658See <a href="https://doc.rust-lang.org/nightly/cargo/reference/timings.html">Documentation</a>
659"#;
660
661static HTML_CANVAS: &str = r#"
662<table class="input-table">
663 <tr>
664 <td><label for="min-unit-time">Min unit time:</label></td>
665 <td title="Scale corresponds to a number of pixels per second. It is automatically initialized based on your viewport width.">
666 <label for="scale">Scale:</label>
667 </td>
668 </tr>
669 <tr>
670 <td><input type="range" min="0" max="30" step="0.1" value="0" id="min-unit-time"></td>
671 <!--
672 The scale corresponds to some number of "pixels per second".
673 Its min, max, and initial values are automatically set by JavaScript on page load,
674 based on the client viewport.
675 -->
676 <td><input type="range" min="1" max="100" value="50" id="scale"></td>
677 </tr>
678 <tr>
679 <td><output for="min-unit-time" id="min-unit-time-output"></output></td>
680 <td><output for="scale" id="scale-output"></output></td>
681 </tr>
682</table>
683
684<div id="pipeline-container" class="canvas-container">
685 <canvas id="pipeline-graph" class="graph" style="position: absolute; left: 0; top: 0; z-index: 0;"></canvas>
686 <canvas id="pipeline-graph-lines" style="position: absolute; left: 0; top: 0; z-index: 1; pointer-events:none;"></canvas>
687</div>
688<div class="canvas-container">
689 <canvas id="timing-graph" class="graph"></canvas>
690</div>
691"#;