#
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 unblocked_units: Vec<usize> = ut
    377                .unblocked_units
    378                .iter()
    379                .filter_map(|unit| unit_map.get(unit).copied())
    380                .collect();
    381            let unblocked_rmeta_units: Vec<usize> = ut
    382                .unblocked_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                unblocked_units,
    423                unblocked_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"#;