cargo/core/compiler/timings/
report.rs

1//! Render HTML report from timing tracking data.
2
3use 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
20/// Contains post-processed data of individual compilation sections.
21enum AggregatedSections {
22    /// We know the names and durations of individual compilation sections
23    Sections(Vec<(String, SectionData)>),
24    /// We only know when .rmeta was generated, so we can distill frontend and codegen time.
25    OnlyMetadataTime {
26        frontend: SectionData,
27        codegen: SectionData,
28    },
29    /// We know only the total duration
30    OnlyTotalDuration,
31}
32
33/// Postprocessed section data that has both start and an end.
34#[derive(Copy, Clone, serde::Serialize)]
35pub(super) struct SectionData {
36    /// Start (relative to the start of the unit)
37    start: f64,
38    /// End (relative to the start of the unit)
39    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    /// When Cargo started.
50    pub start: Instant,
51    /// A rendered string of when compilation started.
52    pub start_str: &'a str,
53    /// A summary of the root units.
54    ///
55    /// Tuples of `(package_description, target_descriptions)`.
56    pub root_units: &'a [(String, Vec<String>)],
57    /// The build profile.
58    pub profile: &'a str,
59    /// Total number of fresh units.
60    pub total_fresh: u32,
61    /// Total number of dirty units.
62    pub total_dirty: u32,
63    /// Time tracking for each individual unit.
64    pub unit_times: &'a [UnitTime],
65    /// Concurrency-tracking information. This is periodically updated while
66    /// compilation progresses.
67    pub concurrency: &'a [Concurrency],
68    /// Recorded CPU states, stored as tuples. First element is when the
69    /// recording was taken and second element is percentage usage of the
70    /// system.
71    pub cpu_usage: &'a [(f64, f64)],
72    /// Compiler version info, i.e., `rustc 1.92.0-beta.2 (0a411606e 2025-10-31)`.
73    pub rustc_version: &'a str,
74    /// The host triple (arch-platform-OS).
75    pub host: &'a str,
76    /// The requested target platforms of compilation for this build.
77    pub requested_targets: &'a [&'a str],
78    /// The number of jobs specified for this build.
79    pub jobs: u32,
80    /// Fatal error during the build.
81    pub error: &'a Option<anyhow::Error>,
82}
83
84/// Writes an HTML report.
85pub(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    // It helps with pixel alignment to use whole numbers.
97    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
117/// Render the summary table.
118fn 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
200/// Write timing data in JavaScript. Primarily for `timings.js` to put data
201/// in a `<script>` HTML element to draw graphs.
202fn 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
223/// Render the table of all units.
224fn 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    // Make the first "letter" uppercase. We could probably just assume ASCII here, but this
229    // should be Unicode compatible.
230    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    // We can have a bunch of situations here.
240    // - -Zsection-timings is enabled, and we received some custom sections, in which
241    // case we use them to determine the headers.
242    // - We have at least one rmeta time, so we hard-code Frontend and Codegen headers.
243    // - We only have total durations, so we don't add any additional headers.
244    let aggregated: Vec<AggregatedSections> = units
245        .iter()
246        .map(|u|
247            // Normalize the section names so that they are capitalized, so that we can later
248            // refer to them with the capitalized name both when computing headers and when
249            // looking up cells.
250            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        // This is a bit complex, as we assume the most general option - we can have an
306        // arbitrary set of headers, and an arbitrary set of sections per unit, so we always
307        // initiate the cells to be empty, and then try to find a corresponding column for which
308        // we might have data.
309        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    // Create a map to link indices of unlocked units.
357    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            // These filter on the unlocked units because not all unlocked
374            // units are actually "built". For example, Doctest mode units
375            // don't actually generate artifacts.
376            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                    // We draw the sections in the pipeline graph in a way where the frontend
390                    // section has the "default" build color, and then additional sections
391                    // (codegen, link) are overlaid on top with a different color.
392                    // However, there might be some time after the final (usually link) section,
393                    // which definitely shouldn't be classified as "Frontend". We thus try to
394                    // detect this situation and add a final "Other" section.
395                    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
430/// Aggregates section timing information from individual compilation sections.
431fn aggregate_sections(unit_time: &UnitTime) -> AggregatedSections {
432    let end = unit_time.duration;
433
434    if !unit_time.sections.is_empty() {
435        // We have some detailed compilation section timings, so we postprocess them
436        // Since it is possible that we do not have an end timestamp for a given compilation
437        // section, we need to iterate them and if an end is missing, we assign the end of
438        // the section to the start of the following section.
439
440        let mut sections = vec![];
441
442        // The frontend section is currently implicit in rustc, it is assumed to start at
443        // compilation start and end when codegen starts. So we hard-code it here.
444        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            // Store the previous section, potentially setting its end to the start of the
453            // current section.
454            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        // Store the last section, potentially setting its end to the end of the whole
464        // compilation.
465        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        // We only know when the rmeta time was generated
476        AggregatedSections::OnlyMetadataTime {
477            frontend: SectionData {
478                start: 0.0,
479                end: rmeta,
480            },
481            codegen: SectionData { start: rmeta, end },
482        }
483    } else {
484        // We only know the total duration
485        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"#;